class MailServer {
void send(String smtpMessage) throws IOException {
// Opens a connection to an SMTP server, sends the SMTP message
}
}
JUnit and Mockito tips
TweetPosted on Friday Jul 03, 2020 at 06:40PM in Technology
In this entry I’ll share some tips about JUnit and Mockito for making unit tests better.
Mockito annotations
Mockito has some annotations that can be used for reducing redundancy of tests:
-
@Mock
-
@InjectMocks
-
@Captor
Before looking into the usage of those annotations, let’s assume we have the following production code which consists of 2 classes.
The first one is called MailServer, which has a method called send() that sends an SMTP message which this object receives as the parameter of the method. Note that the MailServer class most probably needs to be mocked out when you want to write a unit test for a class which uses the MailServer class because it really opens a TCP connection to an SMTP server, which is not a preferable thing for unit tests.
The other class is called Messenger, which depends on the MailServer class. This class requires an instance of the MailServer class in its constructor. This class has a method called sendMail(), which has 3 parameters. The responsibility of this method is first constructing an SMTP message based on those 3 parameters and then asking the MailServer object to send the SMTP message. It also does quick error handling which translates IOException into an unchecked one with embedding the content.
class Messenger {
private final MailServer mailServer;
Messenger(MailServer mailServer) {
this.mailServer = mailServer;
}
void sendMail(String from, String to, String body) {
String smtpMessage = String.join("\n", "From: " + from, "To: " + to, "", body);
try {
mailServer.send(smtpMessage);
} catch (IOException e) {
throw new UncheckedIOException("Error! smtpMessage=" + smtpMessage, e);
}
}
}
Let’s try writing a unit test for the Messenger class. But we don’t want to use the real MailServer class because it really tries to open a connection to an SMTP server. It will make testing harder because in order to test with the real MailServer class, we really need an SMTP server which is up and running. Let’s avoid doing that and try using a mocked version of a MailServer instance for the testing here.
A happy path test case would look like the following:
class MessengerPlainTest {
MailServer mailServer;
Messenger sut;
@BeforeEach
void setUp() {
mailServer = Mockito.mock(MailServer.class);
sut = new Messenger(mailServer);
}
@Test
@DisplayName("Messenger constructs the SMTP message and feeds MailServer")
void test() throws IOException {
String expected = "From: joe@example.com\n"
+ "To: jane@example.com\n\n"
+ "Hello!";
sut.sendMail("joe@example.com", "jane@example.com", "Hello!");
Mockito.verify(mailServer).send(expected);
}
}
In the setUp() method, a mock MailServer object is created and injected into the constructor of the Messenger class. And in the test() method, first we create the expected SMTP message which the Messenger class has to create, then we call the sendMail() method and finally we verify that the send() method of the mock MailServer object has been called with the expected SMTP message.
With annotations, the test above can be written as follows:
@ExtendWith(MockitoExtension.class)
class MessengerTest {
@Mock
MailServer mailServer;
@InjectMocks
Messenger sut;
@Test
@DisplayName("Messenger constructs the SMTP message and feeds MailServer")
void test() throws IOException {
String expected = "From: joe@example.com\n"
+ "To: jane@example.com\n\n"
+ "Hello!";
sut.sendMail("joe@example.com", "jane@example.com", "Hello!");
Mockito.verify(mailServer).send(expected);
}
}
First we annotate the test class with @ExtendWith(MockitoExtension.class) (Note that it’s a JUnit5 specific annotation, so for JUnit4 tests we need something different). Having the test class annotated with that one, when there is a field annotated with @Mock in the test class, Mockito will automatically create a mock for the field and inject it. And when there is a field annotated with @InjectMocks, Mockito will automatically create a real instance of the declared type and inject the mocks that are created by the @Mock annotation.
This is especially beneficial when many mock objects are needed because it reduces the amount of repetitive mock() method calls and also removes the need for creating the object which gets tested and injecting the mocks into the object.
And also it provides a clean way to create a mock instance of a class which has a parameterized type. When we create a mock instance of the Consumer class, a straightforward way would be the following:
Consumer<String> consumer = Mockito.mock(Consumer.class);
The problem here is that it produces an unchecked assignment warning. Your IDE will complain about it and you will get this warning when the code compiles with -Xlint:unchecked :
Warning:(35, 49) java: unchecked conversion
required: java.util.function.Consumer<java.lang.String>
found: java.util.function.Consumer
With the @Mock annotation, we can get rid of the warning:
@ExtendWith(MockitoExtension.class)
class MyTest {
@Mock
Consumer<String> consumer;
...
There is another useful annotation called @Captor. Let’s see the following test case:
@ExtendWith(MockitoExtension.class)
class MessengerCaptorTest {
@Mock
MailServer mailServer;
@InjectMocks
Messenger sut;
@Captor
ArgumentCaptor<String> captor;
@Test
@DisplayName("Messenger constructs the SMTP message and feeds MailServer")
void test() throws IOException {
sut.sendMail("joe@example.com", "jane@example.com", "Hello!");
Mockito.verify(mailServer).send(captor.capture());
String capturedValue = captor.getValue();
assertTrue(capturedValue.endsWith("Hello!"));
}
}
The @Captor annotation creates an object called ArgumentCaptor which captures a method parameter of a method call of a mock object. In order to capture a parameter with an ArgumentCaptor, first we need to call the capture() method in a method call chain of the Mockito.verify() method. Then we can get the captured value with the getValue() method and we can do any assertion against it. It’s especially useful in a situation where checking the equality is not sufficient and a complex verification is needed.
AssertJ
AssertJ is an assertion library for unit tests written in Java. It provides better readability and richer assertions than its older equivalents like the one shipped with JUnit. Let’s see some example code from the official website:
// entry point for all assertThat methods and utility methods (e.g. entry)
import static org.assertj.core.api.Assertions.*;
// basic assertions
assertThat(frodo.getName()).isEqualTo("Frodo");
assertThat(frodo).isNotEqualTo(sauron);
// chaining string specific assertions
assertThat(frodo.getName()).startsWith("Fro")
.endsWith("do")
.isEqualToIgnoringCase("frodo");
// collection specific assertions (there are plenty more)
// in the examples below fellowshipOfTheRing is a List<TolkienCharacter>
assertThat(fellowshipOfTheRing).hasSize(9)
.contains(frodo, sam)
.doesNotContain(sauron);
A unique feature of AssertJ is that all of the assertions here begin with the method assertThat() which receives the parameter that gets asserted. After that we specify the conditions the parameter needs to fulfill. An advantage of this approach is that we can specify multiple conditions with method call chain. It’s more readable and less verbose than the old way where repetitive assertTrue() or assertEquals() calls are involved. It also provides rich assertions for widely used classes like List, Set or Map.
Another useful feature of AssertJ is for verifying an unhappy path where an Exception is involved. Let’s remember the production code we used in the Mockito annotation section and consider a situation where the MailServer cannot connect to the SMTP server. Due to that, the send() method throws IOException. In this situation, the Messenger class is expected to catch the IOException and translate it into UncheckedIOException with the SMTP message embedded. A unit test for this can be written as follows with AssertJ:
@ExtendWith(MockitoExtension.class)
class MessengerUnhappyTest {
@Mock
MailServer mailServer;
@InjectMocks
Messenger sut;
@Test
@DisplayName("Messenger throws UncheckedIOException with the SMTP message when MailServer has thrown IOException")
void test() throws IOException {
doThrow(new IOException("The server is down")).when(mailServer).send(any());
String expectedMessage = "From: joe@example.com\n"
+ "To: jane@example.com\n\n"
+ "Hello!";
assertThatThrownBy(() -> sut.sendMail("joe@example.com", "jane@example.com", "Hello!"))
.isInstanceOf(UncheckedIOException.class)
.hasMessage("Error! smtpMessage=%s", expectedMessage)
.hasCauseInstanceOf(IOException.class);
}
}
First we make the mock MailServer instance throw IOException when its send() method is called. After that we pass a lambda expression which calls the sendMail() method to the assertThatThrownBy() method of AssertJ. After that we can do various assertions. What we are checking here is that the sendMail() method throws UncheckedIOException with the SMTP message embedded and it also contains a parent Exception whose class is IOException.
Conclusion
We’ve discussed some tips about Mockito and the basic uses of AssertJ for test cases that are written in JUnit. Both Mockito and AssertJ have extensive documents and rich functionality which greatly helps writing unit tests. I highly recommend checking the references below:
The pieces of code which are used in this entry can be found on GitHub: https://github.com/nuzayats/junittips
Tags: test