What is a unit test?

In this post I describe what units tests are all about. It seems to me that in the last many years, the focus has drifted away from unit tests and moved on to integration tests. This is likely driven by the emergence of (micro)services and web API's. However unit tests are still a great tool to have in the toolbox, even if you have an integration heavy architecture.

What are they?

A unit test is a test of a unit (duh). I would define a test as a procedure to assess whether a piece of code comply to certain criteria under specific circumstances. The unit part of unit test is harder to define. The smallest testable unit that we have in code are functions or methods. However in object oriented languages we also have objects. When working in object oriented codebases I like to see classes as a unit. In order to cover a class in tests several unit tests are often needed to cover all logical paths of it's public methods. The reason why I like to see a class as a unit is that it can have internal state, which may require you to call several methods on it to make a full test. Such as a list where you need to add something to the list before you can get something.

There are a couple of terms that are good to know when dealing with unit tests.. First of all there is the "test runner" (or "test bed"). Your test runner will often be known as your unit testing framework. This is the part that is responsible for running your tests and providing you with the results. Then there is the "unit under test" which we already covered - it is the unit we are testing.

Testing small parts of the codebase

Since our unit tests are testing a very small part of our codebase (a unit), it will be more easy to track down where the error occurs. Even with a stacktrace it can sometimes be hard to follow a long codepath. But when you test a small unit there is very little code you need to worry about. So when one of your unit tests fail it is easier to track down where the error is. You could also say that unit tests test less than integration tests and system tests - which is by design. Unit tests do not test integration points, so they do not prove that your codebase works when put together, as unit tests do not test the interactions between your classes. But they do prove that using specific input and under certain circumstances the unit behaves as it should.

Fast

Unit tests are also faster to execute than integration tests, this is due to them not having any integrations. For unit tests we stub out any endpoints or interactions with the outside world. Which is due to our test being run in isolation. This makes them execute fast which means we tend to run them more often. This gives us immediate feedback whether we have broken something or not when we refactor.

It is not just endpoints that are stubbed out. Interactions with the filesystem or threading is often attempted to be avoided as well. Using files should be seen as just any other integration. Testing with threads often ends up with having a lot of waiting time or sleeping in your code. Which again slows down your unit tests. Another reason that we want our unit tests to run fast, is that we end up having many of them. Some developers aim for 100% code coverage from unit tests. That is a lot of tests, which requires our tests to be fast if we wish to run them often.

Arrange, Act, Assert

I often call this "Assign, Act, Assert", but it is "Arrange, Act, Assert". These are words used to divide a unit test into these 3 sections. Usually implemented using comments. Often there is a setup (arrange) at the beginning of a test. Then some "acting" where the code is actually executed. After this we "assert" whether our acting worked as it should. This can be seen in my small board game example below:

[Test]
public void BoardGameReturns6WhenDiceReturns6WithMocks()
{
    //Arrange
    var dice = Substitute.For<IDice>();
    dice.Roll().Returns(6);
    var boardGame = new BoardGame(dice);
    //Act
    var result = boardGame.RollDice();
    //Assert
    Assert.AreEqual(6, result);
}

Often the Arrangement is the biggest part of the test. Here dependencies are mocked and stubbed. The acting part is often small as it is usually just a call to a function that may return something. Assertions are then done upon what the function returns. Sometimes several acts and asserts are repeated in a unit test. This can often be the case when we are testing for boundary values.

In my example above I use the mocking framework NSubstitute. In order to carry out your tests you may have to use several types of "test doubles". Which are different types of test implementations used for testing.

TDD and clean code

Unit tests are an essential part of Test Driven Development. When I first started testing I found that unit tests and TDD really helped me structure my code better. It gave me a great understanding of how to decouple classes and use constructor injection. It also made my code more SOLID. In order to make your testing efforts easier you often split up your classes into smaller units. When your class has a single responsibility it is easier to test. With TDD you focus on writing your test first. Which forces you to think about what the purpose (responsibility) of your classes are - even before you implement them. Even though I find myself writing tests first less and less, the learnings I have made from this approach sticks with me. It is my belief that unit tests can make your code more clean and maintainable.

If you are looking for a book on TDD I would recommend the following:


Isolation - solitary vs social unit tests

Something that I was not aware of for many years was solitary and sociable test approaches. When I started doing unit tests I learned that you should stub out all dependencies, so that you are testing your unit in total isolation. However Martin Fowler challenges this by saying that there are two ways of writing tests. One that is solitary - where your unit is not using any actual dependencies, but only ones made explicitly for testing. With sociable testing on the other hand, it is okay to use other actual implementations that you would normally use. You only stub out dependencies when they are too hard to use or have any network or filesystem access.

Wrapping it up

This was my post on what unit tests are and some of their surrounding aspects. I would like to underline that I do not think that, unit tests should stand on their own. Having a suite of integration or system tests is a must have if you have integrations to other systems. Think about it, which system does not have any integrations in these times? This does not make unit tests obsolete. They are great for testing small bits of logic and they are executed fast. Giving you fast feedback and very precise error tracking.

This was my post on unit tests, did I miss anything? Please let me know in the comments below!