How to write a unit test

Wednesday, May 15, 2013

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:

  1. The method I’m testing - not description, scenario just the name of the method
  2. The scenario I’m testing
  3. 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:
image

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


  1. So what did we do:
  2. Decided what to test
  3. Write the method under test
  4. Add assertion
  5. Run the test
  6. Add more code
  7. Repeat steps 4,5 if necessary
  8. 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…

4 comments

  1. So how would you test if the email was sent if you didn't use an interface? Why is marking a method as virtual better than using interfaces?

    ReplyDelete
  2. You test using the same mocking tools. You can rewrite virtual methods on objects with any good mocking tool. My personal favorite combination is MSpec+Machine.Fakes it makes it like ice cream at how smooth it becomes to mock things.

    The next major reason using concrete objects with virtual methods is substantially better than IEmailClient. It simplifies dependency injection. With a concrete object, you will not have to tell any decent IOC container any single thing about your object. It will just use constructor injection as needed because it knows it needs to do new MyEmailClient(), it doesn't need a mapping of IEmailClient to EmailClient (even though lots of containers can guess these things for you)

    One less code file to maintain. Add in having xml documentation on methods, it now has 2 places it needs to be maintained also.

    The last simple reason is that it's more honest. As I talked about IEmailClient isn't being used to define a contract used by many services. By using a concrete class you acknowledge there's a dependency here and the standard implementation is directly in line without impacting testing negatively.


    I was guilty of this myself for years, only in the past year have i really come to understand this.

    ReplyDelete
  3. I get that. I suppose my real question was about just putting a facade over SmtpClient. As far as I recall you'd have to put a facade over it anyway to get virtual methods and not actually send an email when testing.

    ReplyDelete
  4. Actually you don't TRULY need to abstract SmtpClient. http://mikehadlow.blogspot.com/2010/01/configuring-smtpclient-to-drop-emails.html you can use configuration to make SmtpClient write to disk instead of actual SMTP. This might not cover every scenario, but it is generally sufficient. After I've started using SpecifiedPickupDirectory I've never again abstracted SmtpClient

    ReplyDelete

Newer Older
Related Posts Plugin for WordPress, Blogger...