简体   繁体   中英

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 = "recipient@abc.com";
    private String emailSenderAddress = "sender@abc.com";
    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);
        }
    }
}

Unfortunately there are simply many things wrong with the code given in the question. For example: you instruct your test to throw an exception. And to expect that exception to make it up into the test.

But:

  try {
        mailSender.send(message);
    } catch (MailException e) {
        LOG.error("Error while sending notification email: ", e);
    }

Your production code is catching the exception. So you wrote a test that can only pass when your production code is incorrect !

Now, if the test is wrong: you could look into mocking that logger object; to verify that a call to it happens. And you would change the test case to not expect any exception. That's the whole point of try/catch; isn't it.

Or the other way round: if not catching is what you want; your unit test told you that the try/catch has to go away.

As said, that is just one problem here - the other answers do a good job listing them.

From that point of view may answer is: don't try to learn unit testing by trial and error. Instead: get a good book or tutorial and learn how to do unit testing - including how to use mocking frameworks correctly.

You shouldn't really be mocking a class that you are trying to test. I think what you really want to do here is to mock the MailSender to throw an exception. I notice you're already using a mock MailSender in your successful test. Just use this again and set the expectation:

 when(mailSender.send(any(SimpleMailMessage.class))).thenThrow(MailException.class);

But as mentioned in @GhostCat's answer you are doing exception handling in the method so the you'd need to specify a different expectation other than the exception to be thrown. You could mock the logger but mocking static loggers is usually a lot more effort that it's worth. You might want to consider reworking your exception handling to make it easier to test.

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 = "recipient@abc.com";
private String emailSenderAddress = "sender@abc.com";
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.

1) See this doc on how to mock void methods with exception:

In your case it should be something like this:

doThrow(new MailException()).when(mockEmailService).notify( anyString() );

2) Your testNotify does not do proper testing. After calling actual notify method is does not check the expected outcome.

3) Your testNotifyMailException first mocks notify and then calls actual notify method on non mocked EmailService. The whole point of mocking notify is to test the code that calls it and not the actual method you are mocking.

Based on the above-mentioned responses I got it working by modifying both my actual class and unit test.

EmailSendException.java (New class added in order to promote testablity)

public class EmailSendException extends RuntimeException {
    private static final long serialVersionUID = 1L;

    public EmailSendException(String message) {
        super(message);
    }

    public EmailSendException(String message, Throwable cause) {
        super(message, cause);
    }
}

EmailService.java (Instead of catching, throwing a RuntimeException)

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;
    private static final String ERROR_MSG = "Error while sending notification email";

    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) {
            throw new EmailSendException(ERROR_MSG, e);
        }
    }
}

EmailServiceTest.java (Mocking and testing )

public class EmailServiceTest {

    @Rule
    public MockitoJUnitRule rule = new MockitoJUnitRule(this);

    @Mock
    private MailSender mailSender;
    private String emailRecipientAddress = "recipient@abc.com";
    private String emailSenderAddress = "sender@abc.com";
    private String messageBody = "Hello Message Body!!!";
    @Mock
    private EmailService mockEmailService;

    @Test
    public void testNotify() {
        EmailService EmailService = new EmailService(mailSender, emailRecipientAddress, emailSenderAddress);
        EmailService.notify(messageBody);
    }

    @Test(expected = KlarnaEmailSendException.class)
    public void testNotifyMailException() {
        doThrow(new KlarnaEmailSendException("Some error message")).when(mockKlarnaEmailService).notify(anyString());
        mockKlarnaEmailService.notify(messageBody);
    }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM