Qual é a diferença entre zombar e espiar ao usar o Mockito?

137

Qual seria um caso de uso para o uso de um espião Mockito?

Parece-me que todo caso de uso de espião pode ser tratado com uma farsa, usando callRealMethod.

Uma diferença que posso ver é que, se você deseja que a maioria das chamadas de método seja real, ela salva algumas linhas de código para usar uma simulação contra um espião. É isso ou estou perdendo a foto maior?

Victor Grazi
fonte

Respostas:

100

A resposta está na documentação :

Zombarias parciais reais (desde 1.8.0)

Finalmente, após muitos debates internos e discussões na lista de discussão, foi adicionado suporte parcial a Mockito. Anteriormente, considerávamos zombarias parciais como odores de código. No entanto, encontramos um caso de uso legítimo para zombarias parciais.

Antes do release 1.8, o spy () não produzia zombarias parciais reais e era confuso para alguns usuários. Leia mais sobre espionagem: aqui ou no javadoc para o método spy (Object).

callRealMethod()foi introduzido depois spy(), mas spy () foi deixado lá, é claro, para garantir a compatibilidade com versões anteriores.

Caso contrário, você está certo: todos os métodos de um espião são reais, a menos que sejam esquecidos. Todos os métodos de um mock são stubbed, a menos que callRealMethod()sejam chamados. Em geral, eu preferiria usar callRealMethod(), porque não me força a usar o doXxx().when()idioma em vez do idioma tradicional.when().thenXxx()

JB Nizet
fonte
O problema de preferir zombar do espião nesses casos é quando a classe usa um membro que não é injetado (mas inicializado localmente) e é usado posteriormente pelo método "real"; no mock, o membro será inicializado com seu valor Java padrão, o que pode causar um comportamento incorreto ou até uma NullPointerException. A maneira de passar isso é adicionar um método "init" e depois "realmente" chamá-lo, mas isso me parece um pouco exagerado.
Eyal Roth
No documento: "espiões devem ser usados ​​com cuidado e ocasionalmente, por exemplo, ao lidar com código legado". O espaço de teste da unidade sofre muitas maneiras de fazer a mesma coisa.
Gdbj # 22/17
89

Diferença entre um Spy e um Mock

Quando o Mockito cria uma simulação - ele o faz a partir da Classe de um tipo, não de uma instância real. A zombaria simplesmente cria uma instância simples da Classe, totalmente instrumentada para rastrear interações com ela. Por outro lado, o espião quebra uma instância existente. Ele ainda se comportará da mesma maneira que a instância normal - a única diferença é que ele também será instrumentado para rastrear todas as interações com ele.

No exemplo a seguir - criamos uma simulação da classe ArrayList:

@Test
public void whenCreateMock_thenCreated() {
    List mockedList = Mockito.mock(ArrayList.class);

    mockedList.add("one");
    Mockito.verify(mockedList).add("one");

    assertEquals(0, mockedList.size());
}

Como você pode ver - adicionar um elemento à lista zombada na verdade não adiciona nada - apenas chama o método sem nenhum outro efeito colateral. Um espião, por outro lado, se comportará de maneira diferente - na verdade, chamará a implementação real do método add e adicionará o elemento à lista subjacente:

@Test
public void whenCreateSpy_thenCreate() {
    List spyList = Mockito.spy(new ArrayList());
    spyList.add("one");
    Mockito.verify(spyList).add("one");

    assertEquals(1, spyList.size());
}

Aqui certamente podemos dizer que o método interno real do objeto foi chamado porque, quando você chama o método size (), obtém o tamanho como 1, mas esse método size () não é ridicularizado! Então, de onde eu venho? O método interno real size () é chamado como size () não é zombado (ou stubbed) e, portanto, podemos dizer que a entrada foi adicionada ao objeto real.

Fonte: http://www.baeldung.com/mockito-spy + notas próprias.

Saurabh Patil
fonte
1
Você não quer dizer que size () retorna 1?
black
No primeiro exemplo, por que mockedList.size()retornar 0se esse método também não foi excluído? Esse é apenas um valor padrão, dado o tipo de retorno do método?
Mike
@mike: mockedList.size()retorna um intvalor padrão de int0 em Java. Se você tentar executar assertEquals(0, mockedList.size());depois mockedList.clear();, o resultado permanecerá o mesmo.
realPK
2
Esta resposta está bem e simplesmente escrita e me ajudou a finalmente entender a diferença entre simulação e espião. Agradável.
PesaThe
38

Se houver um objeto com 8 métodos e você tiver um teste no qual deseja chamar 7 métodos reais e stub um método, você tem duas opções:

  1. Usando um mock, você teria que configurá-lo chamando 7 callRealMethod e stub one method
  2. Usando um, spyvocê deve configurá-lo stubbing um método

A documentação oficial sobre doCallRealMethodrecomenda o uso de um espião para zombarias parciais.

Veja também javadoc spy (Object) para descobrir mais sobre zombarias parciais. Mockito.spy () é uma maneira recomendada de criar simulações parciais. O motivo é que ele garante que métodos reais sejam chamados contra objetos construídos corretamente, porque você é responsável por construir o objeto passado para o método spy ().

user2412398
fonte
5

O espião pode ser útil quando você deseja criar testes de unidade para código legado .

Criei um exemplo executável aqui https://www.surasint.com/mockito-with-spy/ , copio alguns deles aqui.

Se você tiver algo parecido com este código:

public void transfer(  DepositMoneyService depositMoneyService, WithdrawMoneyService withdrawMoneyService, 
             double amount, String fromAccount, String toAccount){
    withdrawMoneyService.withdraw(fromAccount,amount);
    depositMoneyService.deposit(toAccount,amount);
}

Você pode não precisar de espionagem, porque pode apenas zombar de DepositMoneyService e WithdrawMoneyService.

Mas com alguns códigos herdados, a dependência está no código assim:

    public void transfer(String fromAccount, String toAccount, double amount){

        this.depositeMoneyService = new DepositMoneyService();
        this.withdrawMoneyService = new WithdrawMoneyService();

        withdrawMoneyService.withdraw(fromAccount,amount);
        depositeMoneyService.deposit(toAccount,amount);
    }

Sim, você pode alterar para o primeiro código, mas a API é alterada. Se esse método estiver sendo usado por muitos lugares, você precisará alterar todos eles.

Alternativa é que você pode extrair a dependência assim:

    public void transfer(String fromAccount, String toAccount, double amount){
        this.depositeMoneyService = proxyDepositMoneyServiceCreator();
        this.withdrawMoneyService = proxyWithdrawMoneyServiceCreator();

        withdrawMoneyService.withdraw(fromAccount,amount);
        depositeMoneyService.deposit(toAccount,amount);
    }
    DepositMoneyService proxyDepositMoneyServiceCreator() {
        return new DepositMoneyService();
    }

    WithdrawMoneyService proxyWithdrawMoneyServiceCreator() {
        return new WithdrawMoneyService();
    }

Então você pode usar o espião e injetar a dependência assim:

DepositMoneyService mockDepositMoneyService = mock(DepositMoneyService.class);
        WithdrawMoneyService mockWithdrawMoneyService = mock(WithdrawMoneyService.class);

    TransferMoneyService target = spy(new TransferMoneyService());

    doReturn(mockDepositMoneyService)
            .when(target).proxyDepositMoneyServiceCreator();

    doReturn(mockWithdrawMoneyService)
            .when(target).proxyWithdrawMoneyServiceCreator();

Mais detalhes no link acima.

Surasin Tancharoen
fonte
0

Mocké um objeto duplo simples. Este objeto tem as mesmas assinaturas de métodos, mas a realização está vazia e retorna o valor padrão - 0 e nulo

Spyé um objeto duplo clonado. O novo objeto é clonado com base em um objeto real, mas você tem a possibilidade de zombar dele

class A {

    String foo1() {
        foo2();
        return "RealString_1";
    }

    String foo2() {
        return "RealString_2";
    }

    void foo3() {
        foo4();
    }

    void foo4() {

    }
}
@Test
public void testMockA() {

    //given
    A mockA = Mockito.mock(A.class);
    Mockito.when(mockA.foo1()).thenReturn("MockedString");

    //when
    String result1 = mockA.foo1();
    String result2 = mockA.foo2();

    //then
    assertEquals("MockedString", result1);
    assertEquals(null, result2);

    //Case 2
    //when
    mockA.foo3();

    //then
    verify(mockA).foo3();
    verify(mockA, never()).foo4();
}

@Test
public void testSpyA() {
    //given
    A spyA = Mockito.spy(new A());

    Mockito.when(spyA.foo1()).thenReturn("MockedString");

    //when
    String result1 = spyA.foo1();
    String result2 = spyA.foo2();

    //then
    assertEquals("MockedString", result1);
    assertEquals("RealString_2", result2);

    //Case 2
    //when
    spyA.foo3();

    //then
    verify(spyA).foo3();
    verify(spyA).foo4();
}

[Teste tipos duplos]

yoAlex5
fonte