barbarian meets coding

WebDev, UX & a Pinch of Fantasy

The Art of Unit Testing 2nd Edition - a Barbaric Book Review

| Comments

With Barbaric Book Reviews I bring you interesting reviews and useful insights from awesome books that I have read. I also bring myself the magic of everlasting memory so that I don’t forget these tidbits of knowledge as I grow old and wither.

The Art Of Unit Testing book cover

A lot of what I know today about unit testing I learned from The Art of Unit Testing the first edition. That single book gave me such a strong foundation in unit testing that I have been able to apply the same basic principles in every environment/platform/language I have worked in afterwards. Even after reading many other books on unit testing, TDD, BDD… I always feel like these are just different sides or dimensions that stem, complete or expand things I learned in this book.

When I heard that @royosherove had written a reviewed version that addressed one of the issues that has been my biggest pain in the butt I could not resist: I had to read it.

TL;DR In its new edition, the art of unit testing subtly changes the definition of good unit tests from testing units of code in isolation to testing units of work. Testing units of work will increase the maintainability of your unit tests tenfold. The book remains being an awesome introduction to unit testing and will help you write more readable, maintainable and trustworthy tests.

On My Experience Writing Unit Tests, TDD and The Pain in the Butt

Throughout my years as a software developer [heavily inspired by the art of unit testing], the term of unit testing has meant one and only one thing to me: test a unit of code in isolation (obviating the automation part). Where unit of code would be a public method of a class, and in isolation would mean to fake all dependencies/collaborators:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// pseudocode-ish
public class StringCalculator{
    private readonly IArgumentParser parser;
    private readonly ILogger logger;
    private readonly IArithmeticCalculator calculator;

    public StringCalculator(IArgumentParser parser, ILogger logger, IArithmeticCalculator calculator) {...}
    public int Calculate(string arguments) {...}
}

public class StringCalculatorTests{

    [TestCase("+,1,3", "4")]
    public Add_WhenGivenASeriesOfAStringArguments_ShouldCalculateResult(string arguments, int expectedResult){
        // stub IArgumentParser: parse("+,1,3") => operation(+, [1,3])
        // stub anonymous ILogger (doesn't participate in this test)
        // stub IArithmeticCalculator: calculate(+, [1,3]) => 4
        // Assert expectedResult 4
    }

    // and so on
}

This notion of unit testing and the use of SOLID) and TDD has led me to write applications composed of very many loosely coupled, well-tested components. I probably started relying heavily on the ideas I learned from the art of unit testing, and the SOLID principles, but nowadays I probably rely the most on instinct and on being open to feeling the pain: if it hurts when testing, then it is wrong. If it hurts when testing, then it is too complex, there are too many dependencies, there are too many responsibilities being handled…

So this works great (like awesome) when you are writing a new project, a new application or a new feature where you yourself write most of the code from scratch. In this context it works flawlessly. You are going to end up with a bunch of very small components, that are loosely coupled, have a single responsibility and in a situation with a high degree of test coverage. Which in terms of actual benefits to you as a developer mean (and I am paraphrasing myself from Test Behavior, not implementation) that:

  • You have a great number of tests that:
    • ensure that the code behaves as you want it to behave
    • are fast to write, fast to run, give you a short feedback loop and provide good value/cost ratio
    • help you find bugs with super-high granularity (if a unit test shows a bug, you pretty much know which class it is in, you don’t need to go throw all classes nor debug it, that’s priceless)
    • increase your confidence to refactor
    • act as a documentation for your future self and other developers
    • allow you to know if something changes that affects the unit under test (regardless if it is a bug or a new implementation xD)
  • You have production code with:
    • very simple classes, high modularity and separation of concerns (because writing unit tests for fat, complex classes with tons of collaborators is horrible and we, humans, have a natural tendency to want to avoid pain). This basically means that you reuse more code, bugs propagate less, simple classes are easier to name thus the code is probably more readable, you can change an implementation without affecting collaborators (DIP), etc.

That’s Good!

Let’s move on to that pain in the butt I was talking about…

Pain in the Butt #1. Handling Change

First curve ball and the daily bread for a developer: Hello changing requirements!. Sometimes, based on how you have decided to design your system you will be able to extend an existing application’s functionality without the need of changing existing code (OCP – implement a contract defined by an interface with new implementation for instance). Another times, however, a changing or new requirement will be something that you hadn’t anticipated at all in the initial design, something that will be contrary, or the opposite to the very core premises of its existence. In those cases, you will most likely need to change your code and your tests. At this point, there can still be different degrees of change to be handled in different ways. At times you can isolate the need of change to a single class for instance:

  1. You can tackle this cowboy style and go straight to the production code, change at will, break existing tests and then rewrite former tests of write new ones as needed or,
  2. my method of choice (super proud for figuring this out myself XDD):
    1. Write a new test that reflects the new expected behavior
    2. Verify the test does not pass/compile (it would be pretty unexpected if it did xD the kind of thing that you really don’t want to happen)
    3. Write new implementation coexisting with the previous one
    4. Verify that the test now passes
    5. Remove old implementation and test
    6. Profit (and you had working code all along the way)

Other times… (travelling back in time)

Example from real life: A system for handling user notifications in an invoice automation software. Each user within the system has a default notification configuration that varies for each type of notification and each type of invoice/document within the system.

The existing domain model looked more or less like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// Domain model
public class User{
    //...stuff...
    IEnumerable<NotificationConfiguration> NotificationConfigurations {get;set;}
}

public class NotificationConfiguration{
    // type of notification
    public string NotificationType {get;set;}
    // collection of configurations per document type
    public IEnumerable<NotificationConfigurationRow> Rows {get;set;}
}

public class NotificationConfigurationRow{
    // type of document
    public string DocumentType {get;set;}
    // html message template to use when sending the notification
    public MessageTemplate MessageTemplate {get;set;}
    // send notifications of this type in a summary
    public bool SendInSummary {get;set;}
    // enable this notification
    public bool IsEnabled {get;set;}
    // notification details with additional configuration
    public NotificationDetails Details {get;set;}
}

and I started writing tests and production code to add the user default configurations

1
2
3
4
5
6
7
8
// Configuration
public interface INotificationConfigurator{
    AddConfiguration<TReminderType>(IEnumerable<string> typesOfDocuments,
                                    MessageTemplate messageTemplate,
                                    NotificationDetails details,
                                    bool IsEnabled = true,
                                    bool SendInSummary = false);
}

in time we iterated over this and wrote a fluent interface over this configurator that would allow to setup default configurations in a more easy and yet more versatile way:

1
2
3
4
5
6
7
8
9
10
11
12
configuration
    .AddNew<NewDocumentNotification, Document>()
        .WithMessageTemplate(newDocumentMessageTemplate)
    .Done()
    .AddNew<NewDocumentNofification, Invoice>()
        .WithMessageTemplate(newInvoiceMessageTemplate)
    .Done()
    .AddNew<BeforeDueDateNotification, Invoice>()
        .WithMessageTemplate(beforeDueDateNotificationMessageTemplate)
        .WithDetails(new DayConstraint { Days = 5 })
    .Done()
    .Commit();

And I wrote additional unit tests so that we had:

  • Unit tests for the fluent interface that would test that the fluent interface would work as a facade in the expected way and call the INotificationConfigurator appropriately. The INotificationConfigurator was used as a mock in these tests
  • Unit tests for the INotificationConfigurator that would test that when using the configurator we would store the expected configurations in the database via a repository. In this case the repository was a mock.

So now the context is set. The requirement given this scenario was to improve performance. As the number of users grew, the performance of the algorithm used to generate the notifications configurations went to hell. This required a pretty hardcore rewrite where the public API of IReminderConfigurator changed and thus we needed to re-write both code and tests.

What went wrong? Could I have written my tests in a more maintainable fashion so I wouldn’t have needed to rewrite them after doing the performance improvements? Hold on to that thought for a minute.

The Art of Unit Testing 2nd Edition

The Art Of Unit Testing book cover

With the second edition of The Art of Unit Testing @royosherove makes a subtle change in his definition of a good unit test that makes a world of difference in terms of improving the maintainability of your tests. Instead of a good unit test being a test of a unit of code (method/class) in isolation, a good unit test is a test of a unit of work in isolation where this unit of work is:

A unit of work is the sum of actions that take place between the invocation of a public method in the system and a single noticeable end result by a test of that system. A noticeable end result can be observed without looking at the internal state of the system and only through its public APIs and behavior.

Roy Osherove The Art of Unit Testing 2nd Edition

So by that definition, if we go back to my example, a unit of work would comprise testing from the public API of my system – the fluent interface – to a noticeable end result – storing the newly generated notifications in the DB via a repository. In my case, you could say that I was overspecifying my tests, and testing a middle ground that did not add so much value and that, in fact, caused my tests to be brittle and less maintainable. Had I written a test for the complete unit of work, the necessary changes to improve performance wouldn’t have required me to rewrite my unit tests.

title="Mindblooooown" alt="Mindbloooown"

And now you will be asking yourself? Did you really need a book to tell you that? :) In my defense I will say I almost got to the same conclusion, I was just much slower XDD.

Anyhow, it feels like this represents a tradeoff and duality, do you want to test each class in isolation, each single class responsibility, have more tests, more granularity in finding bugs? or do you want to test a unit of work and have more maintainable tests. I think I may just start testing how it works with the latter.

Aside from this update on the definition of a good unit test and how to apply it in practice, The Art of Unit Testing comes packed with new stuff and, more importantly, it still remains a cornerstone book to learn unit testing. You who haven’t done any unit testing before will begin your journey with strong foundations and learn how to write readable, maintainable and trustworthy tests. You who have been doing unit testing for a while, will surely learn a couple of new tricks.

Appendix on TDD-ing. Pain in the Butt #2. Playing the “Fake” Seer

Another problem I have experienced some times happens when I am doing TDD and writing code against existing parts of our codebase that I haven’t written myself. This has bitten me more than once… so I go and I make assumptions on how a class works when I am setting up your collaborators as stubs… write tests, write code, everything looks fine and all tests are green but when I actually run the code in production I discover that it doesn’t work at all: The assumptations that I had made regarding how those collaborators work were completely flawed.

Has this ever happened to you? I guess I just need to be more patient, and read the freaking source. :)

Comments