Mock 🧪

A ideia por trás dos objetos mock está na possibilidade de simular o comportamento de uma ou mais dependências (acoplamentos) que por ventura possa existir em um método. Uma vez que conseguimos simular e, consequentemente, controlar o comportamento das dependências, podemos então testar de forma segura um trecho de código do nosso interesse.

A grande maioria de linguagens de programação possui frameworks para construir objetos mock. Em Java, por exemplo, existe uma série de ferramentas capazes de realizar essa tarefa, entre elas: Mockito, EasyMock, JMock.

Possivelmente, o Mockito seja o framework em Java mais utilizado na construção de objetos mock. Nesse sentido, observe trecho de código do exemplo abaixo que ilustra a utilização de objetos mock em um teste unitário: 😃

// 1 - Estende o Junit para suportar, por exemplo, injeção de dependência de objetos Mock
@ExtendWith(MockitoExtension.class)
public class AppTest {

    // 2 - Cria um objeto mock da interface (ou classe) DataBase
    @Mock
    DataBase base;

    @Test
    public void create() {
        // 3 - define o comportamento do método createUser (stub)
        when(base.createUser("Rodrigo")).thenReturn("Rodrigo");

        // TODO ... código do método de teste

        assertEquals("Rodrigo", base.createUser("Rodrigo"));
    }

    @Test
    public void delete() {
        when(base.deleteUser(5L)).thenReturn(false);

        // TODO ... código do método de teste

        assertEquals(false, base.deleteUser(5L));
    }

    @Test
    public void deleteProblem() {
        // 4 - define que o método deleteUser irá lançar uma exceção se receber um valor negativo
        when(base.deleteUser(-1L)).thenThrow(new IllegalArgumentException());

        // TODO ... código do método de teste

        // 5 - verifica se a exceção lançada é igual a esperada
        Assertions.assertThrows(IllegalArgumentException.class, () -> {
            base.deleteUser(-1L);
        });
    }
}

Como pode ser visto no item (3) do exemplo acima, utilizamos o comando when para criar um stub. Um stub faz com que uma chamada de método sempre retorne o mesmo valor, ou seja, com essa técnica podemos prever o comportamento das dependências e testar de forma segura um trecho de código.

Principais anotações do Mockito

O Mockito possui algumas anotações úteis que nos auxiliam no momento de construir objetos mock, não elas: @Mock, @Spy, @InjectMocks e @Captor.

A anotação mais usada no Mockito é a @Mock. Por meio desta anotação podemos criar e injetar instâncias de classes/interfaces simuladas e, por meio da operação de stub, podemos definir os valores de retorno para as chamadas dos métodos. O exemplo acima demostra a utilização da anotação @Mock.

Já a anotação @Spy é usada para adicionar um mecanismo de rastreamento em um objeto real, por essa razão, trata-se de um mock “parcial”, vejamos um exemplo:

@ExtendWith(MockitoExtension.class)
public class MockitoSpyTest {

    @Spy
    private final List<String> list = new ArrayList<>();

    @Test
    public void shouldAddItemsToListSuccessfully() {
        // 1 - estamos fazendo algumas operações no objeto que estamos espionando
        // onde cada chamada é rastreada pelo Mockito.
        list.add("one");
        list.add("two");

        // 2- o método verify analisa se algumas das condições especificadas
        // foram atendidas
        verify(list, times(2)).add(anyString());

        // 3 - verificando se o método add foi chamado com o valor esperado
        verify(list).add("one");
        verify(list).add("two");

        // 4 - a assertiva prova que o método add foi chamado na instância real
        Assert.assertEquals(2, list.size());
    }
}

Podemos configurar os objetos que estamos espionando de forma que os métodos selecionados retornem um valor específico (stub), veja o exemplo abaixo:

@ExtendWith(MockitoExtension.class)
public class MockitoSpyStubTest {

    @Spy
    private final List<String> list = new ArrayList<>();

    @Test
    public void shouldReturnDifferentSizeWhenStubbed() {

        // 1 - Estamos sobrescrevendo o comportamento original do método size()
        // (stub)
        when(list.size()).thenReturn(100);

        list.add("one");
        list.add("two");

        verify(list, times(2)).add(anyString());

        verify(list).add("one");
        verify(list).add("two");

        // 2- Nesse caso, não podemos mais esperar que o método size retorne 2
        Assertions.assertEquals(100, list.size());
    }

}

A anotação @InjectMocks permite injetar objetos Mock em um objeto real. Vejamos um exemplo, imagine uma interface chama Network e uma classe Communication que utiliza essa interface:

public interface Network {

    public boolean send(String message);

}
public class Communication {

    private Network network;

    public boolean send(String message) {
        boolean result = false;
        try {
            result = network.send(message);
        } catch (Exception e) {
            // TODO: handle exception
        }
        return result;
    }

}
@ExtendWith(MockitoExtension.class)
public class MockitoInjectMocksTest {

    // 1 - a interface Network, que é uma dependência da classe Communication, será simulada
    @Mock
    Network network;

    // 2 - a anotação @InjectMocks permite criar um mock da classe Communication e resolver
    // a dependência Network
    @InjectMocks
    Communication communication;

    @Test
    public void injectMocksTest() {
        when(communication.send("message")).thenReturn(true);
        Assertions.assertEquals(true, communication.send("message"));
    }

}

Outra anotação interessante é a @Captor, utilizada em conjunto com a classe ArgumentCaptor, permite capturar os argumentos passados para um método que queremos inspecionar. A captura de parâmetros pode ser útil na construção de alguns tipos de testes, por exemplo:

public class EmailService {

    private DeliveryPlatform platform;

    public EmailService(DeliveryPlatform platform) {
        this.platform = platform;
    }

    public void send(String to, String subject, String body, boolean html) {
        Format format = Format.TEXT_ONLY;
        if (html) {
            format = Format.HTML;
        }
        Email email = new Email(to, subject, body);
        email.setFormat(format);
        platform.deliver(email);
    }

}
@ExtendWith(MockitoExtension.class)
public class EmailServiceUnitTest {

    @Mock
    DeliveryPlatform platform;

    @InjectMocks
    EmailService emailService;

    // 1 - utilizando a anotação @Captor em conjunto da classe ArgumentCaptor
    @Captor
    ArgumentCaptor<Email> emailCaptor;

    @Test
    public void whenDoesSupportHtml_expectHTMLEmailFormat() {
        String to = "info@baeldung.com";
        String subject = "Using ArgumentCaptor";
        String body = "Hey, let'use ArgumentCaptor";

        // 2 - invocando o método send da classe EmailServices
        // note que foi criado um objeto mock chamado platform
        emailService.send(to, subject, body, true);

        // 3 - capturando o argumento do método deliver do objeto platform
        verify(platform).deliver(emailCaptor.capture());

        // 4 - recuperando o último valor capturado por meio do método getValue
        Email value = emailCaptor.getValue();

        // 5 - verificando se o e-mail foi enviado no formato HTML
        assertEquals(Format.HTML, value.getFormat());
    }
}

Exemplos

Para se obter o código completo dos exemplos dos Mocks acima, por favor acesse:

git clone -b dev https://github.com/rodrigoprestesmachado/vvs
code vvs/exemplos/mockito/

Referências

Mockito framework site. Disponível em: https://site.mockito.org.

Rodrigo Prestes Machado
CC BY 4.0 DEED

Copyright © 2024 RPM Hub. Distributed by CC-BY-4.0 license.