I have written the following unit test using mockito
to test my EmailService.java
class. What I am not sure is if I am testing this correctly or not (both happy path and exception scenario).
Moreover I am getting this Error: 'void' type not allowed here
in the following code snippet in my unit test
when(mockEmailService.notify(anyString())).thenThrow(MailException.class);
I understand that since my notify()
method is returning void I am getting that exception. But not sure how to resolve this. Is there any code change required in my unit test or actual class or both?
Can somebody please guide.
EmailServiceTest.java
public class EmailServiceTest {
@Rule
public MockitoJUnitRule rule = new MockitoJUnitRule(this);
@Mock
private MailSender mailSender;
@Mock
private EmailService mockEmailService;
private String emailRecipientAddress = "[email protected]";
private String emailSenderAddress = "[email protected]";
private String messageBody = "Hello Message Body!!!";
@Test
public void testNotify() {
EmailService emailService = new EmailService(mailSender, emailRecipientAddress, emailSenderAddress);
emailService.notify(messageBody);
}
@Test(expected = MailException.class)
public void testNotifyMailException() {
when(mockEmailService.notify(anyString())).thenThrow(MailException.class);
EmailService emailService = new EmailService(mailSender, emailRecipientAddress, emailSenderAddress);
emailService.notify(messageBody);
}
}
EmailService.java
public class EmailService {
private static final Log LOG = LogFactory.getLog(EmailService.class);
private static final String EMAIL_SUBJECT = ":: Risk Assessment Job Summary Results::";
private final MailSender mailSender;
private final String emailRecipientAddress;
private final String emailSenderAddress;
public EmailService(MailSender mailSender, String emailRecipientAddress,
String emailSenderAddress) {
this.mailSender = mailSender;
this.emailRecipientAddress = emailRecipientAddress;
this.emailSenderAddress = emailSenderAddress;
}
public void notify(String messageBody) {
SimpleMailMessage message = new SimpleMailMessage();
message.setSubject(EMAIL_SUBJECT);
message.setTo(emailRecipientAddress);
message.setFrom(emailSenderAddress);
message.setText(messageBody);
try {
mailSender.send(message);
} catch (MailException e) {
LOG.error("Error while sending notification email: ", e);
}
}
}
I'm going to assume that the EmailService
implementation here is correct and focus on the tests. They're both flawed. While the testNotify
executes without errors, it isn't actually testing anything. Technically, it will at least confirm that notify
does not throw an exception when mailService
does not throw an exception. We can do better.
The key to writing good tests is to ask yourself, "What is this method supposed to do?" You should be able to answer that question before you even write the method. For specific tests, ask "What should it do with this input?" or "What should it do when its dependency does this?"
In the first case, you create a MailService
passing it a MailSender
and the to and from addresses. What is that MailService
instance supposed to do when its notify
method is called? It's supposed pass a SimpleMailMessage
to MailSender
through the send
method. Here's how you do that (Note, I assumed that MailSender
actually takes a MailMessage
interface instead of SimpleMailMessage
):
@Mock
private MailSender mailSender;
private EmailService emailService;
private String emailRecipientAddress = "[email protected]";
private String emailSenderAddress = "[email protected]";
private String messageBody = "Hello Message Body!!!";
@Before
public void setUp(){
MockitoAnnotations.initMocks(this);
emailService = new EmailService(mailSender, emailRecipientAddress, emailSenderAddress);
}
@Test
public void testMessageSent() throws MailException {
ArgumentCaptor<MailMessage> argument = ArgumentCaptor.forClass(MailMessage.class);
emailService.notify(messageBody);
Mockito.verify(mailSender).send(argument.capture());
Assert.assertEquals(emailRecipientAddress, argument.getValue().getTo());
Assert.assertEquals(emailSenderAddress, argument.getValue().getFrom());
Assert.assertEquals(messageBody, argument.getValue().getText());
}
This makes sure that EmailService
actually sends the message you'd expect based on arguments passed to its constructor and notify
method. We do not care whether or not MailSender
does its job correctly in this test. We just assume it works- presumably because it's either tested elsewhere or part of a provided library.
The test for the exception is a little more subtle. Since the exception is caught, logged, and then ignored there's not as much to test. I personally don't bother checking that anything is logged. What we really want to do is confirm that if MailSender
throws MailException
then notify
will not throw an exception. If MailException
is a RuntimeException
then we should test this. Basically, you just mock mailSender
to throw an exception. If EmailService
does not handle it correctly, then it will throw an exception and the test will fail (This uses the same setup as the previous example):
@Test
public void testMailException() throws MailException {
Mockito.doThrow(Mockito.mock(MailException.class)).when(mailSender).send(Mockito.any(MailMessage.class));
emailService.notify(messageBody);
}
Alternatively, we can catch the MailException
and then explicitly fail the test:
@Test
public void testMailExceptionAlternate() {
try {
Mockito.doThrow(Mockito.mock(MailException.class)).when(mailSender).send(Mockito.any(MailMessage.class));
emailService.notify(messageBody);
} catch (MailException ex){
Assert.fail("MailException was supposed to be caught.");
}
}
Both approaches confirm the desired behavior. The second is more clear in what it's testing. The downside, though, is that if notify
were allowed to throw a MailException
in other circumstances, then that test might not work.
Finally, if MailException
is a checked exception- that is it not a RuntimeException
- then you would not even need to test this. If notify
could possibly throw a MailException
then the compiler would require it declare it in the method signature.