Java Libraries to Supercharge Unit Testing
Unit test, you must. Save time, you can. Become smart, you should.
I'll admit it: I'm a lazy developer. That's not to say I don't work hard or care about my craft, but when it comes to unit testing—a notoriously time-consuming part of software development—I want to spend as little time on it as possible. So when the codebase you're working on is significant or your tests require lots of setups, automation becomes essential.
Here are some libraries that will make your life easier by removing all the grunt work in writing unit tests.
Java provides several frameworks for writing tests, but one library stands out above all others: JUnit. There are alternative libraries like Spock, but JUnit is the most used, so I will limit my discussion to libraries that work well with JUnit.
Mocking frameworks: Mockito & PowerMock
It can be difficult to test when writing code with many dependencies. That's where mocking comes in. A mocking framework allows you to replace real objects with fake ones during testing to isolate the code under test from its dependencies and ensure it works correctly without worrying about external factors like network connections or databases.
Mockito is the de-facto Java library used for creating and testing mocks in software development. It provides a simple and intuitive API for creating mock objects, which can be used in unit tests to simulate the behaviour of objects that are not yet available or are difficult to obtain.
With Mockito, you can mock classes, interfaces, and even individual methods. You can also specify how the mock objects should behave, such as returning a specific value for a given method call or verifying that a method was called a certain number of times.
import org.junit.Test;
import static org.junit.Assert.*;
import static org.mockito.Mockito.*;
class CalculatorTest {
@Test
void testAdd() {
Calculator calculator = mock(Calculator.class);
when(calculator.add(1, 2)).thenReturn(3);
assertEquals(3, calculator.add(1, 2));
}
@Test
void testMultiply() {
Calculator calculator = mock(Calculator.class);
when(calculator.multiply(2, 3)).thenReturn(6);
assertEquals(6, calculator.multiply(2, 3));
}
}
Mockito works in most cases, but it expects you to write code in complete idiomatic gang-of-four style code. Often you will find yourself needing to test a piece of code which bends around those rules, such as needing to mock a static method. Mockito stays away from byte-code black magic required for these cases. Time to summon the dark wizard - PowerMock!
PowerMock has helpful additional features, such as mocking final methods and static methods that Mockito does not support. It may be beneficial when writing tests involving these types of declarations (e.g., mocking static initializers). In this example, we will mock some static methods of java.lang.Math
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import java.util.ArrayList;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.junit.jupiter.MockitoExtension;
@ExtendWith(MockitoExtension.class)
class MathTest {
@Test
void testMax() {
Math mockMath = mock(Math.class);
when(mockMath.max(1, 2)).thenReturn(2);
assertEquals(2, mockMath.max(1, 2));
}
}
Assertion libraries: AssertJ & Hamcrest
While JUnit's built-in assertions will suffice most of the trivial assertions, there are times when you need something more powerful. AssertJ is an assertion library that provides additional features, like fluent assertions and method chaining, for better readability.
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.within;
import org.junit.jupiter.api.Test;
class AssertJAssertionTest {
@Test
void testAssertThat() {
int actual = 1;
assertThat(actual).isEqualTo(1);
}
@Test
void testWithin() {
double actual = 0.99;
double expected = 1.0;
double delta = 0.01;
assertThat(actual).isCloseTo(expected, within(delta));
}
}
Hamcrest is a library for writing “matchers” of various kinds, including matching on the presence or absence of a particular element, matching against the text content of elements, and matching against the structure of elements. It provides a fluent interface for writing these matches in a way that is more readable than using JUnit's built-in Matchers class.
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.number.IsCloseTo.closeTo;
import org.junit.jupiter.api.Test;
class HamcrestAssertionTest {
@Test
void testIs() {
int actual = 1;
assertThat(actual, is(1));
}
@Test
void testEqualTo() {
String actual = "hello";
assertThat(actual, equalTo("hello"));
}
@Test
void testCloseTo() {
double actual = 0.99;
double expected = 1.0;
double delta = 0.01;
assertThat(actual, closeTo(expected, delta));
}
}
Data faking: Instancio & DataFaker
While writing, your tests need a lot of fake data. You can hand-roll the objects, but it becomes tiresome rather quickly. Instancio is an open-source library that allows you to create random instances of your classes without writing a single line of code.
public record User(String firstName, String lastName, String email) {}
// use Instantio to create a list of 10 users
List<User> user = Instancio.ofList(User.class).size(10).create();
If you need real-looking data for populating a database or testing validations, use DataFaker. It generates real-looking data for unit tests, such as names and addresses or bank account numbers. It can also generate random data for integration tests.
import net.datafaker.Faker;
// ...
Faker faker = new Faker();
String name = faker.name().fullName(); // Miss Samanta Schmidt
String firstName = faker.name().firstName(); // Emory
String lastName = faker.name().lastName(); // Barton
String streetAddress = faker.address().streetAddress(); // 60018 Sawayn Brooks Suite 449
Web service mocking: WireMock
WireMock is a Java library that allows you to quickly mock, stub and validate web services. It helps you test the interaction between your application and the remote services it depends on when deployed. Using WireMock, you can define HTTP routes and test HTTP calls with their expected request bodies and mock responses.
import static com.github.tomakehurst.wiremock.client.WireMock.*;
import static org.junit.jupiter.api.Assertions.assertEquals;
import com.github.tomakehurst.wiremock.WireMockServer;
import com.github.tomakehurst.wiremock.core.WireMockConfiguration;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
public class ClientServiceTest {
private static WireMockServer wireMockServer;
@BeforeAll
public static void setup() {
wireMockServer = new WireMockServer(WireMockConfiguration.options().port(8080));
wireMockServer.start();
}
@AfterAll
public static void teardown() {
wireMockServer.stop();
}
@Test
public void testClientService() {
wireMockServer.stubFor(get(urlEqualTo("/service/endpoint"))
.willReturn(aResponse().withBody("{\"data\":\"mocked data\"}")));
ClientService client = new ClientService();
String response = client.callService();
assertEquals("mocked data", response);
}
}
WireMock can simulate slow or unavailable services to test how your application handles them, and you can verify if your code responds correctly in such situations.
Mocking application dependency with TestContainers
TestContainers is a lightweight, open-source library that lets developers quickly mock and test interactions between their code and popular software systems. It's great for testing with real, local dependencies like databases or caches. It also simulates third-party services like Amazon S3 or Google Cloud Storage (using their corresponding local emulators). Here is an example with a PostgreSQL container spun up for use during the test.
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
@Testcontainers
public class TestContainersExample {
@Container
private static final PostgreSQLContainer database = new PostgreSQLContainer();
@BeforeEach
public void setup() {
String jdbcUrl = database.getJdbcUrl();
// Use jdbcUrl to setup connection to the database container
}
@AfterEach
public void teardown() {
// Close the database connection
}
@Test
public void testDatabaseOperations() {
// Perform database operations and assertions
}
}
TestContainers is built on the Docker API and works with any Dockerfile or image. The library provides ready-made definitions for popular software such as PostgreSQL, MySQL, and Redis and allows you to define your mocks using a simple, declarative syntax. Then it uses these definitions in conjunction with the official docker run command to create new containers that behave like the real thing but provides sandboxed environments for testing purposes.
Unit testing can be a powerful tool for ensuring that your code works correctly, but it also takes time to learn how best to use it. I've covered some great libraries that make unit testing more effortless than ever, and they're free! You don't have any excuse not to try them out today!