Skip to content
  • Technology
  • Data
  • Digital design and delivery

Unit testing best practices for more effective code

Unittestingeffectivecode

by TPXimpact team

In this article, we'll look at some unit testing best practices which lead to higher quality code and better workflows.

A suite of unit tests is essential for checking the proper operation of any software project. But how can you ensure that you are writing good tests that provide value to yourself and your team, both in the short and long term? 

What is a unit test?

A unit test is simply that; a test that covers the smallest piece of our code base, a ‘unit’ if you will, in isolation from other parts of the system. In Java, this would normally refer to testing individual methods.

Unit tests are distinct from integration tests, which aim to test how various pieces of the system fit together and integrate with parts of the architecture such as databases. They are also different to functional tests, which test slices of functionality with no consideration for how the system is working under the hood.

In the test pyramid (see image above – hat tip to both Martin Fowler and Mike Cohn), they sit at the bottom – you should have many more unit tests than any other type of testing.

Benefits of unit tests

The first, and simplest benefit of unit tests is that they enable you to directly verify that a piece of code you have written does, at its lowest level, what you want it to do, instead of needing to access the system through the front end.

Unit testing also gives you confidence that your code can cope with negative or unexpected inputs, and deal with them appropriately.

Beyond that, unit tests should provide a safety net for your whole development team. If a code base is comprehensively covered by tests, developers can refactor, modify and extend any code, safe in the knowledge that the test suite will show them if the underlying functionality has changed in a way that was not intended.

How to write unit tests

In Java, the most common and popular testing framework is JUnit. You can find out more about its powerful functionality at the JUnit website, but at its core, JUnit gives you runners for running tests and assertions for validating expected results. A simple unit test might look like this:

import static org.junit.Assert.assertTrue;
import org.junit.Test;

public class NumbersTest {
  @Test
  public void testNumberIsEven() {
    NumberUtil util = new NumberUtil ();
    assertTrue(util.isEvenNumber(2));
  }
}

What makes a good unit test?

A good unit test should be:

  • Fast. Having a unit test suite that takes ages to run removes most of the advantages. The longer it takes to run, the more developers will put off running them until just before commit time, or start skipping them altogether.

  • Isolated. It can be tempting to have a bunch of common set-up tasks used by multiple unit tests, but where possible, try to limit this. It ties unit tests together in ways that can be harder to isolate in the future.

  • Succinct. If your unit tests are hard to set up and complicated to write, it’s a good sign that the code you are trying to test is either too complex or badly structured. Either way, it’s a good sign that a refactor might be required.

  • Easy to Understand. Good naming of unit tests is vital. You should be able to read a test name and understand what the test is testing, what results the test is expecting, and why it’s failing. Do not be afraid to make unit test names long – the more information the better. After all, you should only see them when they are failing.

  • Reliable. Tests shouldn’t be bound to a given environment or rely on external factors to work. They should work 100% of the time, or they are not going to fulfil their purpose.

Unit testing best practices

One of the best ways to ensure high quality tests that cover a lot of the code base is to use Test Driven Development, where the tests are written first.

Here’s some advice on how to perfect your use of TDD.

Even when you write your tests before your production code, one of the best ways to improve both the quality and the efficiency of your unit tests is to consider the structure of your code.

Consider you have a requirement to identify the number of days in today’s month. In Java, you could approach this by writing a method that takes no inputs, and as its first line pulls the current Date using the standard Java Calendar Class.

public int getDaysInCurrentMonth(){
	Date today = Calendar.getInstance().getTime();
	
	//logic around the month here

	return daysInMonth;
}

That method looks good at first pass: it’s clear in what it wants to achieve and easy to understand. However, when it comes to testing it, the proposition gets a little harder. Because the current date is defined within the method, you will need to either overwrite the calendar date on the system or write convoluted tests that can only test some of the conditions, neither of which are good.

We can extract the date as a parameter in the method. This gives us the flexibility to construct date values to pass in to fully test the method. It also makes the method potentially reusable for a variety of future needs. We can also wrap this up in a helper method for the current month to prevent the need to pass in a bunch of variables. This structure allows us to be much more confident that the business logic is isolated in a fully-tested method. The helper methods can be changed or expanded as requirements change.

public int getDaysInCurrentMonth(){
	return getDaysInGivenDate(Calendar.getInstance().getTime());
}

public int getDaysInGivenMonth(Date givenDate){
	
//logic around the month here

	return daysInMonth;
}

In the above case, you could then easily write a series of tests for the getDaysInGivenMonth month. As an initial jumping off point, you could consider writing the following tests:

  • Tests for each month

  • Tests for an invalid date

  • Tests for a leap year

Quickfire unit testing tips

  • Review tests over time. Like code, unit tests rot as a code base evolves. Make sure to regularly review unit tests to ensure they are still fit for purpose.

  • Have a single assertion per test. There is some debate here at Manifesto about this, but I think it’s better to have many small tests with a single assertion each so that failures are easy to identify.

  • Use mocking tools where appropriate. Tools such as Mockito and jMock are powerful tools that can make hard-to-test code easier to write tests for. However, it shouldn’t be a crutch for poor design. If your code base requires a lot of mocking to test, it might be time for a refactor.

  • When fixing bugs, make sure the test suite is updated to cover the new failing conditions, so you can ensure it does not recur.

Useful tools for unit testing

There are a bunch of tools out there that can make your testing process easier, and more sustainable. Tied together, they can provide you with much more information about your code base and the work the whole team is putting in to maintain it.

The first thing to consider is a Continuous Integration system, that runs various tasks every time you check in a build, including your tests. Here at TPXimpact, we use Jenkins Pipelines for this, although there are a variety of other tools available. CI tools give you real time feedback if any tests are failing.

Jenkins Pipeline

Another important resource for testing is a Static Code Analysis tool. Manifesto’s preferred tool for this is currently SonarQube. In addition to giving you valuable insight into potential bugs and security risks in your code base, SonarQube can be used to measure code coverage. This is given as a percentage of your code base that is covered by unit tests, both at a line-by-line level, and at branching level, to ensure all logical paths through your code are covered. This can help identify edge cases that are not currently considered.

SonarQube Report

These tools can be easily linked together so that the code analysis and coverage report is run for every commit. The build can then pass or fail depending on conditions you set, such as if the coverage number drops below an agreed threshold. This means all developers have real-time feedback on their commits.

Put it to the test

Writing good unit tests, especially in complex applications with lots of frameworks, can be difficult, but once mastered, can have long-lasting benefits: in addition to improving your immediate development workflow, it helps identify problems early, prevents compound errors and leads to higher-quality code and less need to rework.

 

TPXimpact team's avatar

TPXimpact team

Experts in design, data, experience and technology

Contact TPXimpact

Our recent insights

Tennis Header

Tennis on the net: a technology game-changer

Serving up multi-cam streaming for the Lawn Tennis Association

What VAR can teach us about digital transformation

There are valuable lessons to be learnt about implementing technology from the recent football controversies

Getting started with your Drupal 7 to Drupal 9 migration

Our Lead Software Engineer recommends the best ways to take on the Drupal 7 to Drupal 9 upgrade.