I had an interesting moment with a team member this week. We were pairing on a new feature that included some calculations and I suggested that this feature would be a great candidate for writing some unit tests. Despite showing a bit of hesitation, or at least none of the enthusiasm that I have come to associate with an experienced test-driven developer, he agreed to try it with me.

As we opened up some of the existing tests in the project that covered similar calculations I noticed that they were hard to read.

The tests were spread out with one test case per class/file and the test inputs and outputs buried in a lot of syntactical ceremony.

[csharp]class When_converting_imperial_to_metric 
{
    void Should_have_expected_value()
    {
        result should be 23
    }

    void GivenThat()
    {
        mock unit.value to return 73
        mock unit.type to return Imperial
    }

    void WhenIRun()
    {
        result = calculator.Convert(unit, Metric)
    }
}
[/csharp]

I'm using pseudo-code here rather than actual syntax but I think it makes the point.

After some basic refactoring to reduce ceremony and improving readibility we managed to reduce this quite a bit.

[csharp]class When_converting { void 73_imperial_should_become_23_metric() { unit = new Unit(73, Imperial) calculator.Convert(unit, Metric) should be 23 } } [/csharp]

Before moving on I want to call out a few things about this revised test:

  • it expresses the entire test as a single 2-line method that can be read and understood in a single chunk;
  • we are using a literal component instead of the mocked one;
  • the specific inputs and expected outputs are written with less noise so they can be identified more quickly;
  • the logic is ordered as
    [Given] setup, [When] I act, [Then] I expect
    which more closely matches a common pattern of speech than the original
    Should (behave as expected) - Given (mocked setup) - When (I act)

In the end, as we leaned back and looked at this revised test, my initially reluctant partner told me how this test made him want to write more tests.

And here we come to my ah-ha! moment.

Testing should be fun

If your tests are not fun to write, you have a problem.

Actually, you have a couple problems.

First off you are going to have a hard time convincing less experienced members of your team to climb up the learning curve and join you in covering your ass with tests while you are all coding up a storm under the heat of that upcoming deadline.

Beyond that, if your tests are not fun to write, the rest of your code probably isn't either.

I've seen a high correlation between code that's not fun to write and code that is difficult or expensive to maintain, so if your team isn't having fun it's likely to cost your busines real dollars.

Okay, I believe fun is cheaper. Now what?

So just how do we make testing fun?

Here are a few things that I've seen work well.

1. make testing accessible

  • name and organize your tests so that they describe business value, not implementation details;
  • favor specific scenarios over generic ones as they are often easier to read and understand;
  • remember that there is no one right size of unit for a unit test - the right size is the one that brings your team the most value for the investment;
  • favor literal components over heavy mocking as it's more straightforward and can be a more realistic way of exercising your code;
  • introduce mocking only when needed to make tests fast or determinstic (see below) or else to ease setup;
  • I agree with Oren Eini on this one - I'd rather have 80 tests break at once and help me triangulate a problem than 100 tests that are so isolated they break one at a time.

2. make testing easy

  • invest constantly in removing friction from your tests;
  • ruthelessly drive out ceremony and repetitious setup and teardown so your devs can focus on verifying the business value of the test case at hand;
  • if tests are still hard to write review the code under test to see if responsibilities are broken out properly;

3. make testing fast and deterministic

  • the places to guard against coupling are at the latency boundaries or external dependencies of your code - database, internet, file system access, etc.
  • mocking here can increase the speed and predictability of your tests while reducing setup and teardown maintenance;
  • mocking inside those logical units, however, can make tests slow and cumbersome to write or debug.

4. make testing visible

  • the value of automated tests is when they provide tighter feedback loops that testers, stakeholders, and customers - run them often;
  • hooking up a continuous integration server to your source control system can ensure that tests are run on each checkin, nightly, or with whatever frequency makes sense for your team;
  • most continuous integration systems have some form of notifications or indicators that can signal team members when a test run passes or fails;
  • making the results of running your tests visible not only to individuals but across the team as a group can increase the accountability for team members to engage with tests and keep them passing;

In conclusion

If you have devs on your team who are reluctant to write tests, do some investigation.

Ask them why they don't write tests in your system.

I bet that if you can put aside your ego and listen to their answer you will find something that can be done to improve the experience of writing tests.

That in turn will lead to more participation in writing and maintaining tests and that, finally, will lead to more participation in writing and maintaining your software.