Adding Value to Your Tests with Custom Mockito Matchers

Lane Maxwell Senior Software Engineer
Lane Maxwell
Senior Software Engineer

For our unit tests, we all know to use mocks to test our collaborators. In order for our tests to have the most possible value, the matchers we use against our mocks need to be a strict as they possibly can be. We’re going to create a simple controller, which takes a DTO, constructs an object from it, and calls a service method, which does some work and returns the object back to the caller. The service is injected into the controller, which we need to test.

Here’s our controller, nothing fancy here. receives a DTO, creates an object that satisfies the contract for the MessageService, and invokes the service method.

We want to test this behavior..

 

@Controller
@RequestMapping("/message")
public class MessageController {

    @Autowired
    private MessageService messageService;

    @PostMapping
    public Message createMessage (@RequestBody MessageDTO messageDTO) {
        Message message = new Message();
        message.setText(messageDTO.getText());
        message.setFrom(messageDTO.getFrom());
        message.setTo(messageDTO.getTo());
        message.setDate(Date.from(Instant.now()));
        message.setId(UUID.randomUUID());

        return messageService.deliverMessage(message);
    }
}

The service is plain and simple, receive the Message and return it.

@Service
public class MessageService {

    public Message deliverMessage (Message message) {

        return message;
    }
}

Our model looks like this.

@Data
public class Message {
    private String from;
    private String to;
    private String text;
    private Date date;
    private UUID id;
}

And finally the DTO, carrying just the relevant data.

@Data
public class MessageDTO {
    private String from;
    private String to;
    private String text;
}

The test is pretty straightforward and appears to successfully test our logic. It creates a Mock for the MessageService, injects it into the controller under test, constructs the DTO and invokes the controller method. Our verification is simple, verify that we called the MessageService exactly 1 time with any Message. There’s no need for explicit assertions here, the call to verify is doing this for us.

@RunWith(MockitoJUnitRunner.class)
public class MessageControllerTest {

    @Mock
    private MessageService messageService;

    @InjectMocks
    private MessageController messageController;

    @Test
    public void createMessage_NewMessage() {
        MessageDTO messageDTO = new MessageDTO();
        messageDTO.setFrom("me");
        messageDTO.setTo("you");
        messageDTO.setText("Hello, you!");

        messageController.createMessage(messageDTO);

        verify(messageService, times(1)).deliverMessage(any(Message.class));
    }

}

The issue with this test is that we’re not truly able to test the behavior because of the any matcher. Due to the fact that the Message is constructed inside the method under test, we’re forced to use any as the matcher. So what happens when the wayward developer comes into our controller and changes

message.setFrom(messageDTO.getFrom());

to

message.setFrom("Santa Claus");

Suddenly all of our messages are now coming from Santa Claus, but our tests are still passing. What we really want to do is test that #to, #from, and #text are being set to their proper values. To achieve this, we’ll write a custom matcher. The matcher will need to match the three fields that we’re concerned with. We’ll create a custom matcher that extends ArgumentMatcher, relevant for the Message type

public class MessageMatcher extends ArgumentMatcher<Message> {

    private Message left;

    public MessageMatcher(Message message) {
        this.left = message;
    }

    @Override
    public boolean matches(Object object) {
        if (object instanceof Message) {
            Message right = (Message) object;
            return left.getFrom().equals(right.getFrom()) &&
                    left.getTo().equals(right.getTo()) &&
                    left.getText().equals(right.getText());
        }

        return false;
    }
}

Now, we’ll modify our test logic slightly to make a stricter match, using argThat and passing it an instance of our Matcher, which wraps our expected Message.

@Test
public void createMessage_NewMessage() {
    MessageDTO messageDTO = new MessageDTO();
    messageDTO.setFrom("me");
    messageDTO.setTo("you");
    messageDTO.setText("Hello, you!");

    messageController.createMessage(messageDTO);

    Message message = new Message();
    message.setFrom("me");
    message.setTo("you");
    message.setText("Hello, you!");
    verify(messageService, times(1)).deliverMessage(argThat(new MessageMatcher(message)));
}

At this point, we’re protected against someone modifying our controller logic to make Santa the sender. That, however, won’t prevent them from removing the Date or ID. So, let’s work on our matcher a little more…

@Override
public boolean matches(Object object) {
    if (object instanceof Message) {
        Message right = (Message) object;
        return left.getFrom().equals(right.getFrom()) &&
                left.getTo().equals(right.getTo()) &&
                left.getText().equals(right.getText()) &&
                right.getDate() != null &&
                right.getId() != null;
    }

    return false;
}

Now we know our Message instance will have the correct from, to, and text, and at least the Date and Id will have been set. We can get more creative to further restrict the Message by checking the date, e.g.,

  return left.getFrom().equals(right.getFrom()) &&
            left.getTo().equals(right.getTo()) &&
            left.getText().equals(right.getText()) &&
            DateUtils.isSameDay(left.getDate(), (left.getDate())) &&
            right.getId() != null;

For further reading, see the Mockito Documentation