Talking Shop: Learning to test? Go backward.
- Chris Wallace
- Mon Nov 14 2016
Recently I’ve been getting back into writing unit and integration tests for my code and learning how to write tests for ES6 code (coming from a .net background). When I first started learning to test it was no easy concept, but I gained some good tips and processes along the way.
Regardless of whatever language you’re writing code in, this pattern works regardless. This is a very common approach so if you already know how to write tests then I’m preaching to the choir. But for those new to automating their testing process, this post is for you.
A couple of initial pointers…
Identify your ‘unit’
Remember, a unit test is a piece of code that runs within a small subset of your solution, this is usually a class or a function. Either way that ‘unit’ is isolated from external factors, you should always be checking the outcome of the ‘unit’ against external factors you give it (parameters, dependencies etc).
For example — a multiplier function.
Imagine a function which multiplies two numbers and returns the answer. Your test result is the answer and your test data are the two numbers. One good example of a test would be ‘when I provide the numbers 2 and 2, the multiplier function should return 4′.
Regardless of how complex your ‘unit’s internal workings are, the input and output help simplify that ‘unit’s behavior.
Use real world language initially
When I first started to learn to test I did it in a BDD style using a C# implementation of gherkin language called specflow. Specflow is powerful by itself but that’s another discussion, the initial advantage I found from writing my tests BDD style is it forced me to put my tests into plain old english. You have to physically describe what the scenario your ‘unit’ is being placed in. When it comes to writing the actual test code this is great because you have to ask yourself ‘is my test code actually doing what my original specflow statement was?’
For example, let’s return to our multiplier function.
Given I provide the numbers ‘2′ and ‘2′ to the multiplier.
When the multiplier multiplies it’s numbers.
Then the multiplier should return the answer ‘4′.
Look’s stupidly simple right? When I first started writing scenarios it felt ‘dumb’ (like ‘no sht Sherlock. Of course 22 is 4’). It’s natural for there to be more test code than the actual code you’re testing. But that’s the purpose of a unit test, take a ‘unit’ and apply extensive scenarios against it (in isolation).
Okay so now I’m going to walk you through how to get this scenario written as an actual test. And the best advice I can give you is, go backward!
What do I mean by this? Write that ‘Then’ test code first. After that insert the ‘When’ test code. Finally, insert your ‘Given’ test code. Here’s the method in the madness…
The ‘Then’ test code
This is our ‘assertion’ it’s the final comparison/check/verification. It’s what will either make our test pass or fail. The ‘Then’ test code is declarative, it’s a statement of your expectation of your code.
The following is a one line assertion using a library called ‘chai’ (if you’re testing JavaScript then I recommend looking into it at some point, but don’t get overwhelmed too soon).
expect(4).to.equal(4);
Again, looks stupid simple right? Of course, 4 is going to always equal 4. This line by itself achieves nothing. But it’s a statement, it says “our mission in this test is to make our ‘unit’ return us the value of 4. We need to get 4 back as our test result”. It gives your test a purpose, and you as the tester ensures your attention is on the test result as you’re writing the test (which is why we wrote this line first).
In our example it’s simple, but in reality, your ‘unit’ may have multiple outcomes (e.g. maybe your ‘unit’ returns an object with different properties). So now we have our end goal, we need to get there.
The ‘When’ test code
This is our ‘action’, it’s our actual ‘unit’ being put through its paces to give us a result. Let’s return to our JavaScript test code.
import multiplier from ‘./multiplier’
const actualResult = multiplier(2,2);
expect(actualResult).to.equal(4);
So what’s been added? At the top of our code, I’ve imported the ‘unit’ multiplier that I want to test (this is ES6, but essentially all we’re saying is “my ‘unit’ happens to live in a dedicated JavaScript file, so I need to import it”).
Now you see I’ve included a const called actualResult, this is our test result and as you can see I’m saying the value of actualResult should be whatever multiplier returns. The multiplier itself has our two parameters, which is our test numbers 2 and 2. Finally, I’ve updated my assertion to assert against actualResult rather than a hard-coded value.
Usually, the when statement is a one-liner, it’s the execution of your actual production code. We’re nearly there, finally let’s do some setup and ceremony.
The ‘Given’ test code
The last part of the test is the ‘arrangement’, it’s setup and ceremony. Getting our test data/dependencies in the right state.
Going back to our example…
import multiplier from ‘./multiplier’
const firstNumber = 2;
const secondNumber = 2;
const actualResult = multiplier(firstNumber,secondNumber);
expect(actualResult ).to.equal(4);
What have I done here? I’ve set our two test numbers ‘2′ and ‘2′ outside the ‘action’, rather than as hard-coded parameters. This means I’ve decoupled the test data from the execution. Why is this important? Well from this example it’s basic, but in practice, the ‘Given’ step can be 80% of your test writing. In the scenario above, we have only two parameters we to setup. But imagine if our multiplier had other dependencies (say a web service?), we would need to setup these dependencies as well so our multiplier runs in the correct manner. This introduces us to ‘test doubles’ (mocks, stubs and spies) which I’m not going to introduce in this post.
The rule around the ‘arrangement’ is to always treat it as your setup step “get the planets to align just right”.__ The ‘arrangement’ code is a good measure of how many external dependencies your code requires to get it to run.__ If you’re having to set up too many external dependencies, maybe you should be refactoring your code to separate levels of abstraction.
We’re nearly done but to finish, I’m going to do one last change…
import multiplier from ‘./multiplier’
// Arrange
const firstNumber = 2;
const secondNumber = 2; __ const expectedResult = 4;__
// Act
const actualResult = multiplier(firstNumber,secondNumber);
// Assert
expect(actualResult ).to.equal(expectedResult);
What I’ve done is made expectedResult a separate const and placed it next to my test data and part of my test setup. My ‘arrangement’ for the scenario is now completely decoupled from the ‘action’ and ‘assertion’. The ‘action’ and ‘assertion’ are now dynamic and in a reusable state. Why is this great? We can reuse this code to test multiple combinations of inputs and outputs for even more tests! We can go further by testing larger numbers, negative numbers, decimals and even illegal values. The ‘arrangement’ changes but the ‘action and ‘assertion’ remain’s the same.
Bonus round: Make it fail!
Now you’ve written a test you need a sanity check. If you ran this you will get a pass, which is great you’ve proved your code works. But have you? In reality, you’ve proved your code “doesn’t fail” (there is a difference). The trick is to make sure your code also fails when given ‘the wrong’ expectedResult. If we made the following change
const expectedResult = 5;
Then we expect the test to fail (2*2 doesn’t equal 5). If the test still passes, we know we’ve got a problem and need to revisit our production code, if it fails we know we’ve achieved what we wanted to achieve with the test!
It’s also a good idea to try multiple combinations of test data. For example…our underlying implementation for multiplier could look like this…
const multiplier = (first, second) => { return first + second; }
Doesn’t look very correct does it? True our test passes (2+2 does equal 4). But what if we wrote the following test?
// Arrange
const firstNumber = 3;
const secondNumber = 3;
const expectedResult = 9;
// Act
const actualResult = multiplier(firstNumber,secondNumber);
// Assert
expect(actualResult ).to.equal(expectedResult);
Suddenly our test yells fail! (actualResult is 6, we expected 9). Thanks to having another set of test data we can revisit our underlying implementation and correct it accordingly.
const multiplier = (first, second) => { return first * second; }
The lesson to take from this is, identify test scenarios and test data. The more edge cases and scenarios you have tested, the more confident you can be that your code is doing what it says it does.
Congratulations! You’ve just written your first coherent unit test and did it in reverse (feel free to brag). Of course, that was the simplest of simple tests, but this pattern will hold true regardless of whatever you have to test in the future. The real complexity is in your ‘arrangement’ and ‘assertions’, but with experience, you’ll learn how to these steps as painless as possible.
Good luck and go test all your code!