Skip to content

Instantly share code, notes, and snippets.

@gregoryyoung
Created November 29, 2022 02:04
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save gregoryyoung/adcf117cbd809ffe08c365f1043b5c69 to your computer and use it in GitHub Desktop.
Save gregoryyoung/adcf117cbd809ffe08c365f1043b5c69 to your computer and use it in GitHub Desktop.
When teaching in person classes testing is my favourite section to get into. The types of systems we have been discussing are both quite easy to test and they offer quite a few interesting opportunities that are not really feasible in many other types of systems.
One of these opportunities is that we can get documentation about our system being produced from our tests. This is a highly valuable thing to have on a project, these style tests are often called Executable Specifications. These tests are not only used for the purpose of validating the system but also for verifying that the system is ... doing the right thing which we will get more into later. So with that let's jump right into ... testing.
Testing is an important aspect of any system we build. I think every developer can agree that a failure caught early in our process is significantly less expensive than a failure caught late in the process or in production. Our goal is to fail fast.
So let's start by looking at an aggregate and how we might write a very simple test for it then we will iterate to other ways of doing things.
'''code
[Test]
public void the_inventory_item_deactivates() {
var testId = Guid.NewGuid();
var item = getTestItem(testId);
item.Deactivate("some reason");
Assert.IsFalse(item.IsActivated);
Assert.Equals(item.DeactivationReason, "some reason");
}
'''
If you have been doing TDD for some time I am sure this style of test looks familiar. This is not a *bad* test. So what we are doing is we generate a UUID. We then have a small factory method which will handle creating an InventoryItem for us. Finally we call Deactivate on the InventoryItem then assert that the item is no longer activated and that the deactivation reasn was properly set.
This test does however has some "smells" associated with it. The first "smell" is that in order for this test to work I would need to have public getters on the object for IsActivated and DeactivationReason. As discussed prior getters are generally not preferable as once you have them people have a tendency of well ... using them. I do not feel strongly about their use in a test but they will nearly always start being used in many other places often where they should not be used.
There are some ways we could kind of bypass this such as making them internal or some languages allow exposing internals only to friends. Its much simpler however to just have them not exist as then we can be reasonably certain nobody is using them inappropriately.
The second smell though is a much more subtle one in that this test is only asserting that what it expected to happen happened. Something we expected to happen not happening is only one category of bug we can run into. There is another common type of bug which is much more insidious in that something we didn't expect to happen happened. These types of bugs are however much more difficult to deal with. Perhaps you will assert off all of the other 27 properties of the InventoryItem showing that they did not change? That's going to make all of our tests significantly longer and more brittle. There is a further concern here over how do we keep up with this as new properties are added over time.
There is of course a reasonably easy solution to this, you will just write a new framework that uses runtime reflection to allow you to state which things you expect to change and it will automatically check the difference as absolutely nothing could ever possibly go wrong with this ... you can likely even OSS it and give it a cool name.
Or ... we could change how our tests work. What if instead of asserting off the state of the domain object we had the domain object producing events and we asserted off of those events?
'''code
[Test]
public void the_inventory_item_deactivates() {
var testId = Guid.NewGuid();
var item = getTestItem(testId);
item.Deactivate("some reason");
var events = item.GetUncommittedEvents();
Assert.AreEqual(1, events.Count);
Assert.IsType<Deactivated>(events[0]);
Deactivated d = (Deactivated) events[0];
Assert.AreEqual(d.InventoryItemId, testId);
Assert.AreEqual(d.DeactivationReason, "some reason");
}
'''
This test did get a bit longer but it is no longer directly interacting with the internals of the domain object. There is also no longer a need for the domain object to expose its internal state via getters, the test only assets off of the events that were produced.
What would happen if you were change the internal structure of the InventoryItem? Will the test break? This test although it is seemingly dealing with state is actually a behavioural test, it will continue working and more importantly the object no longer needs to expose its internal state.
There is however a slight issue here as well. There is a lot of code in this test and much of this code is going to be essentially identical between many tests. Let's look at another test.
'''code
[Test]
public void the_inventory_changes_name() {
var testId = Guid.NewGuid();
var item = getTestItem(testId);
item.ChangeNameTo("ANewName");
var events = item.GetUncommittedEvents();
Assert.AreEqual(1, events.Count);
Assert.IsType<NameChanged>(events[0]);
NameChanged d = (NameChanged) events[0];
Assert.AreEqual(d.InventoryItemId, testId);
Assert.AreEqual(d.NewName, "ANewName");
}
'''
We are going to see a ton of duplication like this throughout our test code. So it would probably be wise of us to refactor this a bit. There are a few strategies we could take on this.
The most obvious and quickest strategy would be to come up with a better way of doing asserts. It could easily end up that the asserts on the event are 15-20 lines of code. A very simple strategy for dealing with this is not to directly assert each property of the event but to instead create an event which is the one you expect then override equals on the event itself.
As an example we could refactor the code above to look something like this.
'''code
[Test]
public void the_inventory_changes_name() {
var testId = Guid.NewGuid();
var item = getTestItem(testId);
item.ChangeNameTo("ANewName");
var events = item.GetUncommittedEvents();
var expectedEvent = new NameChanged(){InventoryId=testId, NewName="ANewName"}
Assert.That(events.Count == 1);
Assert.AreEqual(events[0], expectedEvent);
}
'''
This is definitely an improvement. If you go and add say a new property to the NameChanged event this test will continue working and find bugs that may exist so long as you also add that new property to the overriden equality check on the NameChanged event. Remember we might have 100 tests that use the event, doing things this way as opposed to needing to change all of those tests is likely a reasonable idea. There is even a good chance that you are even generating the actual event class from schema using one of the numerous tools that does this so this would in fact be essentially free.
But while reducing a few lines of code is certainly a good thing there is much much more that we can do. What if we were to instead try modeling our tests as classes instead of as functions.
'''code
public class the_inventory_changes_name : MyTestBase {
public override void Given() {
sut = new InventoryItem(12, "some product");
}
public override void When() {
sut.ChangeNameTo("foobaz");
}
[Test]
pulic void the_name_is changed() {
Assert.That(produced.OnlyContains<NameChanged>(x=>x.InventoryItemId == 12));
}
}
'''
In order to make this work we are going to need to write a small base class for our tests.
```code
[TestFixture]
public class MyTestBase {
protected DomainObject sut;
public abstract void Given();
public abstract void When();
[SetUp]
public void Setup() {
Given();
When();
}
}
```
Et voila! We can now write tests in the style provided. We have not really added much value in this though, we have just changed how we expressed things. There are long discussions to be had over which style people **prefer** but this is a relatively silly discussion as it is only based upon opinion not capability.
Perhaps thouh we can take this a bit further? An obvious place for a change here would be to no longer have Given return void as its a bit odd given the name and that it sets something in the base class. Who would really expect "Given" to not ... return something? :)
'''code
public class the_inventory_changes_name : MyTestBase {
public override DomainObject Given() {
return new InventoryItem(12, "some product");
}
public override void When() {
sut.ChangeNameTo("foobaz");
}
[Test]
pulic void the_name_is changed() {
Assert.That(produced.OnlyContains<NameChanged>(x=>x.InventoryItemId == 12));
}
}
'''
To make this work we will need to change our base class slightly.
'''
[TestFixture]
public class MyTestBase {
protected DomainObject sut;
public abstract void Given();
public abstract void When();
[SetUp]
public void Setup() {
sut = Given();
When();
}
}
```
This should will work and our tests can now just access "sut" (Subject Under Test) from the base class when they are doing asserts. If you try actually even using this just as it is it will work but unfortunately it will only work for your **successful tests**. This current form that we have will not work when we expect an exception. In fact it will be awful in terms of usage as not only wil the exception happen but the exception will happen in the setup phase of the test. If you have dealt much with testing tools they have a distinct tendency to not like it much when you throw an exception inside of your SetUp.
This begs the question of how would we write a test where we expect an exception? We cannot use something like [ExpectedException] with our test as the exception would be happening during the setup not during the execution of what the test runner considers the test itself.
The way we get around this issue is relatively simple, instead of letting the exception bubble out to the testing framework from the setup we catch it then allow the derived test assert off of it.
```
[TestFixture]
public class MyTestBase {
protected DomainObject sut;
protected Exception caught;
protected bool hasException;
public abstract void Given();
public abstract void When();
[SetUp]
public void Setup() {
try {
sut = Given();
When();
}
catch(Exception ex) {
hasException = true;
caught = ex;
}
}
}
```
Once we have done this we can in a derived class use it.
```code
public class the_inventory_item_changes_name_to_null : MyTestBase {
public override DomainObject Given() {
return new InventoryItem(12, "some product");
}
public override void When() {
sut.ChangeNameTo(null);
}
[Test]
pulic void an_argument_null_exception_is_thrown() {
Assert.IsTrue(hasException);
Assert.IsType<ArgumentNullException>(caught);
}
}
```
Of course when you are doing this in a production system you probably would want to add a little helper method here to do this, we will be doing this a lot! We probably want the helper method to check in one step that it has an exception and the exception is of the correct type. We might even want to provide an overload where we can pass in a Func<Exception, bool> which will be called to verify certain things about the exception as some custom exceptions might have data fields etc we also want to assert on.
If we wanted to do some "basic documentation work" off of our tests in this style we can. We could write a bit of reflection code to go through our test assembly. It would look for anything that was a [TestFixture] then look for all of the methods that were marked as a [Test]. We could then print the TestFixture name and the Tests under it with a tab indentation. This is literally +-20 lines of code. We would end up with something which looked like this.
```
When: the inventory item changes name to null
an argument null exception is thrown
```
We could even if we wanted to call the setup method and the asserting method to show whether it passed or failed.
```
When: the inventory item changes name to null
an argument null exception is thrown PASSED
```
But ... we are just getting started :-D Have you noticed that we up until this point 2000 words into the chapter have not yet discussed Event Sourcing which is kind of what this book is actually about?
We tend to test Event Sourced systems in exactly this way but with slight difference. Instead of our Setup returning us the Subject Under Test (SUT) we instead have the Setup return us ... a series of events. We will then use those events to build the Subject Under Test in our test base. This is in fact the exact same process we would use hydrating the aggregate from a repository when its actually running, we are just saying what the events it would get will be.
Let's try changing our TestBase to support using an Event Sourced aggregate.
```
[TestFixture]
public class MyTestBase<T> where T:Aggregate {
protected DomainObject sut;
protected Exception caught;
protected bool hasException;
public abstract IEnumerable<Event> Given();
public abstract void When();
[SetUp]
public void Setup() {
try {
var events = Given();
sut = new T();
sut.LoadFromHistory(events);
When();
}
catch(Exception ex) {
hasException = true;
caught = ex;
}
}
}
```
So now instead of getting the SUT from the derived test class we are instead getting the events from the derived class. We then create an instance of the domain object and pass the events to it. This would allow us to write a test such as the following.
```code
public class a_deactivated_inventory_item_throws_already_deactivated_exception_when_deactivated_again : MyTestBase {
public override IEnumerable<Event> Given() {
yield return new InventoryItemCreated(12, "pickle ice cream");
yield return new InventoryItemDeactivated(12, "I didn't like this one");
}
public override void When() {
sut.Deactivate("some reason");
}
[Test]
pulic void an_already_deactivated_exception_is_thrown() {
Assert.IsTrue(hasException);
Assert.IsType<AlreadyDeactivated>(caught);
}
}
```
You will need to modify your documentation generator slightly to handle the returning of the IEnumerable<Event> instead of receiving a domain object but you would end up with something like the following.
```
a deactivated inventory item throws already deactivated exception when deactivated again
Given:
Inventory Item id=12 name="pickle ice cream" was created
Inventory Item id=12 was deactivated because "I didn't like this one"
Then:
Exception: Already Deactivated
```
Of course in our documentation generator we could also grab the sut (we can just ask the fixture to build it) and use it in our documentation as well.
```
a deactivated inventory item throws already deactivated exception when deactivated again
Given:
Inventory Item id=12 name="pickle ice cream" was created
Inventory Item id=12 was deactivated because "I didn't like this one"
Result:
Inventory Item id=12 name="pickle ice cream" deactivated=true
Then:
Exception: AlreadyDeactivated
```
This does however seem to be missing something in terms of our documentation. Don't we generally like to know what the actual operation is? Currently we only know what the "When" is in our test based on the test name which is kind of annoying. In any actual project you can rest assured someone will have a class name that does not match what the test **actually does** which makes such documentation less than umm useful.
So how could we get the "When" to also show up in our documentation? Take a moment and think about it then turn the page.
GFY EDITORNOTE *THIS IS A PAGE BREAK TO PUT ON ODD NUMBERED PAGE REQUIRING A PAGE FLIP*
Did you think "well we could use a command instead"? You got it! What if instead of having our When() method actually make a call on the SUT which would be a real pain to parse out etc we were instead to return a command which was then applied to the SUT by a command handler?
I know this is starting to sound "less like a unit test" to many who are purists but there is a quite reasonable argument that it is in fact a valid boundary for tests such as these. We are testing the system from the input to output. We also by doing so gain a benefit of if we change how things are internally modeled even say breaking something apart into multiple aggregates internally our "unit tests" will continue working through this change.
So let's change our "test framework" to support this.
```
[TestFixture]
public class MyTestBase<T> where T:Aggregate {
protected DomainObject sut;
protected Exception caught;
protected bool hasException;
public abstract IEnumerable<Event> Given();
public abstract Command When();
public abstract Handles<Command> OnHandler();
public void GetFakeRepository() {
return new FakeRepository(Given());
}
[SetUp]
public void Setup() {
try {
var events = Given();
sut = new T();
sut.LoadFromHistory(events);
When();
}
catch(Exception ex) {
hasException = true;
caught = ex;
}
}
}
```
This will allow us to be able to pass a Repository to our Command Handler with the events specified in the test to be able to load up the aggregate. Note: I left out the generic argument here on the FakeRepository but it might be a FakeRepository<T> depending how precisely things are implemented, adding the generic argument is a relatively simple process if you need to.
This will allow us to rewrite our test so that it looks like the following.
```code
public class a_deactivated_inventory_item_throws_already_deactivated_exception_when_deactivated_again : MyTestBase<InventoryItem> {
public override IEnumerable<Event> Given() {
yield return new InventoryItemCreated(12, "pickle ice cream");
yield return new InventoryItemDeactivated(12, "I didn't like this one");
}
public override Handles<Command> OnHandler() {
return new DeactivateInventoryItemCommandHandler(GetFakeRepository());
}
public override Command When() {
return new Deactivate(12, "some reason");
}
[Test]
pulic void an_already_deactivated_exception_is_thrown() {
Assert.IsTrue(hasException);
Assert.IsType<AlreadyDeactivated>(caught);
}
}
```
Our test can now run. But more importantly we now have
Given
A series of events
When
A command
Expect
A series of events or an exception
As such we can modify our document generator discussed previously so that it can also now call When() and get the command this will change our output to.
```
a deactivated inventory item throws already deactivated exception when deactivated again
Given:
Inventory Item id=12 name="pickle ice cream" was created
Inventory Item id=12 was deactivated because "I didn't like this one"
Result:
Inventory Item id=12 name="pickle ice cream" deactivated=true
When:
Deactivate Inventory Item id=12 reason="who cares"
Then:
Exception: AlreadyDeactivated
```
Could a business person understand this? Could we print these out and staple them to the card we were working on? Could we have these automatically updating into an online site with a hierarchy associated where you could pick a SUT and expand out all of the tests covering it in this format? How fun would this be for a Saturday hackathon and how much value would it add?
Testing is a key part of our systems in terms of ensuring they are valid but we can also use our tests to communicate what our system actually does. We can use our tests to verify that the system is being built within expectations. Think not of your tests as being there to validate things, think of them as how you communicate what the system actually does.
GFY TODO Maybe add a few more paragraphs "in closing"?
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment