In software engineering, unit testing is a technique that involves testing individual components of a software system in isolation. In this context, individual components are similar to methods or classes in Java. Unit tests help you verify that your implementations of individual methods and classes are correct.

Unit testing plays a crucial role in detecting bugs at an early stage, whether you’re dealing with newly written code or making changes to existing code.

From a code hygiene perspective, enforcing good unit testing habits as a team can push each individual developer to write more modular and, thus, testable code. In addition, unit testing provides developers with another mechanism to verify that any code they deploy will continue to work as expected in production.

In this article, we’ll look at some best practices for unit testing in Java. This includes critical concepts, such as mocking, dependency injection, and code coverage. You’ll also see some unit test examples using JUnit assertions and Mockito. By the end of this article, you’ll be ready to use these testing tools and frameworks to write your own comprehensive unit tests in Java.

Writing Testable Code

To properly unit test your application, you need to adopt a mindset of writing testable source code. Good, testable Java code often includes the following three characteristics: it separates concerns, implements getters/setters, and uses dependency injection. As you learn about these concepts in the following sections, you’ll also review some basic Java implementations of a calculator app to help you fully understand these concepts.

Separation of Concerns

When building out any software system, you want its individual components to be modular. Each component should handle a single task and have as little dependence on other components as possible. To illustrate this separation of concerns, let’s write some code.

Suppose you were to implement a Calculator class like this:

public class Calculator {

	public int calculate(int a, int b, String operation) {
		if (operation.equals("add")) {
			return a + b;
		} else if (operation.equals("subtract")) {
			return a - b;
		}
	}
}

Please note: You’ll use int for simplicity throughout this article. For more precise calculations, you should use a different class, such as BigDecimal.

The previous code is a bad implementation of Calculator because it doesn’t properly separate concerns. In particular, the calculate method does too much. It should be broken down further to make the code more modular and testable:

public class Calculator {

	public int add(int a, int b) {
		return a + b;
	}

	public int subtract(int a, int b) {
		return a - b;
	}
}

This is a better implementation of Calculator, and it allows you to unit test the add and subtract methods individually.

Getters/Setters

Getters and setters are methods that primarily provide a way for you to access and modify the private properties of a class. However, they also come with the added benefit of making it easier to write more robust unit tests since you can use them to verify the values of each attribute of an object.

In regard to the calculator scenario, suppose we modify the implementation further to keep the result of the previous operation stored as a state in the Calculator object:

public class Calculator {

	private int result = 0;

	public int add(int b) {
		this.result = result + b;
		return result;
	}

	public int subtract(int b) {
		this.result = result - b;
		return result;
	}
	
	public int getResult() {
		return result;
	}

	public void setResult(int result) {
		this.result = result;
	}
}

In this example, the getResult and setResult methods are useful for retrieving and modifying the private variable result. It also allows our unit tests to access the value of result and ensure that its value is what you’re expecting.

Dependency Injection

Dependency injection (DI) is a simple but powerful concept that is essential for writing testable code in any object-oriented programming language. The idea behind DI is to avoid direct object creation within a component and instead pass in any required dependencies from outside. As a result, your components become less coupled to others, and it’s easier to test the component using mock objects, which you’ll see later in this article.

In regard to the calculator example, suppose you want to have a CalculatorUI class that uses a Display object to display the results of calculations performed by a Calculator object. A naive implementation might look like this:

public class CalculatorUI {
    private Calculator calculator = new Calculator();
    private Display display = new Display();

    public void performAddition(int b) {
        int result = calculator.add(b);
        display.displayResult(result);
    }

    public void performSubtraction(int b) {
        int result = calculator.subtract(b);
        display.displayResult(result);
    }
}

While this example works fine in terms of functionality, it doesn’t follow a DI pattern. CalculatorUI creates instances of Calculator and Display within its implementation, which makes them hard dependencies. Rather than tightly coupling these classes, a far better approach would be to use DI:

public class CalculatorUI {
    private Calculator calculator;
    private Display display;

    public CalculatorUI(Calculator calculator, Display display) {
        this.calculator = calculator;
        this.display = display;
    }

    // ... same implementations for performAddition and performSubtraction
}

This is a better implementation because the CalculatorUI class doesn’t need to know the implementation details of the Calculator and Display classes, which removes the tight coupling. Instead, implementations of Calculator and Display are passed in during instantiation, which this CalculatorUI can use to display results. In addition, this makes the CalculatorUI class easier to unit test since you can pass in a mock object or alternate implementation of Calculator or Display (ie if they were interfaces).

Choosing the Right Assertions

Now that your code is modular and testable, let’s discuss test assertions. In this section, it’s assumed you’re using the JUnit 5 framework—this is currently one of the most commonly used frameworks, and many IDEs offer built-in integrations with it.

Following are some of the most common JUnit 5 assertions you might use in your unit tests:

assertEquals

You can use assertEquals to verify that two values are equal. Equality is evaluated using the class’s .equals() method. Here’s an example:

@Test
void add_twoIntAddition_isEqual() {
	// ... mock calculator instantiation (will discuss mock objects shortly!)
	assertEquals(2, calculator.add(2));
	assertEquals(4, calculator.add(2));
}

For this particular test scenario, you can also verify that the result of the Calculator is as expected using the getter method:

@Test
void getResult_twoIntAddition_storedResultIsEqual() {
	// ... mock calculator instantiation (will discuss mock objects shortly!)
	calculator.add(3);
	assertEquals(3, calculator.getResult());
}

assertNull and assertNotNull

You can use assertNull and assertNotNull to verify that objects are null/non-null:

@Test
void getResult_twoIntSubtraction_nullBeforeThenNotNullAfter() {
	// ... mock calculator instantiation (will discuss mock objects shortly!)
	assertNotNull(calculator.getResult());
	calculator.subtract(1);
	assertNotNull(calculator.getResult());
}

assertTrue and assertFalse

You can use assertTrue and assertFalse to check Boolean conditions. For instance, we could test that a result is an even number:

boolean isEven(int result) {
	if (result % 2) == 0) {
		return true;
}
return false;
}

@Test
void getResult_isEven_isTrue() {
	// … mock calculator instantiation (will discuss mock objects shortly!)
	calculator.add(2);
	assertTrue(isEven(calculator.getResult()));
}

assertSame and assertNotSame

You can use assertSame and assertNotSame to check that the expected and actual values refer to the same object (ie not just that the respective properties are the same). Sameness is evaluated using the == comparator. Let’s use lists of Strings as an example:

@Test
void testSameObject_assertSame() {
	List<String> strList1 = Arrays.asList("test");
	List<String> strList2 = strList1;
	assertSame(strList1, strList2); // Yes, strList1 and strList2 are the same object.
}

@Test
void testSameObject_assertNotSame() {
	List<String> strList1 = Arrays.asList("test");
	List<String> strList2 = Arrays.asList("test");
	assertNotSame(strList1, strList2); // Yes, strList1 and strList2 are different objects, although they are equal
}

assertThrows

Finally, you can use assertThrows to test that an exception of the correct type is thrown. For simplicity, and in keeping with the calculator theme, let’s use divide by zero as an example:

private int divide(int a, int b) { return a / b; }

@Test
public void testDivideByZero() {
    int a = 10;
    int b = 0;
    assertThrows(ArithmeticException.class, () -> divide(a, b));
}

When to Use Each JUnit Assertion

To summarize, the following are the assertions covered previously and when it makes sense to use each one:

  • Use assertEquals to verify that a method returns the expected result. Equality is evaluated using the class’s .equals() method. You can use assertEquals alongside getter methods to verify that all properties of an object are as expected.
  • Use assertNull and assertNotNull to verify that an object is empty or non-empty. This can be useful to check that certain objects are or aren’t created due to control flow branches in your code.
  • Use assertTrue and assertFalse to verify the result of Boolean operations.
  • Use assertSame and assertNotSame to verify that the expected and actual objects you’re comparing are one and the same. Sameness is evaluated using the == comparator.

Using Code Coverage

Code coverage is a metric that indicates how much of an application’s source code is exercised by test code. Code coverage is usually expressed as a percentage using the following two measures:

  1. Line coverage (ie statement coverage or instruction coverage): The number of lines that have been executed by tests out of the total number of lines. For example, if your application has 1,000 lines of code and your unit tests run through 800 lines of it, your line coverage is 80 percent.
  2. Branch coverage: The number of branches that have been tested out of the total number of branches. For example, if a method that you write has five if statements and your unit tests cover three of them, your branch coverage for that method is 60 percent.

JaCoCo

A popular code coverage tool for Java applications is JaCoCo. JaCoCo helps generate code coverage reports that look like the following:

JaCoCo analyzes unit test coverage and produces instruction coverage and branch coverage metrics for each package of your application. The report is interactive, so you can click on each element until you reach specific classes in your source code. JaCoCo highlights lines with colors that indicate full coverage (green), partial coverage (yellow), and no coverage (red):

You can then modify or write additional unit tests to cover the missing lines.

Understanding Code Coverage Metrics and Limitations

You might think that you should always strive for the highest code coverage possible. In general, it’s good to have high code coverage to help verify that each line and branch of your code are working as expected. However, even if you achieve 100% code coverage (and you probably can’t), this is not a guarantee that your code is completely bug-free or that it will function correctly in all scenarios.

This is why it’s better to view code coverage simply as another tool to help you build high-quality software. It rarely makes sense to mandate 100% code coverage because coming up with unit tests to exhaustively test each scenario - however unimportant - isn’t the most productive use of your developers’ time.

An AI-powered tool like Diffblue Cover automates unit test writing to increase up your coverage and free developers to work on more functional code changes. But there’s still considerable debate on what that ideal coverage number should be. Sources such as Atlassian recommend 80 percent coverage, whereas the standard at companies like Google seems to be “60% as acceptable, 75% as commendable, and 90% as exemplary.” In reality there’s no ‘correct’ target. It depends on many factors like the coverage you have today, the criticality of your application, the specific code that covered, how the tests exercise that code - and so on. You should work with your team to agree an appropriate goal for your application. This Diffblue webinar looks in more detail at why arbitrary coverage targets can be a bad idea.

With Diffblue Cover you can understand unit test coverage more clearly than ever before thanks to its Reports feature. Cover Reports is available in Teams and Enterprise editions: it breaks down exactly what is and isn’t tested, and why, helping you to get beyond a single number and identify crucial code that is lacking coverage.

Using Mocking Frameworks Effectively

In the section discussing assertions, you can see this comment inserted into some of the code:

// ... mock calculator instantiation (will discuss mock objects shortly!)

But why? And what are these mock objects? To answer the why, consider this: suppose that you just created an instance of Calculator in the test as follows:

@Test
void someMethod_someScenario_someResult() {
	Calculator calculator = new Calculator();
	// test code...
}

For a simple example like this, you may be fine since Calculator contains very basic implementations and no additional hard dependencies. But most software applications that you want to test are much more complex than this primitive Calculator. If the class in question had multiple dependencies requiring specific environments to instantiate correctly, this would be an absolute pain to unit test.

Instead, you can use mock objects. These are simulated objects that mimic the behavior of real objects in a software system but without having to fully instantiate these objects directly. Mocking allows you to set up an object and define its behavior in response to different inputs, thereby making it easier to unit test your objects in isolation.

However, keep in mind that there are certain limitations to mock objects. For example, since mock objects are not real objects, it’s possible that they can’t exactly replicate the behavior of your object once deployed into production. This means you might see false positive tests or incorrect behavior in certain edge cases. In addition, since you have to define the behavior of mock objects, this can lead to verbose tests (in an already verbose language in Java).

Despite these limitations, the pros of using mock objects still tend to outweigh the cons.

Mocking with Mockito

In Java, mocking frameworks, such as EasyMock, JMockit, or Mockito, expose APIs that help you create mock objects and use them in unit tests.

Let’s break down an example unit test using Mockito:

import static org.mockito.Mockito.*;
import org.junit.jupiter.api.Test;

public class CalculatorTest {

	@Test
	void testPerformAddition() {
		// Create a mock Calculator object and define behavior
		Calculator calculator = mock(Calculator.class);
		when(calculator.add(4)).thenReturn(4);

		// Create a mock Display object
		Display display = mock(Display.class);

		// Create a CalculatorUI object with mocks
		CalculatorUI calculatorUI = new CalculatorUI(calculator, display);

		// Call performAddition method
		calculatorUI.performAddition(4);

		// Verify that Display.displayResult method was called with the expected result
		verify(display).displayResult(4);
	}
}

Here, notice that you don’t directly create an instance of Calculator for testing. Instead, you set up a mock calculator before running through each test. Then the actual test itself illustrates the process of writing unit tests with mocks:

  1. Define the behavior of the mocks for this test. In Mockito, this involves a when()...thenReturn() statement.
  2. Call the target method performAddition() on the mock object.
  3. Verify that the method was called with the expected arguments. Mockito will fail this test if the method is not called or called more often than the number of verify statements in the unit test.
  4. Verify the result is what you expected, if applicable.

Writing Java Unit Tests Automatically

After this overview of Java unit testing best practices you hopefully have a clearer idea of what a good unit test looks like and how to write one. But even though unit tests are really important, time spent writing them is time you could have spent working on real functional code. A better option would be to hand over the task to Diffblue Cover.

What Is Diffblue Cover

Diffblue Cover is an AI-powered tool that automatically writes Java unit tests so that developers don’t have to do all the work. A typical unit test might take developers a few minutes to write; however, Diffblue Cover typically writes unit tests in around 2.5 seconds while incorporating all the best practices you’ve learned about in this article. This service is a game changer for individuals and corporations looking to quickly boost code coverage for poorly tested codebases in a fraction of the usual time, and free up developers for more productive work.

Integrating Diffblue Cover into a Unit Testing Workflow

Diffblue is available as an IntelliJ plugin or as a CLI tool. In this section, we’ll review a sample unit testing workflow using the Diffblue Cover CLI. General steps to get started with Cover CLI can be found in the Diffblue documentation.

Once you’ve obtained a free-trial license key, you can start using Diffblue Cover to generate some unit tests for your Calculator app with the following CLI command:

dcover create com.example.CalculatorUI.performAddition --mock com.example.Calculator

In this command, you specify the method that you want to generate tests for (ie CalculatorUI.performAddition()). You can also specify that you want to mock the Calculator object that gets injected into a CalculatorUI instance (this means adding Mockito to the project’s dependencies). Diffblue automatically comes up with the following file:

package com.example;

import static org.mockito.Mockito.anyInt;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import org.junit.jupiter.api.Test;

class CalculatorUIDiffblueTest {
	/**
	* Method under test: {@link  CalculatorUI#performAddition(int)}
	*/

	@Test
	void testPerformAddition() {
		// Arrange
		Calculator calculator = mock(Calculator.class);
		when(calculator.add(anyInt())).thenReturn(2);

		// Act
		(new CalculatorUI(calculator)).performAddition(2);

		// Assert
		verify(calculator).add(anyInt());
	}
}

This test checks all the boxes: a mock object with defined behavior, a test call to the target method, and an assertion that the underlying Calculator.add() method was called. Then you can simply build this package (ie mvn install), and this test will pass.

Conclusion

Unit testing is a critical part of the software development cycle. Good unit tests use the right mock objects and assertions, and comprehensively test individual components of your system. Focusing on unit tests also helps reinforce source code best practices, such as the concept of separation of concerns and the use of dependency injection.

Becoming well-versed in unit tests is a must for any Java developer. However, it’s still possible to find yourself spending valuable development time coming up with tests that could be automated with a tool like Diffblue Cover. As you saw, Diffblue Cover can generate clean test code that follows all the best practices that you learned about here. You can try Diffblue Cover now to see how it could help you spend more time on real code and less on unit tests.