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.