Last week I had the pleasure of participating in Sela Developer Practice.
Before my session I sat through Gil ZIlberfeld’s session “7 steps for writing your first unit test” and I found myself thinking – what are steps I take when writing a new unit test?
I’ve been writing them for so long and never noticed I’ve been following a “methodology” of sort. And so without further ado – here is the steps I take when writing a new unit test:
The code under test
In order to write a unit test I’ll need an example and so I came up with the following scenario:
This is the latest and greatest bug tracking software and we need to add a feature – send an email when a bug with certain severity is created.
The code we want to test looks something (read: exactly) like this:
public class BugTracker
{
private readonly IBugRepository _repository;
private readonly IEmailClient _emailClient;
public BugTracker(IBugRepository repository, IEmailClient emailClient)
{
_repository = repository;
_emailClient = emailClient;
}
public Bug CreatNewBug(string title, Severity severity)
{
if (string.IsNullOrEmpty(title))
{
throw new ApplicationException("Title cannot be empty");
}
var newBug = new Bug
{
Title = title,
Severity = severity
};
SaveBugToDb(newBug);
// Here be code
return newBug;
}
And so since we’re avid TDD (Test Driven Design) practitioners we’ll start by writing the test first.
Decide what you’re testing
Although this might sound trivial – deciding what to test is a step that many developers tend to forget - instead they write a chunk of code and assert whatever they can.
I found that naming the test method in such a way that forces me (and my fellow developers) to think about what they are about to do:
[TestFixture]
public class BugTrackerTests
{
[Test]
public void CreatNewBug_CreateBugHasHighestSeverity_SendEmailToProjectManager()
{
}
}
I didn't invent this naming convention but I find it very useful. The name is divided into three parts:
- The method I’m testing - not description, scenario just the name of the method
- The scenario I’m testing
- What I expect to happen
The added benefit is that when this test would fail – all I need is to read the name of the test to know what went wrong – take that F5!
There are other similar naming schemas – choose one and be persistent about it.
So now I know what I’m about to test and the next step is to write exactly that.
Write the method under test
I’m starting from the bare minimum and building my test from the inside out.
First I’ll create the class I’m testing and then I’ll run the method I’m testing.
[Test]
public void CreatNewBug_CreateBugHasHighestSeverity_SendEmailToProjectManager()
{
var cut = new BugTracker();
cut.CreatNewBug("my title", Severity.OhMyGod);
}
Unfortunately this does not even compile. The problem is that I need to “feed” the BugTracker class two dependencies of type IBugRepository and IEmailClient – so let’s add them courtesy of an Isolation framework (in this case FakeItEasy):
[Test]
public void CreatNewBug_CreateBugHasHighestSeverity_SendEmailToProjectManager()
{
var fakeBugRepository = A.Fake<IBugRepository>();
var fakeEmailClient = A.Fake<IEmailClient>();
var cut = new BugTracker(fakeBugRepository, fakeEmailClient);
cut.CreatNewBug("my title", Severity.OhMyGod);
}
And now we can write the actual assertion.
Write the assertion
Since we need to check that our email client has sent a message we need to use the power of our isolation framework to assert exactly that
[Test]
public void CreatNewBug_CreateBugHasHighestSeverity_SendEmailToProjectManager()
{
var fakeBugRepository = A.Fake<IBugRepository>();
var fakeEmailClient = A.Fake<IEmailClient>();
var cut = new BugTracker(fakeBugRepository, fakeEmailClient);
cut.CreatNewBug("my title", Severity.OhMyGod);
A.CallTo(() => fakeEmailClient.Send("manager@project.com", "Don't Panic!")).MustHaveHappened();
}
Run the test
Now that we have all the parts placed it’s time to run the test and see it fails.
The test did fail but from the wrong reasons:
It appear I have a code inside the method “SaveBugToDb” that throws an exception:
private void SaveBugToDb(Bug newBug)
{
if (!_repository.Connected)
{
throw new ApplicationException("Cannot access bug repository");
}
_repository.Save(newBug);
}
This means going back to the drawing board for us.
Add more code
In order to make the test fail from the correct reason I’ll add one more line courtesy of our Isolation framework to make sure that a call to Connected will always return true:
[Test]
public void CreatNewBug_CreateBugHasHighestSeverity_SendEmailToProjectManager()
{
var fakeBugRepository = A.Fake<IBugRepository>();
A.CallTo(() => fakeBugRepository.Connected).Returns(true);
var fakeEmailClient = A.Fake<IEmailClient>();
var cut = new BugTracker(fakeBugRepository, fakeEmailClient);
cut.CreatNewBug("my title", Severity.OhMyGod);
A.CallTo(() => fakeEmailClient.Send("manager@project.com", "Don't Panic!")).MustHaveHappened();
}
Run the test (again)
No I run the test again and it fails on the assertion. If it wasn’t the case I would go back and add more code to make sure that the test follow the correct path until I’m satisfied that I’m testing the correct thing.
Write the code to make the test pass
This one is simple – just add the code that makes the test pass:
public Bug CreatNewBug(string title, Severity severity)
{
if (string.IsNullOrEmpty(title))
{
throw new ApplicationException("Title cannot be empty");
}
var newBug = new Bug
{
Title = title,
Severity = severity
};
SaveBugToDb(newBug);
if (severity == Severity.OhMyGod)
{
_emailClient.Send("manager@project.com", "Don't Panic!");
}
return newBug;
}
Conclusion
- So what did we do:
- Decided what to test
- Write the method under test
- Add assertion
- Run the test
- Add more code
- Repeat steps 4,5 if necessary
- Write the code that makes the test pass
A few comments before the end:
- This is nothing new if you’re been writing unit tests in the past – you’ve probably followed a similar process – I just needed to write is down explicitly.
- It seems like a lot of steps just to create a single test? Don’t despair - writing all of them should not take too long and besides – can you write a test without going through all of them in one way or another.
- I'm a true believer of writing the tests before the code but don’t worry this method sans step 7 will work even if you write your tests retroactively.
Happy coding…Labels: .NET, C#, FakeItEasy, Mock objects, NUnit, TDD, Tips and Tricks, Unit tests