HOW TO WRITE A UNIT TEST
Let’s start with making this one thing perfectly clear.
Unit tests validate behaviour. NOT code.
Sounds simple enough, but is it?
To start with, the function you test is a black box. When you write the test, you don’t know what the code looks like. And that is important, because the reason unit tests exists is to tell you when your code stops doing what it is expected to do. It doesn’t matter how it does it, or how many times you change it, as long as it delivers the correct result. They are not a code review.
Which means that you are free to refactor the s**t out of it, and you will know the moment you do something wrong. You can basically do whatever you want with the code, as long as it delivers the expected result. As long as it behaves correctly.
As I said, you test behaviour.
How can you test a black box?
A valid question: “How can I test what I don’t see?” Well, you test what you can see. And that is what you send in to the function, like parameters in the function call, or what the function spits out, like return values.
You can also see the data sent to functions that are called from within the function under test, as well as what results these functions deliver back.
Ok, so you do have to know a little bit about the code. But that is actually a very well-defined part. You only know about code that reaches out of the function, code that collaborates with other units, and that is it. You still do not know of the code that does the actual work inside the function. You could say that you know what interfaces with the function.
You know what goes in and what comes out, but not what happens in-between.
You put fuel in the car, you push the throttle and it moves forward. What happens under the hood to make that happen, not our concern. Or compare with a human. You get to know people by talking to them, seeing how they react, body language and so on. You do not have to know what the brain looks like to do that. And still, you can get to know a person really well. Same thing goes with unit testing.
Bias
First a little note from our sponsor… (that’s me 🙂 )
I am a .Net developer. C# is my main language.
Unit testing on the other hand is for everybody.
Add these two and you get a text written for everyone but the code examples, the few there are, are in C#. I hope that won’t keep you from reading on.
What is this unit you keep mentioning?
After all the time I have worked with tests, all the articles I’ve read and all the YouTube movies I’ve seen, I think I sum my view of it like this. It is the smallest part of the code that you can test. It usually is the function. It does not include all the classes that this function call. There are ways to limit that, stubs and mocks (google it :-).
But despite what I just said, a unit can include functionality from other classes. You do that when it makes sense. Take C# strings as an example. They are classes. But their behaviour is so atomic that it is more of a hassle to mock them than to add them. So, if you have a function that adds two strings, by all means include the behaviour of the string objects. Make them a part of the unit.
And that goes for any class that you consider small, safe and crucial to the functionality enough. However, that is, at least when I code, rarely the case. But it happens. Ultimately, it’s your call.
I recommend that you have the mindset to not include them, and only do it when it really makes sense.
How do you write a unit test?
A test is always made up of a few steps. Setup, execution, verification and tear-down.
Setup is when you prepare the test, where you configure the code to be in the state that the test requires. For instance, you want to test what happens when you send in an empty list as a parameter. You create that empty list in the setup section.
Execution is when you actually run the code you are testing. Where you send in the empty list.
Verification is done immediately after execution, obviously, and is the part of the test where you see if the result of the execution produced the expected result or if there is a bug.
In the tear-down section you undo what you did in the setup, but in languages with garbage collection, almost all tearing down is done automatically for you.
How about an example? I use NUnit this time. In it you will see the word “SUT”, which is short for System Under Test and is a standard abbreviation that is used by many. It refers to the class that you are testing.
[TestFixture]
internal class ExampleTests
{
ClassUnderTest _sut;
// ==> This is the first of two parts where you do setup.
// ==> The function will be executed right before every test.
// ==> This is a common setup for all test in the class.
[SetUp]
public void Setup()
{
_sut = new ClassUnderTest();
}
// Function naming convention, version one.
public void Behaviour_ExpectedResult()
{
// Arrange
// ==> This section is the second part where you do the setup.
// ==> This is setup that is only for this test.
const int requestedUnknownId = -1;
const int expectedErrorCode = 3;
const int expectedErrorCode = 3;
// Act
// ==> Execute what you want to test here.
var result = _sut.FunctionUnderTest( idToFind: requestedUnknownId );
// Assert
// ==> And validate the result.
Assert.That( result, Is.EqualTo( expectedErrorCode ) );
}
// Function naming convention, version two.
public void This_function_does_this()
{
// Arrange
// Act
// Assert
}
}
Another thing you might have noticed are the three comment in the test function:
// Arrange, // Act and // Assert.
I find it a good practice to always include them in the tests. It makes reading the test easier because it splits it up and adds negative space which works really well for my brain. Now, I know that this is a personal preference, many consider this to be clutter. Your choice.
I also included two naming conventions for the test function. They are two commonly used naming models. More on that below.
Always write code readable. Choose readable before smart.
Tests are NOT production code. It is a helper to find bugs and it is documentation to show how the prod code works using code examples. And therefore, they have a little different set of code patterns, that makes the test code maximize it.
First of all. Readability is everything! Whatever you do, make the code as readable as possible. If the choice is between readable code and clever, choose readable. Always! Because, when you are in the middle of the code battle, halfway done, deadline approaching, and this nasty little bug shows its ugly little head, you do not want to start decoding yet another piece of code. You just want to know what the problem is and fix it, get it out of the way as soon as possible. The test should scream what it does to you. And remember, you write code for others to read. Don’t be rude and make them work extra.
Of course, there are a few techniques you can use to do this.
Naming conventions
By naming things in a certain way, you can quickly filter out what you are looking for. This is not written in stone, they are things that I have picked up over the time. If it doesn’t fit you, find your own way.
The test function, version one
The function name is made up of two camel case sections, separated with underscore, which together document the behaviour.
public void BehaviourToTest_ExpectedOutcome()
Behaviour to Test
A short camel cased sentence of what part of the function is being tested.
Examples: Adding, TheRequestedUserIsntFound and so on.
Expected Outcome
A short camel cased sentence telling you what the function is supposed to do.
Examples: TheSumIsReturned, ThrowsUserMissingException and so on.
The test function, version two
The function name is more of a sentence that describes the behaviour that is tested.
public void Adding_two_values()
public void The_requested_user_is_not_found()
The test function, version three
Also, I should mention that it was common to include the function name that is tested in the first version:
public void FunctionName_Behaviour_ExpectedResult()
The motivation is that you will find which function has the bug really quick when you run a test. But the test runner will tell you that.
Now, that also has the unfortunate effect that it also couples the test to the tested function. If you refactor and rename the function, you have to rename all the tests too. I prefer to leave it out. It saves a lot of work. You really want code that is as decoupled as you can.
It also clutters the code. Test function names tend to be quite long. And if you also add the function name, which also can be quite long, you have to have screen that is really wide to see the entire line without scrolling.
Sut
Sut is short for System Under Test, and as I mentioned before, this is the object you allocate to run your test on. This object contains the function you are testing.
Result
I always have the same name for the return value from the test. It is the result I want to test, so I call it just that. Never any doubt about what that means.
var result = sut.DoThis();
Dummy
Whenever you have a variable or constant that is not a part of the thing you are testing, but still has to be there, let the name start with ‘dummy’. Whenever you look through the code you will automatically filter them out, there’s no need to waste brain power on them. You just skip them.
For instance. You are testing that a function will report the correct error if you send in a negative value in the first parameter. The second parameter has to have an arbitrary value or the code will not execute, but you don’t care what it is since it isn’t part of the test. You just can’t leave it out.
var result = sut.DoThis( -1, dummyId );
Requested data
In contrast to dummy parameters, I name data that do matter in the function call as ‘requested…’ By using an explanatory variable, you make the code much more readable, than just using the value in itself.
const int requestedUserId = 42;
var result = sut.DoThis( requestedUserId );
Expected result
It is a good idea to put the results you expect in variables starting with ‘expected…’ for the same reason that you don’t use magic numbers in code.
What does -1 actually mean? But if you use
const int expectedErrorCodeUserMissing = -1;
instead, things get a whole lot clearer. And by prefacing the variable with expected, as before, you can single out the variable just by glancing at the code.
Named parameters
From the point of view of readability, it is often a good idea to use named parameters in the function call. I say often because that is really from case to case. But as soon as it makes things just a little clearer, use it.
For instance:
var result = sut.DoThis(requestedUserId: dummyId, requestedCompanyId: expectedId);
Much clearer than:
var result = sut.DoThis(dummyId, expectedId);
because you can’t see which of the two is the company ID you’re testing in the second example.
This also makes the code less vulnerable to refactoring, i.e. a decoupling technique. If you switch place of two parameters, the tests will break. But if you add named parameters, the compiler doesn’t have to infer what parameter is which by its place, you declare it explicitly instead, and you get less extra work when you change the code.
And what if you have two int parameters, the first is ID and the second is a counter. You switch the parameters in the function, the tests can’t see the difference and they will then use the counter as an ID and the ID as a counter. That is a bug in your tests, and it is not obvious.
Example
public void GetUser_RequestUser_ReturnsErrorCode()
{
// Arrange
const int requestedUserId = 42;
const int dummyId = -1;
const int expectedErrorCode = 3;
// Act
var result = _sut.GetUser( userId: requestedUserId, companyId: dummyId );
// Assert
Assert.That( result, Is.EqualTo( expectedErrorCode ) );
}
A test should never ever be depending on another test!
The test runner does not care what order the tests are executed in. So you have to design each test to be atomic. Each test has its own setup, and that setup is totally isolated from all other tests. They cannot be depending on each other.
If you use one test to do the setup for another test, you will get a headache, because sometimes the tests will work and sometimes not, all depending on which order you run the tests, or maybe if you just run one, or both. And you will not get any help from the system to what’s the problem.
Conclusion
Writing tests is a discipline. Do it the same time every time and you will always get good tests. But frankly, do it the same time every time and you will not have to think about how you write them. You can concentrate on the test logic instead. It becomes a skill.
Now go and write some tests!!
— Cheers!!