Search code examples
javaunit-testingjunitmockito

Not able to unit test the exception thrown in this method using mockito


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);
        }
    }
}

Solution

  • 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.