Mockito - @Spy vs @Mock

103

Mockito - rozumiem, że szpieg wywołuje prawdziwe metody na obiekcie, podczas gdy makieta wywołuje metody na podwójnym obiekcie. Należy również unikać szpiegów, chyba że wyczuwa się zapach kodu. Jednak jak działają szpiedzy i kiedy właściwie należy ich używać? Czym się różnią od kpiny?

Abhinav
źródło
2
możliwy duplikat mockito mock vs. spy
rds
Możliwy duplikat kpiny kontra szpiegowanie w kpiarskich ramach
PenguinEngineer

Odpowiedzi:

94

Z technicznego punktu widzenia zarówno „kpiny”, jak i „szpiedzy” to szczególny rodzaj „testowych dubletów”.

Mockito niestety czyni to rozróżnienie dziwnym.

Mock w mockito jest normalnym mockiem w innych frameworkach do mockowania (pozwala na zastępowanie wywołań; to znaczy zwracanie określonych wartości z wywołań metod).

Szpieg w mockito jest częściowym mockiem w innych frameworach mockujących (część obiektu będzie mockowana, a część będzie używać rzeczywistych wywołań metod).

Crazyjavahacking
źródło
48

Obie mogą służyć do kpiny z metod lub pól. Różnica polega na tym, że w makiecie tworzysz kompletną próbę lub fałszywy obiekt podczas szpiegowania, istnieje prawdziwy obiekt i po prostu szpiegujesz lub odrzucasz określone metody.

Oczywiście w przypadku obiektów szpiegowskich, ponieważ jest to prawdziwa metoda, kiedy nie blokujesz metody, wywoła ona prawdziwe zachowanie metody. Jeśli chcesz zmienić i kpić z metody, musisz ją zablokować.

Rozważ poniższy przykład jako porównanie.

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

Kiedy powinieneś używać kpiny lub szpiega? Jeśli chcesz być bezpieczny i unikać wywoływania usług zewnętrznych i po prostu chcesz przetestować logikę wewnątrz urządzenia, użyj makiety. Jeśli chcesz zadzwonić do usługi zewnętrznej i wykonać wywołanie rzeczywistej zależności, lub po prostu powiedzieć, że chcesz uruchomić program tak, jak jest i po prostu usunąć określone metody, użyj szpiega. Taka jest różnica między szpiegiem a kpiną w mockito.

Vu Truong
źródło
Dobra odpowiedź, ale wyrzuci ona weryfikację () tylko na fałszywy błąd i nie uruchomi testów, chyba że zainicjujesz swoje listy metodą @Before setUp (), tak jak tutaj mockList = mock (ArrayList.class); spyList = spy (ArrayList.class); i usuń fałszywą i sugerowaną tutaj adnotację szpiegowską. Przetestowałem to i teraz moje testy przechodzą.
The_Martian
17

Wersja TL; DR,

Za pomocą mock tworzy dla Ciebie instancję typu bare-bone shell.

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

Dzięki szpiegowi możesz częściowo kpić z istniejącej instancji

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

Typowy przypadek użycia dla Spy: klasa ma sparametryzowany konstruktor, najpierw chcesz utworzyć obiekt.

del bao
źródło
14

Utworzyłem tutaj działający przykład https://www.surasint.com/mockito-with-spy/

Część z nich tu skopiuję.

Jeśli masz coś takiego jak ten kod:

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

Możesz nie potrzebować szpiega, ponieważ możesz po prostu udawać usługę DepositMoneyService i WithdrawMoneyService.

Ale w przypadku jakiegoś starszego kodu zależność jest w kodzie w następujący sposób:

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

Tak, możesz przejść do pierwszego kodu, ale wtedy API zostanie zmienione. Jeśli ta metoda jest używana w wielu miejscach, musisz zmienić je wszystkie.

Alternatywą jest to, że możesz wyodrębnić zależność w następujący sposób:

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

Następnie możesz użyć szpiega i wstrzyknąć zależność w następujący sposób:

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

Więcej szczegółów w powyższym linku.

Surasin Tancharoen
źródło
13

Najlepszym miejscem na rozpoczęcie jest prawdopodobnie dokumentacja dotycząca mockito .

Ogólnie rzecz biorąc, mockito mock pozwala na tworzenie stubów.

Utworzyłbyś metodę pośredniczącą, jeśli na przykład ta metoda wykonuje kosztowną operację. Powiedzmy, że uzyskuje połączenie z bazą danych, pobiera wartość z bazy danych i zwraca ją do dzwoniącego. Uzyskanie połączenia z bazą danych może zająć 30 sekund, spowalniając wykonanie testu do momentu, w którym prawdopodobnie przełączysz kontekst (lub przestaniesz wykonywać test).

Jeśli testowana logika nie dba o połączenie z bazą danych, możesz zastąpić tę metodę kodem pośredniczącym, który zwraca wartość zakodowaną na stałe.

Szpieg mockito pozwala sprawdzić, czy metoda wywołuje inne metody. Może to być bardzo przydatne podczas próby pobrania starszego kodu w trakcie testu.

Jest to przydatne, jeśli testujesz metodę, która działa poprzez skutki uboczne, wtedy użyjesz szpiega mockito. To deleguje wywołania do rzeczywistego obiektu i pozwala zweryfikować wywołanie metody, liczbę wywołań itp.

Jaimie Whiteside
źródło
9

W skrócie:

@Spyi @Mocksą często używane w testowaniu kodu, ale programiści mylą się w przypadkach, gdy używają jednego z nich, a zatem programiści używają go, @Mockaby być bezpiecznym.

  • Użyj, @Mockjeśli chcesz po prostu przetestować funkcjonalność zewnętrznie, bez wywoływania tej metody.
  • Użyj, @Spygdy chcesz przetestować funkcjonalność zewnętrznie + wewnętrznie, przy użyciu samej wywoływanej metody.

Poniżej znajduje się przykład, w którym wziąłem scenariusz Election20xx w Ameryce.

Głosujących można podzielić według VotersOfBelow21i VotersOfABove21.

Idealna exit poll mówi, że Trump wygra wybory bo VotersOfBelow21i VotersOfABove21jak będzie głosować na Trump mówiąc: „ Mamy wybrany prezydent Trump

Ale to nie jest prawdziwy scenariusz:

Wyborcy z obu grup wiekowych głosowali na Trumpa, ponieważ nie mieli innego skutecznego wyboru niż Trump.

Jak więc to przetestować?

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

}

Teraz uwaga w powyższych dwóch pierwszych klasach, obie grupy wiekowe mówią, że nie mają lepszego wyboru niż atut. Co wyraźnie oznacza, że ​​głosowali na Trumpa tylko dlatego, że nie mieli wyboru.

Teraz ElectionOfYear20XX mówi, że Trump wygrał, ponieważ obie grupy wiekowe głosowały na niego w przeważającej mierze.

Gdybyśmy mieli przetestować ElectionOfYear20XX pomocą @Mock, moglibyśmy nie być w stanie znaleźć prawdziwego powodu, dla którego Trump wygrał, po prostu przetestujemy powód zewnętrzny.

Jeśli przetestujemy ElectionOfYear20XXz @Spy, otrzymamy prawdziwy powód, dla którego Trump wygrał z wynikami zewnętrznego sondażu, tj. Wewnętrznie + zewnętrznie.


Nasza ELectionOfYear20XX_Testklasa:

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

}

Powinno to wyprowadzić tylko wyniki testów logicznych, tj. Sprawdzenie zewnętrzne:

We elected President Trump 
We elected President Trump 

Testowanie zarówno @Spyzewnętrznie, jak i wewnętrznie z rzeczywistym wywołaniem metody.

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

}

Wynik:

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
źródło
6

Podoba mi się prostota tej rekomendacji:

  • Jeśli chcesz być bezpieczny i unikać wywoływania usług zewnętrznych i po prostu chcesz przetestować logikę wewnątrz urządzenia, użyj makiety .
  • Jeśli chcesz zadzwonić do usługi zewnętrznej i wykonać wywołanie rzeczywistych zależności, lub po prostu powiedzieć, że chcesz uruchomić program tak, jak jest i po prostu usunąć określone metody, użyj szpiega .

Źródło: https://javapointers.com/tutorial/difference-between-spy-and-mock-in-mockito/

Typowa różnica to:

  • Jeśli chcesz bezpośrednio odgiąć metodę (y) zależności, następnie Mock tę zależność.
  • Jeśli chcesz zablokować dane w zależności, tak aby wszystkie jej metody zwracały potrzebne wartości testowe, przeszukaj tę zależność.
leo9r
źródło
Zauważ, że Szpieg i Mock są zawsze przeznaczone do zależności, a nie do testowanego systemu.
leo9r