Mockito для тестирования пустых методов - PullRequest
19 голосов
/ 09 марта 2011

У меня есть следующий код, который я хочу проверить:

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

Мне нужно проверить, что метод acceptFromOffice действительно устанавливает статус в 0, затем сохранить сообщение, затем изменить его статус на 1, а затем сохранить его снова.

С Mockito я попытался сделать следующее:

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

    }

На самом деле я ожидаю, что проверка подтвердит двойной вызов метода makePersistent с другим состоянием объекта Message. Но нельзя сказать, что

Аргумент (ы) разные!

Есть какие-нибудь подсказки?

Ответы [ 2 ]

25 голосов
/ 09 марта 2011

Тестирование вашего кода не тривиально, хотя и не невозможно. Моя первая идея состояла в том, чтобы использовать ArgumentCaptor , который намного проще в использовании и понимании по сравнению с ArgumentMatcher . К сожалению, тест по-прежнему не проходит - причины, безусловно, выходят за рамки этого ответа, но я могу помочь, если это вас заинтересует Тем не менее я нахожу этот тестовый пример достаточно интересным для показа ( не правильное решение ):

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

}

К сожалению, рабочее решение требует несколько сложного использования Ответ . Вкратце, вместо того, чтобы позволять Mockito записывать и проверять каждый вызов, вы предоставляете своего рода метод обратного вызова, который выполняется каждый раз, когда ваш тестовый код выполняет данную имитацию. В этом методе обратного вызова (в нашем примере объект MakePersistentCallback) у вас есть доступ к обоим параметрам и вы можете изменить возвращаемое значение. Это тяжелая пушка, и вы должны использовать ее с осторожностью:

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

Пример не завершен, но теперь тест завершается успешно и, что более важно, дает сбой, когда вы почти что-либо изменили в CUT. Как видите, метод MakePersistentCallback.answer вызывается каждый раз, когда вызывается messageService.acceptFromOffice(message). Внутри naswer вы можете выполнить все необходимые проверки.

Примечание: используйте с осторожностью, поддерживая такие тесты, по меньшей мере, хлопотно.

0 голосов
/ 09 марта 2011

Вы тестируете конечный автомат.Довольно просто протестировать MessageService с некоторыми пользовательскими реализациями.Я думаю, что TestMessage был бы самым интересным классом.

Чтобы позволить DAO / сообщению записывать постоянный вызов, я сделал пользовательскую реализацию.

Это не Mockito, но равно простой, и должен делать работу.

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

}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...