logo
Tutorialunittest

Basics of unit testing

Views: 1265 Created: 2019-05-20 Read time: 11 minutes
Post Preview
Tags:

1)     Preface

I believe that unit testing should be part of every entry-level coding program. It not only never is, but the concept is not really introduced anywhere until our technical lead underlines that there is a company policy that you have to write at least one unit test per new commit. This is sad, to say the least.

I have been in the industry for more than 10 years, and I know that every programmer (admit it) is dead sure that his solutions are the best and they will work like a charm. Confidence is like money, there is always extra room for more. That is excellent but stable, reliable, extensible and scalable systems do not have their traits because of unrelenting confidence of their creators. It is thanks to the high-quality source code that is safeguarded by an army of automated tests. There is no or minimal manual testing in Facebook. If all the test suites pass, it goes live baby.

There are many variations of automated tests, but the most important one is unit testing. In this tutorial, we will go through the basics of this beautiful art and try to prove why every software project should have them incorporated from the very beginning.

 

Note
Note

 

2)     Automated testing pyramid

Before we take a deep dive directly into the unit testing territory. Let us see how they relate to other types of tests:

 

Summary

 

Now what we have here is the unfortunate reality for a lot of projects. Most of them aren't even on that stage as there is no automation whatsoever. Throughout the years, only manual, functional tests have been done. Back in the past, that is how the test automation looked like for software projects. If a project was lucky enough to have any automation at all.

 

style='line-height:200%'>
Summary

 

Here we have the happy version of how the ratio of our tests should be. The vast majority of work put into validating the correctness of the code/features should be present in the form of unit tests. Thanks to that, we have covered all the logical paths contained in our functionality, and we know they work as units. We have got an oiled machine that can be run in a matter of seconds during any stage of development and tell us if we are on the green. IT and E2E tests come after that just to act as a comma for a well-formulated sentence.

 

Note
Note

 

3)     Traits of unit tests

Unit Test is an automated piece of code that invokes a unit of work in the system

and then checks a single assumption about the behaviour of that unit of work.

 

The traits of a unit test are the following:

       Fast: if a unit test is not fast.. there is something wrong because the primary purpose of a unit test is speed. How fast? Usually, a suite of a thousand tests should not take more than 5 seconds to run. JUnit 5 provides support for parallel and concurrent execution, so it is not that hard to reach these number nowadays. Thanks to that when you are developing a new change, at certain stages, you would just run the whole suite just to check if you haven't broken anything and will be like a blink of an eye (well almost).

       Quickly Developed: unit test should be dead simple when it comes to the structure so that the person looking at it instantly knows what is the deal here. If on the other hand, we have to analyse a unit test to understand it, then the person who wrote it, failed miserably.

       Easily Integrated with CI Process: if you haven't got at least a basic suite of unit tests and you are using tools like Jenkins then well, its like buying the most expensive sound system and then listening just to techno music. I don't have anything against techno (lol), but it's not the most sophisticated audio-wise music.. and probably a medium-range option would give the same results. With jazz, classical or rock you could get the full potential of that best audio set. Otherwise, it is just a waste of money.

       The base ingredient of high-quality code:  to be able to cover your code with unit tests (which are readable and maintainable) you need to have a good design. Otherwise it would just be hell to create a unit test suite... and even more, hell to maintain it. So if you want to have unit tests, you better take care of the design of your code.

       Living documentation of the code: I'm a big enemy of commenting the code as this is definitely something that is not much maintained. The code has been changed 10 times, but the comments around are still from the first change and its just misleading.
When you have unit tests, even when you are not sure what a method does, you just check out the suite and check out all the logical paths.

       Safety of refactoring: ease of refactoring is just a huge bonus which you will get after covering the method with tests. It is also yet another aspect that contributes to creating high-quality code.

 

Tip
Tip

 

4)     What unit test should not do

A unit test should test a given SUT in isolation. Building on that base principle, our suite should never do the following:

       Hit the database

       Hit the file system

       Connect to network

       Invoke threads

       Call non-trivial methods of dependent classes

 

Tip
Tip

 

Reference
Reference
unittest
Mockito stubbing

 

5)     Unit test structure

There is nothing special about unit tests. Each and everyone should look more or less the same. Here is an example structure:

 

Test Code


@Test
public void should<Result>_when<Action/Feature>_given<Input/State>() throws Exception {
    // Given

    // When

    // Then
}

 

A good practice is to always put the throws clause at the method definition because a test method is not a place for taking care of production code exceptions that are not expected by the test itself.

 

When it comes to the method name, it consists of :

       Given (optional): An optional part defining the initial state/input before the main feature is called.

       When(optional): defines the action/feature under test.

       Should: defines what should be outcome/state after the tested method has been invoked.

 

This structure follows the Behavioral Driven Design approach. We aim for expressing the feature of the class here, written in a way that introduces a certain level of abstraction. Don't focus on communicating how the method under test works precisely as we might introduce unnecessary implementation details.

 

Finally, we have the method body. I bet you can't spot any resemblance to the method naming:

       Given (optional): all the setup that needs to be done before the SUT method can be executed and follow a particular logical path goes here. This part is not always necessary.

       When: SUT method is invoked here.

       Then: here we check if everything went fine using assert statements.

 

6)     Assertions in unit testing

6.1) Resulting State

 

This is the most common pattern where we simply check the resulting outcome/state after the SUT method has been called. This is used probably in around 70-80% of the cases.

 

Test Code


@Test
public void shouldObjectBeStored_whenAddingToTheList() throws Exception {
    // Given
    String unitStr = "Unit";
    List<String> strings = new ArrayList<String>();

    // When
    strings.add(unitStr);

    // Then
    assertThat(strings).contains(unitStr));
}

 

6.2) Delta assertion

 

Used when we do not care about the actual result. We just want to check what was the amount/level of change after the SUT method has been called. This strategy is used in no more than 5% of the cases.

 

Test Code


@Test
public void shouldIncrementSize_whenAddingObjectToList() throws Exception {
	// Given
	List<String> strings = new ArrayList<String>();
	int beforeSize = strings.size();

	// When
	strings.add("Unit");

	// Then
	int afterSize = strings.size();
	assertThat("Size after adding", afterSize)
		.equalTo(beforeSize + 1));
}

 

6.3) Guard assertion

 

Used in situations when we want to make sure that the SUT is in an expected state before the method under test will get invoked. For example, we might want to make sure that the constructor sets the instance variables to proper values. This strategy is used in about 5-10% of the cases.

 

Test Code


@Test
public void shouldListNotBeEmpty_whenAddingObject() throws Exception {
	// Given
	List<String> strings = new ArrayList<String>();
	assertThat("List before adding", strings).isEmpty());

	// When
	strings.add("Unit");

	// Then
	assertThat("List after adding", strings).isNotEmpty();
}

 

6.4) Asserting exceptions

 

It is always a good practice to test the method regarding exceptional and border scenarios. Just as a reminder, we should write these tests at the very end, once we tested the happy paths first:

 

Test Code


@Test
public void shouldThrowException_whenRetrievingVisit_givenInvalidId() throws Exception{
    // Given
	Integer invalidId = Integer.valuesOf(-1);
    VisitServiceImpl visitServiceImpl = new VisitServiceImpl();

    // When
    assertThrownBy(() -> visitServiceImpl.getVisitById(invalidId))
		//Then
		.isInstanceOf(DataAccessExcpetion.class);
}

 

Tip
Tip

 

6.5) Behaviour verification

 

Sometimes, apart from checking the result of the method we would like to make sure it has interacted with a particular collaborator. We can achieve that with mocking and Mockito:

 

Test Code


@Test
public void shouldSendEmail_whenCreatingNewUserAccount() throws Exception {
	// Given
	Account newAccount = newAccount("login123", "pass123", "new.acc@mydomain.com");
	EmailService emailServiceMock = Mockito.mock(EmailService.class);
	AccountService accountService = new AccountService();
	accountService.setEmailService(emailServiceMock);

	// When
	accountService.createNewAccount(newAccount);

	// Then
	Mockito.verify(emailServiceMock).sendMail("new.acc@mydomain.com");
}

 

Reference
Reference
unittest
Mockito verification

 

 

7)     Unit testing: example

Let us try to write now a couple of tests for a feature which levels up our games Hero:

 

SUT Code


public class GameController {

    private GameEngine gameEngine;

    public void levelUp(Hero hero){
        hero.setLevel(hero.getLevel() + 1);

        if(hero.getLevel() % 10 == 0){
            hero.addSpell(gameEngine.generateSpecialSpell());
        }

        if(hero.getLevel() % 5 == 0){
            hero.addMoney(gameEngine.generateBonusMoney());
        }

        if(hero.getActiveBuff() == null){
            hero.setActiveBuff(gameEngine.generateRandomBuff());
        }
    }

In this SUT method, we are:

      (L7):  Raising the hero's level by one.

      (L9-11):  If a hero's level is 10,20,30 etc. then he is awarded a particular spell into his spellbook.

      (L13-15):  If the hero level is 5,10,15 etc. then he is awarded a bonus amount of money by the game.

      (L17-19):  If the hero does not have any active buffs during the level-up process, then is given a random one by the game engine.

 

Now it is time to write a basic test. Here we will make sure that during a level up hero gains exactly one level more:

 

Test Code


@Test
public void shouldIncreaseLevelByOne_whenGainingLevel() throws Exception{
	// Given
	Hero hero = new Hero();
	Integer initialHeroLevel = Integer.valueOf(14);
	hero.setLevel(initialHeroLevel);

	// When
	gameControllerSUT.levelUp(hero);

	// Then
	assertThat(hero.getLevel()).isEqualTo(initialHeroLevel + 1);
}

In this test method, we have taken advantage of the delta assertion.

Next, we will make sure that our hero gains only adequate buff during the levelling process:

 

Test Code


@InjectMocks
private GameController gameControllerSUT;

@Mock
private GameEngine gameEngineMock;

@BeforeEach
private void init(){
	MockitoAnnotations.initMocks(this);
}

@Test
public void shouldLevelUpWithoutBonusSpell() throws Exception{
	// Arrange
	Hero hero = new Hero();
	hero.setLevel(14);

	// Act
	gameControllerSUT.levelUp(hero);

	// Assert
	verify(gameEngineMock).generateBonusMoney();
	verify(gameEngineMock).generateRandomBuff();

	verify(gameEngineMock, never()).generateSpecialSpell();
}

In this test method, we are:

      (L16,17):  Preparing the hero details for this particular test scenario.

      (L20):  Triggering the level-up process.

      (L23,24):  Making sure that hero received his bonus money and random buff.

      (L26):  Here we are making sure that the game engine did not waste any time on generating and add a particular spell for the hero.

 

8)     Unit testing: parameterized example

A lot of the times, a certain path in a SUT method may be passed for a variety of input parameters. Junit 5 gives us first-class support in these terms. This time we will be trying to steal some treasure from a dragon:

 

SUT Code


public boolean stealTreasureFromDragon(Hero hero, Dragon dragon, List<Treasure> treasures)
		throws HeroIsAChickenExcpetion {
	if(CollectionUtils.isEmpty(treasures)){
		throw new HeroIsAChickenExcpetion();
	}

	return gameEngine.stealTreasures(hero, dragon, treasures);
}

Now we would like to test scenarios where Hero decides not to steal anything at all:

 

Test Code


@ParameterizedTest
@NullSource
@EmptySource
public void shouldThrowException_whenNoTreasureMeantToBeStolen(List<Treasure> treasures)
	throws Exception{
	// Given
	Hero hero = new Hero();
	Dragon dragon = new Dragon();

	// When
	assertThatThrownBy(() -> gameControllerSUT.stealTreasureFromDragon(hero,dragon,treasures))
			// Then
			.isInstanceOf(HeroIsAChickenExcpetion.class);
}

In this test method, we are:

        (L3):  Defining that we would like to pass the NULL value as one of the params for this test.

        (L4):  Defining that we would like to pass an empty value as one of the params for this test. The nature of the empty value is that it is based on the type of the parameter itself. For example, if String were the type of the input param, then an empty string would be passed instead of an empty Collection.

       (L12):  We invoke the SUT method and expect it to throw a HeroIsChickenException for each given input.

 

Tip
Tip

 

Reference
Reference
unittest
JUnit 5 parameterized tests

 

9)    Unit testing common mistakes

Sometimes it seems there is not much to know when it comes to such a straightforward subject as unit testing. Nothing more misleading. If we do not follow specific standards and principles, we might quickly get ourselves into trouble and do more bad than good for the quality of our application.

 

9.1) Do not overdo it

 

Common sense defines the percentage of the coverage that you should aim for. There are some rules of thumb that you can (and should) follow and the coverage percentage will uncover itself for your particular application:

       Critical pieces of code (components) ought to be tested first with all the logical paths covered.

       Auxiliary and less error-prone code ought to be tested afterwards also with all the logical paths covered (if the deadline is closing fast we have at least the crucial units tested).

       Go for testing the happy path first. Leave the exceptional cases for last. 

Just do not try to test everything that stands in your way.

 

 

9.2) Testing more than one path

 

In most of the situations, the method being tested has more than one logical path.

What you don't want to aim for is writing one test covering all those paths (or more than 1 path in general). The Single Responsibility Principle should be applied to all the code we write. That includes the test code also.

 

9.3) Testing private methods

 

Here are some key points about private methods that make them a terrible target for a unit test:

       Private methods are implementation detail and testing them breaks the isolation.

       Other classes (clients) only see and care about public methods.

       They are highly prone to change.

       Require the use of reflection, which makes the analysis way harder. It also makes them harder to maintain as you need to get the method with a raw String. If the method name changes.. we have a problem.

 

 

9.4) Skipped refactoring

 

Refactoring is like an icing on the cake. Imagine a situation where you start to climb the highest hill in the area, and after all the effort, when you reach the peak, you immediately begin to descend. Now, why would you want to do that.. stay a bit on the top, taking pride in what you accomplished enjoying the fantastic view.

So covering the code with tests is the most important thing but along with refactoring, we end up with a tandem which just immensely boosts the quality of the code. Do not miss out!

 

Reference
Reference
tdd
TDD refactoring phase

 

10)     Conclusion

 

Summary

 

 

Be a real artist in the realm of programming and always strive to cover your code with a suite of unit tests.

 

Need more insight?
Repository
Repository
Glossary
Glossary
Tags:
Reference
You may also like:
set-up-your-workspace-for-automated-testing-intellij
Set up Intellij workspace for automated testing
Comments
Be the first to comment.
Leave a comment