简体   繁体   中英

Mockito to test void methods

I have following code I want to test:

public class MessageService {
    private MessageDAO dao;

    public void acceptFromOffice(Message message) {
        message.setStatus(0);
        dao.makePersistent(message);

        message.setStatus(1);
        dao.makePersistent(message);

    }
    public void setDao (MessageDAO mD) { this.dao = mD; }
}

public class Message {
    private int status;
    public int getStatus () { return status; }
    public void setStatus (int s) { this.status = s; }

    public boolean equals (Object o) { return status == ((Message) o).status; }

    public int hashCode () { return status; }
}

I need to verify, that method acceptFromOffice really sets status to 0, than persist message, then chage its status to 1, and then persist it again.

With Mockito, I have tried to do following:

@Test
    public void testAcceptFromOffice () throws Exception {

        MessageDAO messageDAO = mock(MessageDAO.class);

        MessageService messageService = new MessageService();
        messageService.setDao(messageDAO);

        final Message message = spy(new Message());
        messageService.acceptFromOffice(message);

        verify(messageDAO).makePersistent(argThat(new BaseMatcher<Message>() {
            public boolean matches (Object item) {
                return ((Message) item).getStatus() == 0;
            }

            public void describeTo (Description description) { }
        }));

        verify(messageDAO).makePersistent(argThat(new BaseMatcher<Message>() {
            public boolean matches (Object item) {
                return ((Message) item).getStatus() == 1;
            }

            public void describeTo (Description description) { }
        }));

    }

I actually expect here that verification will verify calling twice of makePersistent method with a different Message object's state. But it fails saying that

Argument(s) are different!

Any clues?

Testing your code is not trivial, though not impossible. My first idea was to use ArgumentCaptor , which is much easier both to use and comprehend compared to ArgumentMatcher . Unfortunately the test still fails - reasons are certainly beyond the scope of this answer, but I may help if that interests you. Still I find this test case interesting enough to be shown ( not correct solution ):

@RunWith(MockitoJUnitRunner.class)
public class MessageServiceTest {

    @Mock
    private MessageDAO messageDAO = mock(MessageDAO.class);

    private MessageService messageService = new MessageService();

    @Before
    public void setup() {
        messageService.setDao(messageDAO);
    }

    @Test
    public void testAcceptFromOffice() throws Exception {
        //given
        final Message message = new Message();

        //when
        messageService.acceptFromOffice(message);

        //then
        ArgumentCaptor<Message> captor = ArgumentCaptor.forClass(Message.class);

        verify(messageDAO, times(2)).makePersistent(captor.capture());

        final List<Message> params = captor.getAllValues();
        assertThat(params).containsExactly(message, message);

        assertThat(params.get(0).getStatus()).isEqualTo(0);
        assertThat(params.get(1).getStatus()).isEqualTo(1);
    }

}

Unfortunately the working solution requires somewhat complicated use of Answer . In a nutshell, instead of letting Mockito to record and verify each invocation, you are providing sort of callback method that is executed every time your test code executes given mock. In this callback method ( MakePersistentCallback object in our example) you have access both to parameters and you can alter return value. This is a heavy cannon and you should use it with care:

    @Test
    public void testAcceptFromOffice2() throws Exception {
        //given
        final Message message = new Message();
        doAnswer(new MakePersistentCallback()).when(messageDAO).makePersistent(message);

        //when
        messageService.acceptFromOffice(message);

        //then
        verify(messageDAO, times(2)).makePersistent(message);
    }


    private static class MakePersistentCallback implements Answer {

        private int[] expectedStatuses = {0, 1};
        private int invocationNo;

        @Override
        public Object answer(InvocationOnMock invocation) throws Throwable {
            final Message actual = (Message)invocation.getArguments()[0];
            assertThat(actual.getStatus()).isEqualTo(expectedStatuses[invocationNo++]);
            return null;
        }
    }

The example is not complete, but now the test succeeds and, more importantly, fails when you change almost anything in CUT. As you can see MakePersistentCallback.answer method is called every time mocked messageService.acceptFromOffice(message) is called. Inside naswer you can perform all the verifications you want.

NB: Use with caution, maintaining such tests can be troublesome to say the least.

You are in effect testing a state machine. It's quite easy to test the MessageService with some custom implementations. I think TestMessage would be the most interesting class.

To allow the DAO/message to record the persist call I made a custom implementation.

It's not Mockito but it is simple, and should do the job.

class TestMessageDAO implements MessageDAO {
  // I have no clue what the MessageDAO does except for makePersistent
  // which is the only relevant part here

  public void makePersistent(Message message) {
    if (message instanceof TestMessage) {
      TestMessage test = (TestMessage)message;
      test.persistCalled(); // will be recorded by TestMessage
    } else {
      throw RuntimeException("This test DAO does not support non-test messages");
    }
  }
}

// Message isn't final so...
class TestMessage extends Message {
  enum state {
    STARTED, STATUS0, PERSIST0, STATUS1, PERSIST1
  }

  public void persistCalled() { // For testing only
    switch (state) {
      case STATUS0:
        state = PERSIST0;
        break;
      case STATUS1:
        state = PERSIST1;
        break;
      default:
        throw new RuntimeException("Invalid transition");
    }
  }

  public void setStatus(int status) {
    switch(state) {
      case STARTED:
        if (status != 0) {
          throw new IllegalArgumentException("0 required");
        }
        state = STATUS0;
        break;
      case PERSIST0:
        if (status != 1) {
          throw new IllegalArgumentException("1 required");
        }

        state = STATUS1;
        break;
      default:
        throw new RuntimeException("Invalid transition");
    }
  }
}

public class TestMessageService {

  @Test
  public void testService() {
    MessageDAO dao = new TestMessageDAO();
    Message message = new TestMessage();
    MessageService service = new MessageService();
    service.setDao(dao);
    service.acceptFromOffice(message);
  }

}

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