Imagine if someone submitted some production code for review, which had lots of magic numbers and magic text, lots of repetition, long methods etc. Would you say that code was OK? How much of your unit or integration test code is like that?

Seeing the wood for the trees

Part of the problem with test code is that there’s usually more of it than production code. One method of production code will often have several test methods that go with it. The production method will give rise to several test cases, and usually each test case is implemented in its own test method.

Assuming that you follow the common pattern of a test class per method under test, it should be relatively easy to work out which production method a given bit of test code is for. However, how can you work out what each test method is trying to do? Why are the tests incomplete without this test method, i.e. how is it doing something that the other test methods don’t do?

As a test author, part of your job is to help the reader of your tests answer these questions as easily as possible. This might seem like a counsel of perfection, but in the long run it will help you. You might be the reader of these tests, in six months’ time when you suddenly have to fix a high priority bug but have forgotten all about this code and its tests.

Someone else in your team will be reading your tests as part of reviewing your work. If you make it too hard to read and understand your tests, one of two bad things will happen:

The reviewer will make you re-write the tests to make them more readable; The reviewer will give up reviewing it properly as too hard, and bugs are more likely to slip through.

Who likes writing tests so much that they want to write a given set of tests twice? Who likes fixing bugs more than they like writing new stuff?

What this article isn’t about

I’m assuming that you’ve worked out what you need to test (the requirements your tests need to check), and I’m talking about just how you implement your tests. If you have a problem with working out the what, then I recommend mutation testing as one way of finding gaps in your tests.

Making your tests easier to read

Keep it DRY

First: don’t repeat yourself. If you find yourself doing the same basic thing in more than one test, make it a separate method and call it. So, if you have many tests that should produce an error, and they all end with calling the method under test (possibly passing many parameters) and then checking for an error code, make this a separate method. Your tests will then end with something like:

RunAndExpectError(param1, param2, …, paramN, expectedError);

There’s probably also repetition at the start of many tests, where you set up the data necessary for the test to run. Consider using one or more methods to create this data, that use default values for parameters. There’s more in the article linked to above, but your tests will change to having things like

PrepareData(customerStatus: inactive);

or

PrepareData(orderStatus: pending);

which makes it really obvious what’s important in the data for this test – the fact that the customer is inactive, or that the order is pending. The rest of the data will be using whatever default values have been set by PrepareData, so must be less important.

Value in your constants

Your production code (I hope) doesn’t have magic constants – arbitrary text or numbers like 1 or 17, such as

if (numOrders == 17)

Instead I hope it’s something more like

if (numOrders == MAX_ORDERS_FOR_SHIPPING_CONTAINER)

This has at least two advantages:

It makes it easier to read the code; It keeps things separate that happen to have the same value but for different reasons.

If you have

if (numOrders == 17)

…

if (shippingDate.AddDays(17) < deadline)

…

it’s hard to realise that these two 17s are different things. Something like this is much clearer:

if (numOrders == MAX_ORDERS_FOR_SHIPPING_CONTAINER)

…

if (shippingDate.AddDays(STANDARD_TARIFF_DELIVERY_DURATION) < deadline)

…

If the maximum number of orders that will fit into a shipping container changes, in the second one you’re much more likely to change just the value you want rather than all 17s in the code.

This is true for test code too. It’s common to have magic constants in test code, which has the same problems as in production code.

If a test contains

service.CreateCustomer(1, 0, “orange”);

it’s hard to see what’s important here. However, if it’s something like:

service.CreateCustomer(RANDOM_REGION_ID, 0, “orange”);

it’s clear that the value for the first parameter (region id) doesn’t really matter in this test. That points you towards the other two parameters as being important.

If you default every id to 1, then not only is it hard to tell things apart in the test (is this a customer id of 1, a region id of 1 etc?) but you are failing to test as aggressively as you reasonably can.

If instead you give each entity / class its own starting point for ids (e.g. 1,000 for accounts, 2,000 for regions etc.) then not only is it easier to tell things apart in the tests, the tests will catch bugs where the production code is using the wrong id – e.g. an account id where it should be using a region id. If all ids are 1, then your tests will pass rather than finding the bug and failing.

You only had one job…

The Single Responsibility Principle applies to tests too. If you neglect testing lower level things, like view models, then you may end up having to cram more stuff into the tests of higher level things, like controllers.

It’s tempting to not test the seemingly trivial stuff but is it OK if it’s got a bug in it? If you can be confident that e.g. a view model is created correctly, then a controller’s test can be simpler and so easier to read and to change.

Summary

Test code is usually the poor relation compared to production code. However, just as it’s often less effort in the long run to put effort up-front into writing your production code to a high standard, this is also often true of test code. It is still code, after all. Because there’s usually more test code than production code, there’s more to read, so it’s even more important that your tests are easy to understand.

Reduce repetition, reduce the number of magic constants, and apply the Single Responsibility Principle.