Make your test suite work for you
Have you tried to pick up testing before and determined it was a pain and that it only slowed you down? That’s probably because you wrote bad tests. Sadly, bad testing practices are all over the place and hopefully this article will improve current affairs.
A bad test looks a lot like this:
The problem space in the example is rather trivial. Can you figure out what is happening? For something so simple it should not be that hard.
Let’s say you are a new developer on my project and you broke something with this piece of business logic. There is no way you could immediately tell what went wrong. This is because the test compounds a bunch of different business rules into a single ‘it’ block. It’s the way a test is written when the developer wanted to get it over with quickly. Probably because they already knew how the service worked since the service was already written. The test was written as a way to solidify behaviour and not as a way to help the codebase.
When you write a test as an afterthought chances are high you will use a version of the AAA pattern for your tests. “Arrange Act & Assert”. Meaning that you will write one ‘it’ block containing the setup, of your test environment, the actual execution of your test and finally one or (usually) more checks to see everything went correctly. I’ve even seen tests where the service is mutated after a couple of assertions, followed by some more assertions. I find this “triple A” name to be very ironic as it’s the worst kind of test you can write.
It will however get the job done of testing that the code works as intended. Sadly, what you end up with is an unmaintainable test suite. One that you will hope to never have to touch again.
But guess what. Requirements change. If something breaks you’ll need to figure out which expectation or “Assertion” is actually failing. You’ll start by commenting all of it. Checking if the setup or “Arrangement” even succeeds. If it does you’ll start re-adding one expectation at a time until you actually see what goes wrong. Once you know what fails you’ll need to figure out what makes it fail. Which part of the setup code is actually used for the failing expectation. It’s not an easy task in a test like this.
When you change the business logic it’s possible another part of the service breaks, causing you to have to figure out which part of the test is failing now. And remember that this is just a small contrived example. Actual services are often much more complex.
Adding multiple expectations to a single ‘it’ block will increase the technical debt every single time. The time to develop will increase and the usefulness of your test suite will start working against you. Your will start feeling that testing effectively slows down your development. Especially when the requirements keep changing, causing your codebase to change and quite possibly your tests to start failing.
This, I think, is the reason why people believe that testing is not for them. And that is a shame because testing can be a very powerful tool to constantly assert that the business logic still behaves like you intend it to behave.
Spec-style testing
Let me show you how to change your testing practices so your tests will speed up development instead of slowing it down. Using this technique will clearly visualize how each single expectation is set up and exactly which part of your test suite is failing and what is still working. It’ll become a tool you’ll use before you start writing any business logic. It’ll help you think about your code before you start writing it.
First let’s see how a naive implementation would be written to get the desired output. This is not the final approach but I’m showing this so you can easily see how useful it is when we have every expectation separated.
Now when a single test fails it is obvious how the service was set up and it’ll speed up reasoning what might have gone wrong.
All you have to do is see which ‘it’ block failed, read what the developer intended to test here, read how the service was set up and what was called on the service. There will never be any logic that is not relevant to your specific expectation. Each ‘it’ block reads like a little scenario. To me this is much easier to comprehend than the compounded version inside a single ‘it’ block.
This example shows the desired output we want in our tests, however it also introduces a lot of duplication. We also can’t see if any of these tests are related to each other. To fix this we should leverage our testing framework. Almost all testing frameworks offer these or similar abilities.
By moving all the initialisation logic to ‘beforeEach’ hooks and nesting everything we removed most duplication from our test suite. This is ‘spec’-style instead of AAA-style testing. Because it reads more as a specification.
The test suite still describes the little scenarios separately from each other however it takes going up the ‘describe’ blocks to figure out all the steps necessary to set up the expectation.
This makes everything even more obvious. For example did you realise that we were also initiating the greeter with a strict option or for a given locale?
A new ‘describe’ block should be added whenever a new step is introduced to set up your service and to build up the state you want to eventually test on. In this case determining the language, building the greeter and even the actual execution of the service.
Doing this gives your tests room to expand and change alongside your requirements. Let’s say I want to start testing for a different locale. Or that I want to test a short name with the strict greeter. It’s obvious where to write these new expectations without impacting existing tests.
Separating the execution allows us to easily test multiple aspects of a single scenario. Let’s say I wanted to not do an equality check but check that the result contains the greeting and that it also contains the full name. I would write one ‘it’ block for the “Hi!” and one ‘it’ block for the full name. All inside that single ‘describe’ block. This way I could immediately see that either the greeting or the name is missing for the exact same scenario.
When a test fails it’s also immediately obvious what part of the service is failing. You can see exactly what is going wrong and what is still going right. However if something in the setup logic (aka in a ‘describe’ block) fails it will also notify you what part of the setup failed.
It’ll serve as documentation to a new developer or to yourself when you forgot how the code worked.
TDD
Most importantly this structure helps you to start doing TDD or “Test Driven Development”. As a developer you’ll first write out a spec describing the behaviour you know it should have. You’ll start asking yourself questions as to how it should build up the state and which expectations need to be made.
Reasoning about the interface or API of your business logic before actually writing it is very powerful. Imagine being halfway through implementing some business logic and noticing your API is actually pretty grueling to use. A change in your interface is necessary to clean it up. What will you do?
Reworking it will take time. You’ll also have to double-check that the logic you already wrote is still working as intended. Let’s say you are short on time (which is often the case on projects) so you end up pushing through, not changing the interface and thus introducing technical debt to the project.
Had you reasoned about the API before the implementation you’d have had an easy time changing the spec for the interface. You’ll notice bad API design sooner because you clearly map which kind of dependencies and interactions your service will need. It’s the equivalent of “think before you act” in the programming world.
And finally seeing a spec progressing from 100% failures to 100% passing in small steps is very gratifying and gives you an indication of progress. You’ll also know that every line of code is actually needed to pass the tests, making sure there is no unnecessary code to maintain in your codebase.
TL;DR
You should write your tests using hooks and nest each different rule to set up state for your test in ‘describe’ blocks. You should have each expectation be a new ‘it’ block. This will help your tests become understandable and expandable.
Writing spec-style tests will make your tests an asset to your project instead of something that weighs it down.