Unit testing is when you test individual components of a software system in isolation. In this context, a component could refer to a module, method, or class.
The goal of unit testing is to ensure that each component works as expected on its own. Good unit tests that have been incorporated into automated build processes can help you detect bugs early, encourage good code design practices, and improve overall code quality.
To excel at writing unit tests in Java, you have to master JUnit assertions. In general, assertions are statements in unit tests that check whether the actual result of a code operation matches the expected result.
In the world of JUnit, you have a plethora of JUnit assertions to choose from. From assertEquals
to assertSame
and assertTrue
to assertThrows
, it’s hard to choose the assertion to use and when to use it. Moreover, you need to know how to use the assertions in combination with frameworks like Hamcrest to make your unit tests more comprehensive and readable. In this article, you’ll learn about all these topics and review some code samples to show you different JUnit assertions in action.
Basic JUnit Assertions
Let’s set the stage by defining some classes that you’ll use as the basis for examples throughout this article.
In this scenario, let’s suppose you want to create an application that models a store with a number of products in inventory. You’ll define the interfaces Store
and Product
like this:
import java.util.*;
public interface Store {
Map<Integer, Product> getProducts();
void addProduct(Product product);
void removeProduct(int productId);
void sellProduct(int productId, int quantity);
}
public interface Product {
int getId();
String getName();
double getPrice();
int getQuantity();
void setQuantity(int quantity);
}
From there, you can define a specific store called FooStore
:
class FooStore implements Store {
private final Map<Integer, Product> products;
public FooStore() {
products = new HashMap<>();
}
@Override
public Map<Integer, Product> getProducts() {
return products;
}
@Override
public void addProduct(Product product) {
products.put(product.getId(), product);
}
@Override
public void removeProduct(int productId) {
products.remove(productId);
}
@Override
public void sellProduct(int productId, int quantity) {
Product product = products.get(productId);
if (product != null) {
if (product.getQuantity() > quantity) {
product.setQuantity(product.getQuantity() - quantity);
}
}
}
}
Please note:
This implementation may include a few bugs, but that’s why we need to test it thoroughly.
The FooStore
sells FooProduct
products, which you can define as follows:
class FooProduct implements Product {
private final int id;
private final String name;
private final double price;
private int quantity;
public FooProduct(int id, String name, double price, int quantity) {
this.id = id;
this.name = name;
this.price = price;
this.quantity = quantity;
}
@Override
public int getId() { return id; }
@Override
public String getName() { return name; }
@Override
public double getPrice() { return price; }
@Override
public int getQuantity() { return quantity; }
@Override
public void setQuantity(int quantity) { this.quantity = quantity; }
}
Here, you have a FooStore
that sells FooProducts
, and each of these classes has implementations for the methods defined by their respective interfaces.
With the scenario in place, let’s take a look at some JUnit assertions and how you can write test cases for the sample application. The following examples were written using JUnit version 5.8.2.
Using assertTrue and assertFalse
assertTrue
and assertFalse
are two basic JUnit assertions that all Java developers should know. These assertions check the result of Boolean operations.
For example, you can use assertTrue
to test that the addProduct
method does indeed add to the FooStore
’s products
map:
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.Test;
class JUnitAssertsTests {
@Test
void addProduct_testingAssertTrue() {
FooStore fooStore = new FooStore();
FooProduct fooProduct = new FooProduct(1234, "test", 9.99, 1);
fooStore.addProduct(fooProduct);
assertTrue(fooStore.getProducts().containsKey(1234));
}
}
In a similar vein, you can use assertFalse
to check that the map no longer contains fooProduct
if you add it; then remove it with the removeProduct
method:
@Test
void addThenRemoveProduct_testingAssertFalse() {
FooStore fooStore = new FooStore();
FooProduct fooProduct = new FooProduct(1234, "test", 9.99, 1);
fooStore.addProduct(fooProduct);
assertTrue(fooStore.getProducts().containsKey(1234));
fooStore.removeProduct(1234);
assertFalse(fooStore.getProducts().containsKey(1234));
}
Using assertEquals and assertNotEquals
Two other assertions that you’ll use frequently are assertEquals
and assertNotEquals
. These assertions are most commonly used to verify that a method returns the expected result. The two arguments that you supply to these assertions are evaluated for equality. Here, equality means that the object’s equals()
method returns true (ie a.equals(b)
).
For example, you can use assertEquals
to test that the product you add with the addProduct
method contains the expected parameters:
@Test
void addProduct_testingAssertEquals() {
FooStore fooStore = new FooStore();
FooProduct fooProduct = new FooProduct(1234, "test", 9.99, 1);
fooStore.addProduct(fooProduct);
Product actualProduct = fooStore.getProducts().get(1234);
assertEquals("test", actualProduct.getName());
assertEquals(9.99, actualProduct.getPrice());
assertEquals(1, actualProduct.getQuantity());
}
A tool like Diffblue Cover can be invaluable in helping you generate unit tests automatically. For instance, with the following Diffblue Cover CLI command, you can generate a test for addProduct
:
dcover create com.example.FooStore.addProduct
Diffblue Cover quickly produces the following test. It makes use of the Mockito framework to generate mocks and uses the assertEquals
assertion:
@Test
void testAddProduct() {
// Arrange
FooStore fooStore = new FooStore();
Product product = mock(Product.class);
when(product.getId()).thenReturn(1);
// Act
fooStore.addProduct(product);
// Assert
verify(product).getId();
assertEquals(1, fooStore.getProducts().size());
}
In this example, Diffblue Cover mocks the Product
class to avoid having to create a real instance during testing.
You can also test that the sellProduct
method reduces the quantity of the product as expected:
@Test
void sellProduct_testingAssertEquals() {
FooStore fooStore = new FooStore();
FooProduct fooProduct = new FooProduct(1234, "test", 9.99, 5);
fooStore.addProduct(fooProduct);
Product actualProduct = fooStore.getProducts().get(1234);
assertEquals(5, actualProduct.getQuantity());
fooStore.sellProduct(1234, 5);
assertEquals(0, actualProduct.getQuantity()); // All products sold!
}
As you can see, with the current implementation, this test actually fails. Upon closer inspection, the second if
statement in the sellProduct
method should be a >=
instead of a >
:
if (product.getQuantity() >= quantity) {
product.setQuantity(product.getQuantity() - quantity);
}
After making this change, your test should pass.
As for the assertNotEquals
assertion, let’s consider a scenario that you want to test when a product goes on sale:
@Test
void addThenAddProduct_testingAssertNotEquals() {
FooStore fooStore = new FooStore();
FooProduct fooProduct = new FooProduct(1234, "test", 9.99, 1);
fooStore.addProduct(fooProduct);
Product actualProduct = fooStore.getProducts().get(1234);
assertEquals(9.99, actualProduct.getPrice());
FooProduct fooProductSale = new FooProduct(1234, "test", 6.99, 1);
fooStore.addProduct(fooProductSale); // Should replace fooProduct
Product actualProductSale = fooStore.getProducts().get(1234);
assertEquals(1, actualProductSale.getQuantity());
assertNotEquals(9.99, actualProductSale.getPrice()); // Expected: 6.99
}
Please note:
You could also use assertEquals
here to check that the product’s price is equal to the sale price of $6.99 USD:
assertEquals(6.99, actualProductSale.getPrice());
However, in this instance, you chose assertNotEquals
to emphasize that the price should not be equal to the original price whenever there is a sale. The test could be rewritten like this:
assertTrue(actualProductSale.getPrice() < 9.99);
This test is perhaps even better than assertNotEquals
since it asserts that the price must be lower during sales.
As you can see, there are many ways to write tests to achieve similar results. Different assertions emphasize different things; so choose the assertion that most closely aligns with the underlying intention of your tests.
Using assertSame and assertNotSame
The assertSame
assertion is stricter than assertEquals
: it checks to see if the two objects compared are one and the same. To evaluate sameness, assertSame
uses the ==
operator (ie a == b
).
For example, you can verify that the FooProduct
you add via addProduct
is exactly the same FooProduct
retrieved from the FooStore
products
map:
@Test
void addProduct_testingAssertSame() {
FooStore fooStore = new FooStore();
FooProduct fooProduct = new FooProduct(1234, "test", 9.99, 1);
fooStore.addProduct(fooProduct);
Product actualProduct = fooStore.getProducts().get(1234);
assertSame(fooProduct, actualProduct);
}
In the previous example, using assertEquals
in place of assertSame
would work as well since, in addition to being identical to each other, both objects are also equal. In other words, the default fooProduct.equals(actualProduct)
method would return true. To see the difference between assertEquals
and assertSame
, consider the following test: first override the equals()
implementation for FooProduct
:
@Override
public boolean equals(Object o) {
FooProduct other = (FooProduct) o;
return this.id == other.getId() &&
this.name.equals( == other.getName()) &&
this.price == other.getPrice() &&
this.quantity == other.getQuantity();
}
Then, consider the following test:
@Test
void differenceBetweenAssertSameAssertEquals() {
Product product1 = new FooProduct(1, "test", 0.99, 5);
Product product2 = new FooProduct(1, "test", 0.99, 5);
assertNotSame(product1, product2);
assertEquals(product1, product2);
}
In this example, the assertNotSame
assertion is true because the two objects are not identical. The assertEquals
assertion is also true because of the way we defined the equals()
method: as long as this evaluates to true, the two objects are equal. In other words, product1
and product2
are equal, but they are not the same.
Using assertNull and assertNotNull
Lastly, let’s review assertNull
and assertNotNull
. You can use these assertions to check whether objects are empty or non-empty.
For example, let’s check that the products are correctly added to the products
map using these assertions:
@Test
void addProduct_testNullAndNotNull() {
FooStore fooStore = new FooStore();
FooProduct fooProduct = new FooProduct(1234, "test", 9.99, 1);
fooStore.addProduct(fooProduct);
assertNull(fooStore.getProducts().get(12345));
assertNotNull(fooStore.getProducts().get(1234));
}
This works because the Map.get()
method returns null
if no entry in the map exists with the given key.
Assertions for Exceptions
In addition to testing for expected results, you’ll also often have to test for expected exceptions. For example, let’s expand our sellProduct
implementation to account for cases where you don’t have enough units to sell.
Start by creating a custom InsufficientInventoryException
class for this scenario:
public class InsufficientInventoryException extends Exception {
public InsufficientInventoryException(String errorMessage) {
super(errorMessage);
}
}
Then throw this exception in sellProduct
(Please note: You have to declare the exception in the signature, which means you have to update the interface method’s signature as well):
@Override
public void sellProduct(int productId, int quantity) throws InsufficientInventoryException {
Product product = products.get(productId);
if (product != null) {
if (product.getQuantity() >= quantity) {
product.setQuantity(product.getQuantity() - quantity);
} else {
throw new InsufficientInventoryException(
"Not enough inventory to sell " + Integer.toString(quantity) +
" units of productId " + Integer.toString(productId));
}
}
}
Make sure that your code throws an InsufficientInventoryException
when it reaches this case.
Using assertThrows
After you’ve added the exception, you can test that the InsufficientInventoryException
is thrown using the assertThrows
JUnit assertion. The syntax for this is as follows:
@Test
void sellProduct_testingAssertThrows() {
FooStore fooStore = new FooStore();
FooProduct fooProduct = new FooProduct(1234, "test", 9.99, 5);
fooStore.addProduct(fooProduct);
Product actualProduct = fooStore.getProducts().get(1234);
assertEquals(5, actualProduct.getQuantity());
assertThrows(InsufficientInventoryException.class, () -> {
fooStore.sellProduct(1234, 6); // We expect this call to throw InsufficientInventoryException.
});
}
The first argument to use assertThrows
is the type of exception you expect to be thrown. The second argument is a lambda expression that calls the method that throws the exception.
Please note:
In the method signature, you don’t have to declare throws InsufficientInventoryException
. This is because the assertThrows
assertion automatically consumes the caught exception by default.
Custom Assertions and Hamcrest Matchers
In some scenarios, you may want to create custom assertions that aren’t natively available in the JUnit framework. For example, suppose you want to test whether a list is sorted. You might create a custom assertSorted
assertion in a testing utility class (ie TestUtil.java
) as follows:
public static void assertSorted(List<Integer> list) {
for (int i = 0; i < list.size() - 1; i++) {
assertTrue(list.get(i) > list.get(i + 1));
}
}
Then you can write a unit test that calls this assertSorted
method just like you would any other JUnit assertion:
@Test
public void testSort() {
List<Integer> list = Arrays.asList(3, 1, 4, 1, 5, 9, 2, 6);
Collections.sort(list);
TestUtil.assertSorted(list);
}
Understanding Hamcrest Matchers
For additional customizability in your unit tests, consider using Hamcrest. Hamcrest is a popular framework that you can use together with JUnit to significantly improve the readability of your tests. For instance, consider the assertion you wrote earlier:
assertEquals(5, actualProduct.getQuantity());
If you were to write this assertion using Hamcrest, it would look like this:
assertThat(actualProduct.getQuantity(), equalTo(5));
This assertion reads much closer to plain English: “Assert that actualProduct’s quantity is 5.” Here, assertThat
is a special Hamcrest construct for creating test assertions. equalTo()
is a Hamcrest matcher—a construct used to verify results.
is()
is another common Hamcrest matcher. is()
checks for object equality, similar to the assertEquals
JUnit assertion. For example, here’s one of your previous tests using Hamcrest:
@Test
void addProduct_testingAssertSame_Hamcrest() {
FooStore fooStore = new FooStore();
FooProduct fooProduct = new FooProduct(1234, "test", 9.99, 1);
fooStore.addProduct(fooProduct);
Product actualProduct = fooStore.getProducts().get(1234);
assertThat(actualProduct, is(fooProduct));
}
You can also nest Hamcrest matchers. This is most commonly used with matchers like is()
and not()
:
@Test
void addProduct_testNullAndNotNull_Hamcrest() {
FooStore fooStore = new FooStore();
FooProduct fooProduct = new FooProduct(1234, "test", 9.99, 1);
fooStore.addProduct(fooProduct);
assertThat(fooStore.getProducts().get(12345), is(nullValue()));
assertThat(fooStore.getProducts().get(1234), is(not(nullValue()))); // is(notNullValue()) also okay
}
Combining JUnit Assertions for Complex Tests
Sometimes, you may want to combine multiple JUnit assertions. This is particularly useful for operations where you have to verify that multiple things are all true to determine a test’s success or failure. In JUnit, you can use the assertAll
assertion to achieve this.
For instance, you can verify that all properties of your FooProduct
are as expected with the following test (note that Hamcrest is used in the last two nested assertions as well):
@Test
void addProduct_verifyAllProperties_assertAll() {
FooStore fooStore = new FooStore();
FooProduct fooProduct = new FooProduct(1234, "test", 9.99, 1);
fooStore.addProduct(fooProduct);
assertAll("fooProduct should exist and contain the correct parameters",
() -> assertTrue(fooStore.getProducts().containsKey(1234)),
() -> assertEquals(1234, fooStore.getProducts().get(1234).getId()),
() -> assertEquals("test", fooStore.getProducts().get(1234).getName()),
() -> assertThat(fooStore.getProducts().get(1234).getPrice(), equalTo(9.99)),
() -> assertThat(fooStore.getProducts().get(1234).getQuantity(), equalTo(1))
);
}
Since JUnit evaluates all assertions in a single assertAll
, you can potentially use it to cut down on debugging times. If you had separate assertions, the test would fail on the first assertion and you wouldn’t know the result of any subsequent assertions. However, be mindful that combining assertions could overcomplicate your tests and make them harder to read.
Conclusion
In this article, you learned about all kinds of different JUnit assertions and saw code examples for each of them. Here’s a recap of the most common ones:
assertTrue
andassertFalse
: used to check the results of Boolean operations.assertEquals
andassertNotEquals
: used to check that the actual result of an operation is what you expect. This assertion checks for object equality, meaning that JUnit uses theequals()
method for evaluation.assertSame
andassertNotSame
: used to check that the actual object is the same as the expected object. This assertion checks for object identity, meaning that JUnit uses the==
operation for evaluation.assertNull
andassertNotNull
: used to check that objects are empty or non-empty.assertThrows
: used to verify that a particular type of exception is thrown by an operation.
In addition to learning about the assertion earlier, you also saw how you can use the Hamcrest framework to construct more expressive tests as well as how to use assertAll
to combine related assertions into a single check. Being familiar with these tools is imperative for writing strong unit tests, which will improve the overall code quality across your codebase.
While JUnit is an essential tool to master, any Java developer will tell you that coming up with robust unit tests still takes time (at least 20% is typical but it’s often a lot more).
To further automate your unit testing and to give more productive time back to your developers, consider using Diffblue Cover. Cover is a tool that writes complete unit tests for Java code without any developer input. You can use it at any level, from an individual method to an entire project; it can even run completely automatically in a CI pipeline. Better still, Cover doesn’t just write tests for you – it also updates them automatically every time you change your code, and provides detailed coverage reports across your entire codebase. You can try Cover now – including the full power of the CLI option – with this fourteen-day free trial.
Or if you’d just like to learn more about effective JUnit assertions, we’ve got more on the subject here.