Prosty mock na prostym przykładzie

0

Czy mogłby ktoś pokazać, jak przetestować moja prostą klasę z dwiema zależnościami przy użyciu Mockito ? Wszystkie przykłady które znalazłam są pokazane na skomplikowanych dla mnie przykładach, nawet zmarnowałam pieniądze na kurs z udemy...

a więc chciałabym przetestować klasę Access, która ma jedną metodę:
(być może mój przykład się do tego nie nadaje, moja wiedza z Mockito jest równa 0)

public class Access {

    private Network network;
    private UserType userType;

    public Access(Network network, UserType userType) {
        this.network = network;
        this.userType = userType;
    }

    public boolean access() {
        return userType.getType() == 1 && network.getNetworkType().equals("one");
    }

}

pozostałe klasy:

public class UserType {

    public Integer type;

    public UserType(Integer type) {
        this.type = type;
    }

    public Integer getType() {
        return type;
    }
}
public class Network {

    private String networkType;

    public Network(String networkType) {
        this.networkType = networkType;
    }

    String getNetworkType() {
        return networkType;
    }
}
3

Nie widze tu nic co warto byłoby mockować. Te twoje zależności to jakieś data class a nie na jakieś zewnętrzne zalezności które musisz mockować. Możesz w teście po prostu przekazać normalne obiekty i tyle. No ale założmy na chwilę że to twoje Network to jest jakaś magiczna klasa która uderza jakimś dzikim protokołem na drugi koniec globu i chcesz sobie koniecznie zrobić tutaj unit test (znów trochę bez sensu bo logiki tam w zasadzie nie ma, no ale cóż). Robisz wtedy jakieś

Network networkMock = mock(Network.class);
when(networkMock.getNetworkType()).thenReturn("cośtam");

Access access = new Access(network, userType);
boolean result = access.access();

assert...

Ale podkreślam: w twoim przypadku ABSOLUTNIE tak nie rób! To ma sens tylko jeśli obiekt który mockujesz robi coś skomplikowanego i nie chcesz tego w tej chwili brać pod uwagę w testach. Np. nie chcesz stawiać jakiegoś soapowego webserwisu który odpowiadałby na requesty od jakiejś twojej zalezności (niemniej taki test też należałoby mieć!) bo w tej chwili chcesz zrobić sobie test samej "logiki", oderwany od reszty. Podobnie jeśli chcesz zasymulować jakąś dziwna sytuacje czy wyjątek, który trudno wygenerować na prawdziwym obiekcie.
Co więcej, nawet wtedy często lepiej napisać własną fejkową klasę implementujacą ten sam interfejs, bo pewnie podobne zachowanie chcesz "mockować" w wielu miejscach i taka klasa i tak się przyda.

Podsumowując: prosty mock na prostym przykładzie to jest oksymoron. Troche jakby prosić o zaprezentowanie jak sie pisze kod low-latency, z async-io i eventual consistency na przykładzie Hello World.

1

nawet zmarnowałam pieniądze na kurs z udemy...

No błagam...

Problem z mockami i Mockito jest taki, że o ile sama biblioteka jest banalna w użyciu, to trzeba nieco doświadczenia, żeby wiedzieć kiedy warto jej używać. Biblioteka jest stosowana podczas pisania i przeprowadzania testów jednostkowych - czyli testów skupiających się na możliwie odizolowanym uruchomieniu jednostki kodu (np. klasy). Typowy test wygląda (w podręczniku) z grubsza tak:

Sum sum = new Sum();
int result = sum.sum(2, 2);
assertEquals(4, result);
  • Tworzymy testowany obiekt (w tym przypadku nie ma on żadnych zależności)
  • wywołujemy jakąś metodę
  • sprawdzamy, czy efekt jest zgodny z tym jakiego oczekiwaliśmy od obiektu (wartość znana podczas kompilacji).

Problem z rzeczywistością polega na tym, że klasy, które coś robią, są zależne w sposób bezpośredni lub pośredni od wielu innych. Kod może też działać na innych komponentach systemu (bazy danych, kolejki, serwisy internetowe), które często są trudne, lub niemożliwe do odtworzenia. Mogą tez zwracać wartości nie znane w czasie kompilacji (np. jakiś serwer zwracający aktualną temperaturę powietrza).

Załóżmy, że masz do napisania program, który mierzy temperaturę w domu, sprawdza prognozę pogody i dostosowuje intensywność ogrzewania do tych parametrów. Raczej nie chcesz przygotowywać pełnej infrastruktury do przeprowadzenia takiego testu, będziesz chciała, żeby prognoza pogody była taka sama w każdym wywołaniu testu, podobnie temperatura wewnątrz, a heating.on() nie wywoływało żadnej rzeczywistej akcji.

Tworzysz więc mocki udające te niewygodne w użyciu, albo utworzeniu obiekty, ustawiasz jak konkretnie mają się zachowywać w twoim teście (mock() i when()) i ostatecznie weryfikujesz, czy logika zachowała się prawidłowo (verify()). W prawidłowo napisanym kodzie, można zrobić to w czystej Javie (np. implementując odpowiedni interface) Mockito po prostu ułatwia to zadanie w takich sytuacjach.

0
Shalom napisał(a):

when(networkMock.getNetworkType()).thenReturn("cośtam");

a ten mockito nie wprowadza trochę w błąd ? Próbowałam zdebugować i networkMock po tej metodzie wciąż posiadał networkType == null, a w rzeczywistości chyba chodzi o to, że networkType będzie miał wartość "cośtam" i faktycznie dalej zachowuje się jakby miał wartość "cośtam"

1

Przecież zamockowałaś tutaj ZACHOWANIE a nie stan. Mocki w ogóle zwykle robi sie na bazie interfejsu a nie klasy. Taki mock w ogóle nie musi (i zwykle nie ma) pól takich jak zwykła klasa, bo i po co? On ma mieć tylko zaprogramowane zachowanie! Ba, możesz nawet to zachowanie zaprogramować w dość złożony sposób, chocby tak ze pierwszym raz getNetworkType zwróci ci ala za drugim razem ola a za trzecim razem nulla.

1

@wioletta90:
Chyba wciąż nie rozumiesz czym jest mock, to od początku:

interface MissleLauncher{
  void destroyTheWorld();
}

class VeryComplicateAndDependatMissleLauncherImplementation implements MissleLauncher{
//constructor
  VeryComplicateAndDependatMissleLauncherImplementation(Communication com, Radar radar, List<Silo> silos){
    ///....
  }
  ....
}

class DoomManager{
  private MissleLauncher missleLauncher;
  private int confirmations = 0;

  public DoomManager(MissleLauncher missleLauncher){
    this.missleLauncher = missleLauncher;
  }

  public void confirm(){
    confirmations++;
  }

  public void doomsDay(){
    if(confirmations >= 3){
      missleLauncher.destroyTheWorld();
    }
  }
}

I teraz wypadałoby pokryć DoomManager pokryć testami, ale:

  • integracynie się nie da, bo chcemy żyć
  • klasa zależy od implementacji VeryComplicateAndDependatMissleLauncherImplementation, która wymaga dodatkowych skomplikowanych, trudnych do utworzenia, posiadających własne stany komponentów
  • zależy nam na przetestowaniu, czy metoda doomDay() została wywołana (jak to sprawdzić?)

Ręcznie tak:

class MissleLauncherMockImplementation : MissleLauncher{
    private boolean doom = false;
    public void destroyTheWorld(){
       doom = true;
    }
    public boolean isDoom(){
       return doom;
    }
}

i test:

@Test
doomTest(){
  MissleLauncher missleLauncher = new MissleLauncherMockImplementation();
  DoomManager doomManager = new DoomManager(missleLauncher);

  doomManager.confirm();
  doomManager.confirm();
  doomManager.confirm();

  doomManager.doomsDay();

  assertTrue(missleLauncher.isDoom());
}

I teraz wersja z Mockito - nie ma tu żadnych cudów, wszystko co to robi, to uproszczenie tworzenia mocka i jego weryfikacji:

@Test
doomTest(){
  MissleLauncher missleLauncher = mock(MissleLauncher.class);
  DoomManager doomManager = new DoomManager(missleLauncher);

  doomManager.confirm();
  doomManager.confirm();
  doomManager.confirm();

  doomManager.doomsDay();

  verify(missleLauncher).destroyTheWorld();
}

Wszystko co w tym przypadku zrobiło mockito, to "magicznie" utworzyło odpowiednik MissleLauncherMockImplementation + pozwoliło wygodnie sprawdzić, czy odpowiednia metoda tego obiektu została wywołana.

0

@piotrpo:

co klasa:

class VeryComplicateAndDependatMissleLauncherImplementation implements MissleLauncher{
//constructor
  VeryComplicateAndDependatMissleLauncherImplementation(Communication com, Radar radar, List<Silo> silos){
    ///....
  }
  ....
}

ma wspólnego z DoomManagerem ?

i ta linijka też dziwna dla mnie

MissleLauncher missleLauncher = mock(MissleLauncher.class);

Jakt o MissLauncher.class ? rozumiem że MissLauncher.class zawiera tylko interfejs MissLauncher ?

// jeszcze 3 pytanie, dlaczego DoomManager przyjmuje interfejs w konstruktorze, a nie po prostu go implementuje ?

1

DoomManager potrzebuje do działania klasy implementującej interface MissleLauncher i przyjmuje w konstruktorze zależność do tej klasy. Jedyną istniejącą w kodzie produkcyjnym implementacją tego interface'u jest VeryComplicateAndDependatMissleLauncherImplementation
DoomManager nie implementuje MissleLauncher, bo to jakieś tam ułomne realizowanie SRP - odpowiada za sprawdzenie, czy możemy zacząć zagładę, wiedza o silosach i przekazanie do nich informacji to już odpowiedzialność właśnie MissleLauncher. Im drobniej i ostrzej jesteś w stanie podzielić odpowiedzialności na klasy, tym lepiej. Klasa robiąca wszystko, implementująca wszystko co się da to z dużym prawdopodobieństwem źle zaprojektowana klasa. W tym konkretnym przypadku, byłaby to klasa nie pozwalająca się testować.

rozumiem że MissLauncher.class zawiera tylko interfejs MissLauncher ?

Interface to też typ. W Javie typ pobieramy właśnie przez wywołanie .class.

1 użytkowników online, w tym zalogowanych: 0, gości: 1