Changing how I structure unit tests
When I write unit tests, I use the BDD-style GIven/When/Then format so that the tests are descriptive and explain the business functionality that is being implemented. But I’ve recently changed the way that I do it.
Let’s say that I’m implementing bank account functionality and I am writing code to implement the following:
Given a bank account
When I deposit money into the account
Then the balance should increase by the amount of the deposit
The old way
I used to write them like this:
[TestFixture]
public class When_I_deposit_money_into_the_account : Specification
{
private Account _account;
public void Establish_context()
{
_account = Given_a_bank_account();
}
public void Because_of()
{
_account.Deposit(10m);
}
[Test]
public void Then_the_balance_should_increase_by_the_amount_of_the_deposit()
{
_account.Balance.ShouldEqual(10m);
}
private Account Given_a_bank_account()
{
return new Account();
}
}
This style is used by RSpec and many people in the .NET community. I’ve had a lot of success doing it this way, but I’ve always had some complaints.
- The Given/When/Then phrases are spread out and aren’t in order.
- We have the Specification base class, which isn’t bad but I think it might confuse new people who know NUnit but don’t know my special base class.
- If you have a lot of “given” statements, you’re tempted to use inheritance or nested contexts, each with their own level of setup, virtual properties and methods, etc. This is very unreadable and gets unwieldy very quickly.
The new way
This isn’t really a new way, but it’s new to me. I’ve seen it before over the years but I didn’t really start doing it until I joined my current team where they did it this way and I’ve come to like it a lot better than the old way.
[TestFixture]
public class Bank_account_tests
{
[Test]
public void Deposit()
{
Given_a_bank_account();
When_I_deposit_money_into_the_account();
Then_the_balance_should_increase_by_the_amount_of_the_deposit();
}
#region Helper methods
private void Given_a_bank_account()
{
_account = new Account();
}
private void When_I_deposit_money_into_the_account()
{
_account.Deposit(10m);
}
private void Then_the_balance_should_increase_by_the_amount_of_the_deposit()
{
_account.Balance.ShouldEqual(10m);
}
#endregion
}
The best thing about doing it this way is that the business functionality is clearly specified at the top of the class and the Given/When/Then statements aren’t spread out all over the place. There are no crazy inheritance hierarchies, base classes, or big setup methods. When I write tests, I just write out the Given/When/Then scenarios in plain text and then use Specs2Tests to generate the test code for me. Then all I have to do is fill in the private helper methods. This is really easy, like filling in the blanks.
Also, I typically hate regions but in this case I find that they work quite well because they hide the helper methods that I typically don’t want to see when I open the class file.
It depends
Obviously there are situations where this is all overkill and you can just write simple tests without Given/When/Then methods all over the place. Just do whatever makes sense. I’ve found that this new-to-me way leads to very readable and easy to maintain tests.
At the bottom of the triangle we have unit tests. These tests are testing code, individual methods in classes, really small pieces of functionality. We mock out dependencies in these tests so that we can test individual methods in isolation. These tests are written using testing frameworks like NUnit and use mocking frameworks like Rhino Mocks. Writing these kinds of tests will help us prove that our code is working and it will help us design our code. They will ensure that we only write enough code to make our tests pass. Unit tests are the foundation of a maintainable codebase.
I find that the testing triangle on most projects tends to look more like this triangle. There are some automated integration tests, but these tests don’t use mocking frameworks to isolate dependencies, so they are slow and brittle, which makes them less valuable. An enormous amount of manpower is spent on manual testing.