Nice to meet you!

We're delighted to have you here. Need assistance with our services or products? Feel free to reach out.

Hero Illustration
0 Comments

Java Essentials: Your Ultimate Guide to Unit Testing 

Writing unit tests are highly recommended (and for some – mandatory) when writing code so the code is maintainable and has early bug detection.

It is important to focus on testing service layers, because that is where most of the logic lies.

How the test is conducted can depend on which stage of the project lifecycle you are currently working on. At project inception you may decide to focus on testing the classes in the major components and mocking other components which interact with that class. As the product matures, you will move into end-to-end testing of the full application, including its databases or message brokers.

This article focuses on how to write unit tests in the service layer with the JUnit Jupiter API. The JUnit Jupiter is the name for JUnit 5, if you are using JUnit 4, it has some differences, and you may consider migration by referring to this article, although we can use both JUnit 4 and Jupiter together. The Jupiter base package is under org.junit.jupiter, while JUnit is under org.junit.

Definitions

Mock testing: a testing technique to simulate the behaviour of components that become dependencies.

Stub: a simplified version of the component used in testing. Stub a function will return a predefined response or dummy data.

The difference is that mock is used to verify the behaviour of code under test by checking whether the correct methods were called with the correct arguments, while stub provides specific response.

Basic Jupiter Features

All these annotations are under the org.junit.jupiter.api package.

Example of the Whole Test Class

Example whole test class

Advanced Usage

For more advanced usage, like nested tests, parameterized test, and display name, please refer to this link.

Assertions

This part is used to assert that certain value in our test is like what we expect. All JUnit Jupiter assertions are static methods under org.junit.jupiter.api.Assertions package.

Each unit test that requires validation of values or exceptions thrown should utilize the methods provided within this package. This ensures that we can accurately verify expected values and handle exceptions appropriately during testing. Fails in assertions will make the unit test as fail.

assertEquals

assertEquals is to compare to 2 values in its arguments.
Example:

One important point to remember is that when we perform an assertion of an object, it’s crucial to ensure that the object has already implemented the equals method. Otherwise, the comparison would not function correctly.

assertThrows

JUnit 5 aims to solve some problems of JUnit 4 and takes advantage of Java 8 features, such as lambdas.

When it comes to asserting exceptions, the old way @Test annotation with parameter of exception class is also enhanced, A JUnit 4 example would be as the following:

Or we can use ExpectedException if we want to assert the value of exception.

As a replacement, JUnit 5 introduced the assertThrows() method: It asserts that the execution of the supplied executable throws an exception of the expected type and returns the exception instance, so assertions can be performed on it, we think that this new approach is cleaner and easier to use. Using assertThrows() will make the test will fail if no exception is thrown, or if an exception of a different type is thrown.

Example:

Mockito

If we want to test the class with the dependency to other class, we can use Mockito library to generate mock object to substitute other classes.

Imagine you’re a researcher in a laboratory conducting experiments. In these experiments, you’re often testing the reactions of certain compounds or organisms under specific conditions. Now, sometimes you need to isolate a particular variable or organism to observe its behaviour under controlled circumstances.

In this scenario, think of the mock object in unit tests as your controlled variable or organism. You create a mock object to mimic the behavior of a real object in your system. This mock object behaves exactly as you have defined it, allowing you to observe how other parts of your system interact with it.

Just as in the laboratory, where you isolate certain variables to observe their effects, in unit testing, you isolate specific components of your system using mock objects to observe how they interact with other parts of the system. This controlled environment helps you verify that each component behaves as expected without interference from external factors.

  • Create mock object.
    We can create mock object with syntax like this, as for its usage, will be explained late.
  • Stubbing methods will return value.
    Now, we want to get the value of the method returned by mock object, we can do it by:

when is also static method under org.mockito.Mockito
we can verify that line by

  • Stubbing methods with exception.
    In case the object we interacted with throws an exception, we can also mock the method from the object which will throw an exception, by:

then, we can verify that:

  • Stubbing consecutive calls
    In some cases, we need to call the same method of an object with the same argument multiple times, but expecting different return values, we can do it like this:

It is also possible to mix the method with throwing an exception and returning a value like:

  • Verify method is called with certain argument.
    We can also verify that a mocked object method is called with a certain argument:

this line ensures that mockedList is called with argument “1” two times.

Or, if we just care that the method is called with any integer value:

If the argument is a class that we write, we can do it by:

We can also implement logic to check the argument by using argument matcher

  • Using real object and monitor or tamper it
    In some cases, we might use real object as our dependency with the class that we want to test, for example, using an object mapper which maps one or more values to another value.

    For this case, we can use spy syntax. You can think of spy syntax like a spy, which does its job normally, until you want to do it anything you command, you can also ask it to monitor the called interaction behaviour, or to respond with a method that you choose.

    Spy object initiation:

Then, we can use the spy variable like mock object, except it also behaves like a real object if we don’t specify what’s mocked, the example if we want to add 2 objects to list:

Then, we do assert that the 2 first elements are the same as those used before.

We also can verify that the method is called twice in the next line.

Last, we will mock try to mock the method, before that we will show the result what the method returns before we mock it:

As we can see in the last line, the method returns 2 as it should and then return 45 after we tell it to. Note: Spying when running with Java 17 leads to assertions failures, so use it with care!! It should be fixed by using mockito-inline, which they have made the default in the next major version of Mockito, referenced by this. This link, also mentioned to not mock object that you don’t own.

for example, if we mock by this:

it throws an IndexOutOfBoundsException.

  • Using Mockito Extension
    There is a shortcut to initiate a variable by using MockitoExtension.class by putting this line above our test class:

Then we can put in our field to initiate mock of a List:

We can also initiate a mock object as an argument of the test method:

Next, if we want to initiate spy object:

Summary

Unit testing is needed when the code often changes, has complex logic, or is maintained by multiple people. It also drives engineers to make the code easy to test. For example, fat method makes it harder to test compared to several small methods. We also might find the case that some of our code does not get tested.

It’s always a good idea to create a test scenario which is hard to simulate or is cumbersome in the real world. Let’s say that we want to test a method which handles long names. If we want to do it manually, it could take a long time, as we must input names, requiring registration, email verification, login, opening the profile menu, etc., and it only covers 1 case. Creating unit tests in these cases is an investment from which we can reap the benefits directly. We hope this article covers the basic materials needed to start unit test in Java using JUnit Jupiter API. For more advanced usages you can go through the references below.

Source:

https://junit.org/junit5/docs/current/user-guide/#writing-tests-classes-and-methods

https://javadoc.io/doc/org.mockito/mockito-core/latest/org/mockito/Mockito.html#0

https://javadoc.io/doc/org.mockito/mockito-junit-jupiter/latest/org/mockito/junit/jupiter/MockitoExtension.html

https://www.linkedin.com/pulse/mock-vs-stub-fake-understand-difference-keploy-u4kmc/

https://melkornemesis.medium.com/mocks-vs-stubs-choosing-the-right-tool-for-the-job-dbdbc19cf0c5#:~:text=A%20stub%20is%20a%20dummy,its%20behavior%20in%20a%20test.

https://www.baeldung.com/junit-assert-exception

Contact us to learn more!

Please complete the brief information below and we will follow up shortly.

    ** All fields are required
    Leave a comment