Java Unit Test Practices with JUnit 5
Unit testing is a procedure seamlessly integrated into the software development workflow to verify that each unit of code functions as intended. If conducted accurately, unit tests have the potential to reveal early flaws in code that may be more intricate to identify in later testing stages.
In the course of this narrative, I’ll outline common JUnit 5 unit testing practices, steering clear of the details of JUnit 5 installation and assuming you’ve arranged your environment with the necessary dependencies.
Given When Then Naming Pattern
A suitably named unit test can articulate its purpose distinctly, facilitating a better understanding of the tested functionality.
One common naming convention for unit tests is the ‘Given When Then’ pattern, which is closely associated with ‘Behavior-Driven Development.’ This method involves breaking tests into three distinct sections: ‘Preconditions’ specified in the ‘Given’ section, the ‘Actual method and code part to be tested’ described in the ‘When’ section, and the ‘Expected behavior’ articulated in the ‘Then’ section.
Let’s consider a basic method as shown below. It returns the Customer model when retrieved from the database or throws an exception when no Customer record is found with the provided id parameter.
public Customer getCustomerById(final int id) {
return customerRepository.findById(id).orElseThrow(
() -> new NotFoundException("Customer not found"));
}
We can create two basic unit test methods: one for the success scenario and the other for the failure scenario when no customer record is found in the database. We can title these unit test methods as follows, providing different alternatives:
// Alternative 1:
@Test
void givenCustmerId_whenGetCustomerById_ThenItShouldReturnTheCustomerModel
@Test
void givenNonExistCustomerId_whenGetCustomerById_ThenItShouldThrowAnException
// Alternative 2:
@Test
void givenValidParameter_whenGetCustomerByIdIsCalled_ThenItReturnsTheCustomer
@Test
void givenInValidCustomerId_whenGetCustomerById_ThenItThrowsNotFoundException
The AAA (Arrange-Act-Assert) Pattern
The AAA pattern, which stands for “Arrange, Act, Assert,” is a methodology for organizing the structure of a unit test method. It segments the unit test method into three components: “Arrange,” “Act,” and “Assert.” In the “Arrange” section, you include the necessary code to set up the specific test, including the creation of objects, setup of mocks (if applicable), and potentially defining expectations. The “Act” phase involves invoking the method being tested. Finally, the “Assert” section comprises assert and verify statements that check whether the expectations were met.
You can find a sample of a unit test organized with the AAA pattern below; it is easy to understand at first glance. Please pay attention to the comments preceding each section.
@Test
void givenCustmerId_whenGetCustomerById_ThenItShouldReturnTheCustomerModel() {
// Arrange
var id = 2L;
var expectedCustomer = mock(Customer.class);
when(customerRepository.findById(id)).thenReturn(expectedCustomer);
// Act
var response = customerService.getCustomerById(id);
// Assert
assertNotNull(response);
assertThat(response).isEqualTo(expectedCustomer);
verify(customerRepository, times(1)).findById(id);
}
Using Mock Objects
Mocking is a technique that enables us to create fake objects for dependencies. If the code under test is intricate, involves external dependencies, and includes objects with unpredictable effects, it is advisable to mock such objects and concentrate solely on the code being tested. For example, if the code involves an object that connects to an actual database, we should mock this object to prevent it from connecting to the actual database and performing operations on the database during each unit test.
In the Java world, Mockito, EasyMock, and PowerMock are the most popular mock testing frameworks. My favorite is using Mockito, which provides a rich set of features for mocking and verifying objects. In the code below, I’ve included some samples demonstrating how to create mock objects using Mockito.
You can use the Mock
annotation in a testing class using the Junit MockitoExtension
to run the test:
@ExtendWith(MockitoExtension.class)
class UserServiceTest {
@Mock
private UserService userService;
@Test
void testMethod() {
// Arrange
Mockito.when(userService.count()).thenReturn(153);
}
}
Alternatively, you can use the Mockito.mock()
method:
@ExtendWith(MockitoExtension.class)
class UserServiceTest {
@Test
void testMethod() {
// Arrange
var userService = Mockito.mock(UserService.class);
Mockito.when(userService.count()).thenReturn(153);
}
}
Assertions
I believe the most crucial section of a unit test is the ‘Assert’ section. This is where you include verification and expectations. Each unit test should encompass all necessary assertions in this section.
You can use frameworks such as AssertJ, Hamcrest, Truth. Alternatively, you have the option to utilize the integrated Junit 5 assertion library when formulating your assertion statements. My favorite is using the AssertJ. Because it provides a rich set of assertions and it improves test code readability. To compose an assertion statement with AssertJ, you always need to pass your object to the Assertions.assertThat
method first. Subsequently, you can write your actual assertions. In the code snippet that follows, I showcase some valuable AssertJ statements. But please note that AssertJ has many other rich collections of assertions, which I cannot show all of them here.
@Test
void testMethod() {
// Arrange
...
// Act
...
// Assert
// Sample of String assertions
assertThat(customerUserName).isEqualTo("John");
assertThat(customerUserName).containsIgnoringCase("Jo");
assertThat(customerUserName).endsWith("hn");
assertThat(customerUserName).containsWhitespaces();
assertThat(customerUserName).hasSize(4);
assertThat(customerUserName).isEmpty();
assertThat(customerUserName).isUpperCase();
// Sample of Integer assertions
assertThat(age).isEqualTo(25);
assertThat(number).isZero();
assertThat(height).isGreaterThan(170);
assertThat(degree).isNegative();
assertThat(age).isLessThanOrEqualTo(20);
// Sample of Object assertions.
assertThat(customer).isEqualTo(mockCustomer);
assertThat(customer).hasFieldOrProperty("name");
assertThat(customer).isInstanceOf(Customer.class);
assertThat(customer).isNotNull();
// Sample of List assertions.
assertThat(addressList)
.hasSize(5)
.contains("Street 6")
.doesNotContain("Building 3")
.isNotEmpty()
.hasSizeGreaterThan(3);
}
Verifications
The Mockito.verify() method is employed in the Assert section of the unit test. It verifies whether a specific behavior occurred, tests the number of method invocations, and checks other method interactions. We can verify method invocations on a Mockito mock or spy object. You can find some examples of the verify method in the code snippet below. Please note that the actual implementation details of the CustomerService
class are not displayed in the example to focus solely on the verification methods.
@Test
void testMethod() {
// Arrange
var customerService = Mockito.mock(CustomerService.class);
var userService = Mockito.mock(UserService.class);
// Act
customerService.someMethod();
// Assert
// Sample of verifications
// Check the number of interactions of the getName method
verify(customerService, times(1)).getName();
// Verify an interaction occurs at least/most a certain number of times.
verify(customerService, atLeast(1)).getName();
verify(customerService, atMost(3)).getById();
// Verify no interaction with a particular method
verify(customerService, never()).totalCustomerCount();
// Verify no interaction with the entire userService mock
verifyNoInteractions(userService);
}
JUnit 5 Parameterized Tests
The parameterized tests are among the most effective and powerful features of Junit 5, enabling us to execute a test method multiple times with different parameters. This way, various scenarios can be verified quickly without having to write additional unit testing methods for each scenario.
We use the @ParameterizedTest
annotation instead of the @Test
annotation and declare the arguments in the unit test method as the source of the test.
In the sample below, we can test the isShortName
method with three different name parameters:
@ParameterizedTest
@ValueSource(strings = {"John", "Leon", "Nova"})
void isShortName_ReturnsTrueIfTheCustomerNameIsShort(String name) {
// Assert
assertTrue(customerService.isShortName(name));
}
The @ValueSource annotation is not sufficient for handling both output and multiple input parameters. Another source annotation is @MethodSource, which enables us to reference a method providing the arguments. These methods must return an Iterable, Iterator, Stream or an array of arguments.
In the sample below, we prepare the two parameters and the expected response value for the test the getCustomerByStreetAndZipCode
method within the method source. We then use this method source in the unit test method:
@ParameterizedTest
@MethodSource("prepareParameters")
void givenStreetAndZipCode_ItShouldReturnTheCorrectCustomer(
String street, String zipCode, String expectedName) {
// Act
Customer customer =
customerService.getCustomerByStreetAndZipCode(street, zipCode);
// Assert
assertThat(customer.getName()).isEqualTo(expectedName);
}
private static Stream<Arguments> prepareParameters() {
return Stream.of(
Arguments.of("Street1", "38920", "Customer1"),
Arguments.of("Street2", "67290", "Customer2")
);
}
Mockito ArgumentCaptor Usage
By utilizing Mockito’s ArgumentCaptor feature, we can capture data that is passed to a method and created inside the method that we are testing.
Please take note of the sendPackage
method in the CustomerService
class below. The packageData
variable is created inside the method and then passed to the acceptPackage
method of the ShipmentService
class.
public class CustomerService {
...
public void sendPackage(String packageName, int packageSize) {
Package packageData = new Package(packageName, packageSize);
packageData.setType(Size.SMALL)
shipmentService.acceptPackage(packageData);
}
}
We don’t have the chance to create the packageData
in the unit test method and inject it during testing. Here, we can use the ArgumentCaptor to obtain the value of the packageData
argument and verify its value as shown below.
@ExtendWith(MockitoExtension.class)
class CustomerServiceTest {
@Mock
private ShipmentService shipmentService;
@InjectMock
private CustomerService customerService;
@Test
void givenPackageInfo_ThePackageShouldBeSent() {
// Arrange
var captor = ArgumentCaptor.forClass(Package.class);
// Act
customerService.sendPackage("packageName", 10);
// Assert
verify(shipmentService).acceptPackage(captor.capture());
Package packageCaptured = capture.getValue();
assertThat(packageCaptured.getType()).isEqualTo(Size.SMALL);
}
}
Summary
In this short story, I explored general Java unit test practices using JUnit 5 and Mockito. I didn’t delve into details; instead, I aimed to provide a high-level overview for each item.