web-dev-qa-db-fra.com

Mockito pour tester les méthodes de vide

J'ai le code suivant que je veux tester:

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

Je dois vérifier que la méthode acceptFromOffice définit vraiment le statut sur 0, puis le message de persistance, puis modifie son statut sur 1, puis le persiste à nouveau.

Avec Mockito, j'ai essayé de faire ce qui suit:

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

    }

En fait, je m'attends à ce que la vérification vérifie l'appel deux fois de la méthode makePersistent avec un état d'objet Message différent. Mais ça ne dit pas que 

Les arguments sont différents!

Des indices?

18
glaz666

Tester votre code n'est pas trivial, mais pas impossible. Ma première idée a été d'utiliser ArgumentCaptor , qui est beaucoup plus facile à utiliser et à comprendre que ArgumentMatcher . Malheureusement, le test échoue toujours - les raisons vont certainement au-delà de la portée de cette réponse, mais je peux vous aider si cela vous intéresse. Néanmoins, je trouve ce cas de test assez intéressant pour être montré (solution non correcte):

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

}

Malheureusement, la solution de travail nécessite une utilisation un peu compliquée de Réponse . En un mot, au lieu de laisser Mockito enregistrer et vérifier chaque appel, vous fournissez une sorte de méthode de rappel exécutée à chaque fois que votre code de test est exécuté. Dans cette méthode de rappel (objet MakePersistentCallback dans notre exemple), vous avez accès aux deux paramètres et vous pouvez modifier la valeur de retour. Ceci est un canon lourd et vous devriez l'utiliser avec soin:

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

L'exemple n'est pas complet, mais le test réussit et, plus important encore, échoue lorsque vous modifiez presque tout ce que vous faites dans CUT. Comme vous pouvez le voir, la méthode MakePersistentCallback.answer est appelée à chaque fois que l'on appelle messageService.acceptFromOffice(message) simulé. Dans naswer, vous pouvez effectuer toutes les vérifications souhaitées.

NB: utiliser avec prudence, le maintien de tels tests peut être gênant pour le moins.

24

Vous testez effectivement une machine à états. Il est assez facile de tester MessageService avec des implémentations personnalisées. Je pense que TestMessage serait la classe la plus intéressante.

Pour permettre au message/DAO d'enregistrer l'appel persistant, j'ai effectué une implémentation personnalisée.

Ce n'est pas Mockito mais est simple et devrait faire l'affaire.

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

}
0
extraneon