Mockito - @Spy vs @Mock

103

Mockito - Eu entendo que um espião chama os métodos reais em um objeto, enquanto um mock chama métodos no objeto duplo. Além disso, espiões devem ser evitados, a menos que haja um cheiro de código. No entanto, como funcionam os espiões e quando devo realmente usá-los? Como eles são diferentes dos mocks?

Abhinav
fonte
2
possível duplicata de mockito mock vs. spy
rds
Possível duplicata de Mocking vs. Spying em frameworks de mocking
PenguinEngineer

Respostas:

94

Tecnicamente falando, tanto "mocks" quanto "espiões" são um tipo especial de "duplas de teste".

Mockito infelizmente está tornando a distinção estranha.

Um mock no mockito é um mock normal em outras estruturas de mocking (permite que você crie invocações; isto é, retornar valores específicos de chamadas de método).

Um espião no mockito é um mock parcial em outros frameworks de mocking (parte do objeto será simulado e parte usará invocações de métodos reais).

Crazyjavahacking
fonte
48

Ambos podem ser usados ​​para simular métodos ou campos. A diferença é que no mock, você está criando um mock ou um objeto falso completo, enquanto no spy, existe o objeto real e você apenas espia ou faz stub em métodos específicos dele.

Enquanto em objetos espiões, é claro, uma vez que é um método real, quando você não está fazendo o stub do método, ele chamará o comportamento do método real. Se você quiser alterar e simular o método, precisará fazer um stub.

Considere o exemplo abaixo como uma comparação.

import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.Spy;
import org.mockito.runners.MockitoJUnitRunner;
 
import java.util.ArrayList;
import java.util.List;
 
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.when;
 
@RunWith(MockitoJUnitRunner.class)
public class MockSpy {
 
    @Mock
    private List<String> mockList;
 
    @Spy
    private List<String> spyList = new ArrayList();
 
    @Test
    public void testMockList() {
        //by default, calling the methods of mock object will do nothing
        mockList.add("test");

        Mockito.verify(mockList).add("test");
        assertEquals(0, mockList.size());
        assertNull(mockList.get(0));
    }
 
    @Test
    public void testSpyList() {
        //spy object will call the real method when not stub
        spyList.add("test");

        Mockito.verify(spyList).add("test");
        assertEquals(1, spyList.size());
        assertEquals("test", spyList.get(0));
    }
 
    @Test
    public void testMockWithStub() {
        //try stubbing a method
        String expected = "Mock 100";
        when(mockList.get(100)).thenReturn(expected);
 
        assertEquals(expected, mockList.get(100));
    }
 
    @Test
    public void testSpyWithStub() {
        //stubbing a spy method will result the same as the mock object
        String expected = "Spy 100";
        //take note of using doReturn instead of when
        doReturn(expected).when(spyList).get(100);
 
        assertEquals(expected, spyList.get(100));
    }
}

Quando você deve usar mock ou spy? Se você quiser ficar seguro e evitar chamar serviços externos e só quiser testar a lógica dentro da unidade, use mock. Se você deseja chamar um serviço externo e executar uma chamada de dependência real, ou simplesmente dizer que deseja executar o programa como está e apenas criar um esboço de métodos específicos, use o spy. Então essa é a diferença entre spy e mock no mockito.

Vu Truong
fonte
Boa resposta, mas lançará verify () em um erro de simulação apenas e não executará os testes a menos que você inicialize suas listas no método @Before setUp () como aqui mockList = mock (ArrayList.class); spyList = spy (ArrayList.class); e remova a simulação e a anotação de espionagem sugeridas aqui. Eu testei e meus testes estão passando agora.
The_Martian 01 de
17

TL; versão DR,

Com o mock , ele cria uma instância de shell óssea para você.

List<String> mockList = Mockito.mock(ArrayList.class);

Com o spy você pode simular parcialmente em uma instância existente

List<String> spyList = Mockito.spy(new ArrayList<String>());

Caso de uso típico para Spy: a classe tem um construtor parametrizado, você deseja criar o objeto primeiro.

del bao
fonte
14

Eu criei um exemplo executável aqui https://www.surasint.com/mockito-with-spy/

Eu copio algumas delas aqui.

Se você tiver algo como 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 do espião porque pode apenas simular DepositMoneyService e WithdrawMoneyService.

Mas com algum código legado, 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 mudar para o primeiro código, mas a API é alterada. Se este método está sendo usado por muitos lugares, você deve alterar todos eles.

A alternativa é que você pode extrair a dependência desta forma:

    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 para 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
13

O melhor lugar para começar é provavelmente a documentação do mockito .

Em uma nota geral, o mockito mock permite que você crie stubs.

Você criaria um método stub se, por exemplo, esse método fizer uma operação cara. Digamos que ele obtém uma conexão com o banco de dados, recupera um valor do banco de dados e o retorna para o chamador. Obter a conexão do banco de dados pode levar 30 segundos, retardando a execução do teste até o ponto em que você provavelmente mudará o contexto (ou parará de executar o teste).

Se a lógica que você está testando não se importa com a conexão do banco de dados, você pode substituir esse método por um esboço que retorna um valor codificado.

O mockito spy permite que você verifique se um método chama outros métodos. Isso pode ser muito útil ao tentar colocar o código legado em teste.

É útil se você estiver testando um método que funciona por meio de efeitos colaterais, então você usaria um espião mockito. Isso delega chamadas para o objeto real e permite que você verifique a invocação do método, o número de vezes que é invocado etc.

Jaimie Whiteside
fonte
9

Em resumo:

@Spye @Mocksão muito usados ​​em testes de código, mas os desenvolvedores confundem os casos de quando usar um deles e, portanto, os desenvolvedores acabam usando @Mockpara estarem seguros.

  • Use @Mockquando quiser apenas testar a funcionalidade externamente, sem realmente chamar esse método.
  • Use @Spyquando quiser testar a funcionalidade externa + internamente com o próprio método que está sendo chamado.

Abaixo está o exemplo de onde tirei o cenário de Election20xx na América.

Os eleitores podem ser divididos de acordo com VotersOfBelow21e VotersOfABove21.

A pesquisa de saída ideal diz que Trump vai ganhar a eleição porque VotersOfBelow21e VotersOfABove21ambos vão votar em trunfo dizendo " Nós elegemos o presidente Trump "

Mas este não é o cenário real:

Os eleitores de ambos os grupos etários votaram em Trump porque não tinham outra escolha eficaz além do Sr. Trump.

Então, como você testa ??

public class VotersOfAbove21 {
public void weElected(String myVote){
  System.out.println("Voters of above 21 has no Choice Than Thrump in 20XX ");
}
}

public class VotersOfBelow21 {
  public void weElected(String myVote){
    System.out.println("Voters of below 21 has no Choice Than Thrump in 20XX");
  }
}

public class ElectionOfYear20XX {
  VotersOfAbove21 votersOfAbove21;
  VotersOfBelow21 votersOfBelow21;
  public boolean weElected(String WeElectedTrump){
    votersOfAbove21.weElected(WeElectedTrump);
    System.out.println("We elected President Trump ");

    votersOfBelow21.weElected(WeElectedTrump);
    System.out.println("We elected President Trump ");
    return true;
  }

}

Agora observe nas duas primeiras classes acima, ambas as pessoas da faixa etária dizem que não têm escolha melhor do que trunfo. O que significa explicitamente que votaram em Trump apenas porque não tinham escolha.

Agora, o ElectionOfYear20XX diz que Trump ganhou porque ambas as faixas etárias votaram nele de forma esmagadora.

Se fôssemos testar o ElectionOfYear20XXcom @Mock, talvez não consigamos descobrir a verdadeira razão pela qual Trump venceu, estaremos apenas testando a razão externa.

Se testarmos ElectionOfYear20XXcom @Spy, obteremos a verdadeira razão pela qual Trump ganhou com os resultados da pesquisa de saída externa, ou seja, internamente + externamente.


Nossa ELectionOfYear20XX_Testclasse:

@RunWith(MockitoJUnitRunner.class)
public class ELectionOfYear20XX_Test {

  @Mock
  VotersOfBelow21 votersOfBelow21;
  @Mock
  VotersOfAbove21 votersOfAbove21;
  @InjectMocks
  ElectionOfYear20XX electionOfYear20XX;
  @Test
  public void testElectionResults(){
    Assert.assertEquals(true,electionOfYear20XX.weElected("No Choice"));
  }

}

Isso deve produzir apenas os resultados do teste lógico, ou seja, verificação externa:

We elected President Trump 
We elected President Trump 

Testando @Spyexternamente e internamente com invocação de método real.

@RunWith(MockitoJUnitRunner.class)
public class ELectionOfYear20XX_Test {

  @Spy
  VotersOfBelow21 votersOfBelow21;
  @Spy
  VotersOfAbove21 votersOfAbove21;
  @InjectMocks
  ElectionOfYear20XX electionOfYear20XX;
  @Test
  public void testElectionResults(){
    Assert.assertEquals(true,electionOfYear20XX.weElected("No Choice"));
  }

}

Resultado:

Voters of above 21 has no Choice Than Thrump in 20XX 
We elected President Trump 
Voters of below 21 has no Choice Than Thrump in 20XX
We elected President Trump 
Vishwa Ratna
fonte
6

Gosto da simplicidade desta recomendação:

  • Se você quiser ficar seguro e evitar chamar serviços externos e só quiser testar a lógica dentro da unidade, use mock .
  • Se você deseja chamar um serviço externo e executar a chamada de dependências reais, ou simplesmente dizer, deseja executar o programa como está e apenas criar um esboço de métodos específicos, então use o spy .

Fonte: https://javapointers.com/tutorial/difference-between-spy-and-mock-in-mockito/

Uma diferença comum é:

  • Se você deseja fazer o stub diretamente do (s) método (s) de uma dependência, simule essa dependência.
  • Se você quiser criar um stub para os dados em uma dependência, de forma que todos os seus métodos retornem os valores de teste de que você precisa, então espie essa dependência.
leo9r
fonte
Observe que Spy e Mock são sempre destinados a dependências, e não ao sistema em teste.
leo9r