
Unit testing is a key skill for any modern software developer. There’s no better way to avoid regressions and improve code quality. That’s why you, as a Java developer, should master Java unit testing, and this post is here to help you.
We’ll open by answering the basic what-why-how of unit testing questions. After that’s out of the way, we’ll get practical and show you how to use JUnit to write your first unit test.

Prerequisites
Before starting, let’s review some prerequisites you must meet to follow the tutorial part of the post:
- You don’t need any previous knowledge of unit testing in Java or other languages.
- We assume you have at least some experience with Java.
- We also assume you have a proper Java environment ready: the JSK installed, an IDE or code editor, and so on.
- I suggest you use the IntelliJ IDEA IDE from JetBrains—the community edition is free, and it’s what I’ll use in this article.
A quick disclaimer
There’s no one-size-fits-all definition of a unit test, as strange as it may seem. For this introductory post, I’ve picked a definition to use as a starting point. As you gain more experience, you’ll be able to explore the nuanced and complex world surrounding this topic. Now, without further ado, let’s dig in!
Java unit testing: The fundamentals
Let’s answer the most fundamental questions about Java unit testing: What Is unit testing in Java?
Unit testing means testing an application with automatic tests written in a programming language. Unit tests are simple tests that exercise a small portion of the application code in isolation to verify whether they behave as expected. This small portion is what we call a unit, but what’s a unit? In my experience, most people define a unit as a method or function, so that definition will suffice for us.
Is unit testing important in Java?
If you think that unit testing requires a lot of time and effort, you’re right. So why do people do it, in Java or otherwise? What’s the appeal?
- Bug prevention: Unit tests catch bugs early, ensuring better code quality by addressing issues before they reach production.
- Self-documenting code: Well-written tests also serve as code documentation, making the code’s functionality clear, making onboarding of new team members easier, and facilitating the maintenance of the code.
- Easier refactoring: A comprehensive set of unit tests acts as a safety net for you and your team, instilling confidence so you can make structural improvements to the code in a safe way.
- Cleaner code: Unit testing encourages best practices, resulting in more modular, loosely coupled, and readable code that’s easier to maintain.
How do you write unit tests in Java?
Think of unit tests as small programs that test your program. Software engineers write unit tests using the same language they use for the production code, which we usually call SUT for “system under test.”
To write unit tests, you need a unit testing framework. They are usually named SomethingUnit, such as PHPUnit for PHP, NUnit for .NET, and JUnit for Java. We refer collectively to these tools as the xUnit family of unit testing frameworks. The unit testing framework enables you to write expectations, which we call “assertions,” about the behavior of the code, using a set of special classes and methods.
The framework typically comes with a unit test runner, a tool to execute tests and determine if they pass or fail automatically. This is a key point of unit tests: you shouldn’t need to manually verify whether the tests passed or not.
It’s a best practice to add unit tests, along with other automated tests, when applicable to your CI/CD pipeline. This ensures that each time a developer pushes changes to the mainline, the application undergoes testing. If a test fails, the pipeline stops, preventing the faulty code from reaching production or another environment, such as QA or staging. The developer who is responsible and the whole team are notified and can then fix the issue.
Java unit testing: A practical introduction
Time to roll up your sleeves and start testing.
Obtaining the SUT
You’ll start by getting the SUT—that is, the project you’ll be testing. To make your life easier, I’ve already prepared the project for you. Start by cloning it:
git clone https://github.com/carlosschults/jtask
After cloning, open the project using your favorite IDE or code editor. Then open the file at src/main/java/org/example/Task.java. You’ll see the following contents (imports and package statements are omitted for brevity):
public class Task {
private String description;
private boolean isComplete;
private LocalDate dueDate;
public Task(String description) {
this.description = description;
this.isComplete = false;
}
public Task(String description, LocalDate dueDate) {
this.description = description;
this.isComplete = false;
this.dueDate = dueDate;
}
public String getDescription() {
return description;
}
public boolean isComplete() {
return isComplete;
}
public void complete() {
isComplete = true;
}
public void postponeByDays(int days) {
if (dueDate == null) return;
if (days < 0)
throw new IllegalArgumentException(“The number of days must be positive!”);
dueDate = dueDate.plusDays(days);
}
public LocalDate getDueDate() {
return dueDate;
}
}
It’s a plain old Java class that represents a Task entity. Imagine we’re creating a command-line-based to-do app. It’s a super simple class, but it does have some business logic we can test. And that’s what we’re doing next.
Key unit testing frameworks and tools in Java
There are numerous unit testing frameworks and tools available for Java. Here are the most popular ones:
JUnit
JUnit is the most common unit-testing framework in Java. It is an open-source tool designed for writing and running repeatable tests. It is an instance of the xUnit architecture for unit testing frameworks.
JUnit provides annotations to identify test methods and contains assertions for testing expected results. It provides test runners for running tests and test suites, as well as test case classes for organizing and managing tests.
TestNG
TestNG is another testing framework inspired by JUnit and NUnit. It is designed to cover all categories of tests, including unit, functional, end-to-end, integration, etc. It provides more flexible configuration and execution options than JUnit.
TestNG introduces some new functionalities that make it more powerful and easier to use, such as support for data-driven testing, parallel test execution, and test configuration flexibility. TestNG also provides strong integration with build tools like Maven and Gradle.
Mockito
Mockito is a mocking framework for Java. It is used in conjunction with testing frameworks like JUnit or TestNG. Mockito allows you to create and configure mock objects, which simulate other parts of your environment to ensure unit tests run without external dependencies. Using Mockito, you can verify that methods were called with certain parameters, and you can check the interaction between the objects in your code.
Mockito simplifies the process of creating mock objects by removing the need for explicit configuration or specification of mock behavior. It uses a very simple API for this purpose, making the tests easy to write and read.
AssertJ
AssertJ is a library providing rich assertions for writing unit tests in Java. It provides a fluent assertion syntax, which allows for powerful and easy-to-read assertions.
AssertJ can be used with either JUnit or TestNG, and it provides assertions for a wide range of types. It supports everything from collections and maps to dates and numbers. One of the key features of AssertJ is its ability to chain assertions, which lets you create readable and maintainable tests. It is also extensible, allowing you to write custom assertions that suit your needs.
How do you create a basic Java unit test?
First of all, let’s install JUnit. We’ll use JUnit 5, which is the latest major release. Since the sample project uses Maven, add the following dependencies to your pom.xml file:
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.9.2</version>
<scope>test</scope>
</dependency>
As you can see, we’re using version 5.9.2, which is the latest release as of this writing. After the dependency is installed, you’ll be ready to start writing your tests.
Create a class under test/java and call it TaskTest.java (it’s a common practice to name test classes after the production class and append the word “Test”). The class should have the following contents:
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.Assertions;
public class TaskTest {
@Test
public void Test1() {
Assertions.assertEquals(4,2 + 2);
}
}
As you can see, the class has a single method, Test1(), marked with the @Test annotation. The annotation is what we use to inform JUnit that Test1() is a test method.
The method then uses the assertEquals method from the Assertions class to say that it expects that 2 + 2 results in 4. You’ll see a green “play” sign to the left of the method’s name on IntelliJ IDEA. Click on it and the test will be executed.
You’ll see something like the following:

The test unsurprisingly passes, as you can see. However, it doesn’t have anything to do with our class. Let’s change that. Replace the Test1() with the following code:
@Test
public void testCompletion_WhenTaskNewlyCreated_ThenTaskIsCompleted() {
Task sut = new Task("Write the report for sales department");
sut.complete();
Assertions.assertTrue(sut.isComplete());
}
Run the test and you’ll see that it passes. But what’s happening here? Notice that the test has three distinct phases:
- First, we arrange the scenario for the test, creating the object we’ll test against.
- Then we act—that is, we perform the behavior we wish to verify.
- Finally, we assert that something happened in the way we expected, in this case that the isComplete() method returns true.
This is what we call the AAA pattern, for arrange-act-assert. It’s a good practice to follow this pattern when writing your tests. In longer, more complex tests, you might want to add comments designating each section:
@Test
public void testCompletion_WhenTaskNewlyCreated_ThenTaskIsCompleted() {
Task sut = new Task(“Write the report for sales department”);
sut.complete();
Assertions.assertTrue(sut.isComplete());
}
There are two more things I want to draw to your attention. I named the variable representing the object against which we assert SUT, for “system under test.” Also, the test method’s name is longer than what is usually considered a good name in production code. But in test code, we consider it a good name since it follows a useful convention. It’s split into three parts: the first one indicates the unit we’re testing, the second refers to a specific scenario we’re testing, and the final one expresses the desired outcome.
Adding more tests
Another interesting method we can test is the postponeByDays() one. As you can see, it gets an integer as a parameter and then postpones the due date of the task by how many days it’s received.
Let’s add a test that asserts that the due date shouldn’t change if we pass zero as an argument:
@Test
public void testPostpone_WhenPassingZero_DueDateShouldNotChange() {
// arrange
Task sut = new Task("some description", LocalDate.of(2024, 2, 15));
// act
sut.postponeByDays(0);
// assert
LocalDate expected = LocalDate.of(2024, 2, 15);
Assertions.assertEquals(expected, sut.getDueDate());
}
Now let’s add yet another test, asserting that the due date changes by the expected amount when we pass a value greater than zero:
@Test
public void testPostpone_WhenPassingLargerThanZeroValue_DueDateShouldBeUpdatedByTheCorrectAmount() {
// arrange
Task sut = new Task("some description", LocalDate.of(2024, 2, 15));
// act
sut.postponeByDays(10);
// assert
LocalDate expected = LocalDate.of(2024, 2, 25);
Assertions.assertEquals(expected, sut.getDueDate());
}
Up until now, we’ve tested only the happy path, i.e., what happens when everything goes smoothly. Well, unfortunately, things do go wrong in programming, and your unit tests need to account for that as well. The following test verifies whether the code under test throws the appropriate exception when we pass a negative value to postponeByDays().
@Test
public void testPostpone_WhenPassingANegativeValue_ShouldThrow() {
// arrange
Task sut = new Task("some description", LocalDate.of(2024, 2, 15));
// act, assert
Assertions.assertThrows(IllegalArgumentException.class, () -> {
sut.postponeByDays(-1);
});
}
Here we use a different assertion method to express that we expect an exception in the tested scenario. If the exception is indeed thrown, the test passes; otherwise, it fails.
Now, as an exercise, identify and write more test cases for the class.
Best practices for unit testing in Java
1. Keep tests independent and isolated
One of the fundamental principles of unit testing is that tests should be independent and isolated. That means that each test should stand on its own, and the result of one test should not affect the result of another test.
Keeping tests independent often means setting up and tearing down the test environment for each test. This can be done using the @Before and @After annotations that we learned about earlier. This ensures that each test starts with a fresh environment, free from any changes that might have been made by previous tests.
2. Use a clear naming convention for tests
Good naming conventions can make your tests much easier to understand and maintain. A common convention is to name your test methods in a way that describes what they are testing.
For example, instead of naming a test method test1, you might name it testAdditionWithPositiveNumbers. This makes it immediately clear what the test is doing. It’s also helpful to follow a consistent naming pattern across your tests, to make it easier to find specific tests.
3. Avoid logic in tests
Logic in tests can make them harder to understand and more prone to errors. It’s generally best to avoid using conditions, loops, or other types of control flow in your tests.
Instead, aim to make your tests straightforward and linear. Each test should set up some data, perform an action, and then check the result. If you find yourself needing to use logic in a test, consider whether the test could be split into multiple smaller tests.
4. Test one thing per test method
Each test method should test only one thing. This makes your tests simpler and easier to understand. It also makes it easier to identify the cause of a test failure.
For example, instead of having a single test method that tests both addition and subtraction, you would have two separate test methods: one for addition and one for subtraction. This way, if the test for addition fails, you know that the problem is specifically with the addition functionality.
Wrapping up
Long gone are the days when quality was solely a QA department’s concern. In modern software development, it’s a given that everyone will test, and developers take part in the effort by writing automated tests, particularly of the unit test variety.
In this article, you’ve learned:
- What unit tests are, and why they’re important
- The workings of unit testing in Java
- How to install Junit
- How to name and structure your unit tests according to popular best practices
Where should you go now? There’s a lot to explore and learn, that’s for sure. For starters, I recommend you take a good look at JUnit’s official site. There are a ton of useful guides there, and you’ll certainly learn by doing it.
Other than that? Get your hands dirty and start exploring. Write many tests, read about best practices, and never stop learning. Here are a few topics you might want to look at:
- Parameterized tests
- Mocking and mocking libraries
- TDD
This post was written by Carlos Schults. Carlos is a consultant and software engineer with experience in desktop, web, and mobile development. Though his primary language is C#, he has experience with a number of languages and platforms. His main interests include automated testing, version control, and code quality.
