Mock, stub, spy, fake… i co jeszcze?!

Wszyscy ich używamy, ale czy wiesz czym różni się mock od spy, a dummy od fake’a i co to jest stub? Jeśli nie, to ten wpis jest właśnie dla Ciebie. Po przeczytaniu go przestaniesz nazywać wszystko mock-iem i wprowadzisz dozę poprawnego nazewnictwa do swoich dublerów w testach.

Zaślepka dla testów

W ostatnim wpisie rozmawialiśmy sobie o różnicach pomiędzy podstawowymi rodzajami testów. Porównaliśmy sobie testy jednostkowe, integracyjne oraz e2e i z tego wpisu powinieneś/aś już wiedzieć, że czasami zachodzi potrzpeba „zaślepienia” pewnych elementów naszego systemu na potrzeby testów. I dzisiaj, to właśnie o tych zaślepkach chciałbym Ci co nieco powiedzieć. Zanim jednak przejdziemy do właściwej treści, to wyjaśnijmy sobie najpierw dlaczego czasami próbujemy podmieniać fragmenty logiki naszego systemu, na potrzeby testów.

Clue dubler-ów

Wyobraźmy sobie, że chcemy jednostkowo przetestować klasę typu Repository, która może wyglądać np. tak:

class UserRepository {  
  constructor(
    private readonly userDAO: UserDAO,
    private readonly logger: LoggerInterface
  ) {}

  async findOneById(id: string): Promise<UserEntity> {
   const userData = await this.userDAO.findOneById(id);

   this.logger.log(`Data received from userDao:`, userData);

   return new UserEntity({
    ...userData,
    address: JSON.parse(userData.address)
   });
  }
}

Zależy nam teraz na sprawdzeniu tego, co zwróci metoda findOneById, ale nie chcemy, aby pod spodem nasz test otwierał faktyczne połączenie z bazą danych, z którą aplikacja normalnie się łączy. Cel możemy osiągnąć, dzięki zastosowaniu doubler-a. Pozwoli nam to między innymi na:

  • Przyspieszenie samego testu, ponieważ nie bazujemy na zew. kontekstach;
  • Zmniejszenie jego złożoności, bo nie łączymy się z bazą, tylko na potrzeby przetestowania logiki aplikacji;
  • Oszczędność execution time w procesach CI.

Jakie mamy możliwości?

Ze wstępu tego wpisu wiesz, że oprócz słowa „mock” w nomenklaturze dublerów istnieją także inne i warto pamiętać, że nie są to synonimy, a raczej mechanizmy, które stosujemy w konkretnych sytuacjach. Dla powyższego przykładu mogliśmy z powodzeniem stworzyć atrapę (dummy), zaślepkę (stub), podróbkę (fake), szpiega (spy), a także… imitację, czyli mock-a. Trzeba wiedzieć, kiedy warto, a kiedy nie warto korzystać z konkretnego doubler-a. Spójrzmy na poniższy obrazek pokazujący konkretne doubler-y i ich zachowania.

Podział dubler-ów

Atrapa – dummy

Atrapa stosowana jest tylko, jeżeli nie zależy nam na symulowaniu logiki, a jedynie na spełnieniu interfejsu. Najczęściej, atrapy tworzymy w celu wypełnienia zależności w klasie, którą testujemy, ale akurat ich sprawdzać nie musimy lub nie chcemy. Dummy moglibyśmy zastosować do zdublowania Logger-a z naszej klasy repozytorium. Taka atrapa mogłaby wówczas wyglądać następująco:

class LoggerDummy implements LoggerInterface {
  log(message: string, data: any) {}
}

// in test:
const loggerDummy = new LoggerDummy();
const repository = new UserRepository(daoMock, loggerDummy);

W teście możemy jawnie stworzyć instancję LoggerDummy i przekazać ją do konstruktora testowanego repozytorium. Wywołanie metody log nie spowoduje wtedy niczego i dzięki takiemu zabiegowi, nie zabrudzi nam się konsola z logami podczas uruchamiania testów.

Zaślepka – stub

Zaślepka, to mechanizm dublujący, który jest bardzo podobny do atrapy. Możemy nawet powiedzieć, że to jest atrapa, która zwraca sztywną wartość. Spełniamy tutaj interfejs, a także, na sztywno, zwracamy „okłamany” rezultat bazowej logiki. Dla UserDAO moglibyśmy stworzyć takiego stub-a:

class UserDAOStub implements UserDAOInterface {
  async findOneById(id: string): Promise<UserData> {
    return {
      firstName: 'John',
      address: JSON.strignify({ street: 'Blue Street' })
    };
  }
}

// in test:
const daoMock = new UserDAOStub();
const repository = new UserRepository(daoMock, loggerDummy);

Nie jest ważne, co przekażemy do metody, za każdym razem dostaniemy tę samą zaślepkę. Instancję takiego DAO możemy następnie przekazać do testowanego obiektu repository i oczekiwać konkretnego rezultatu, wiedząc, że jako wynik z DAO dostajemy zawsze ten sam stub.

Podróbka – fake

Kolejny dubler to podróbka. W tym przypadku, oprócz tego, że spełniony jest interfejs, to dodatkowo, zamiast zwracać cały czas tę samą wartość, to próbujemy implementować imitację faktycznej logiki. Popatrzmy na ten przykład:

class UserDAOFake implements UserDAOInterface {
  public users: Map<string, UserData> = new Map();

  async findOneById(id: string): Promise<UserData> {
    return this.users.get(id);
  }
}

Do mapy users możemy dodać dowolne rekordy UserData, które później, w teście, będziemy obsługiwać. W tym momencie metoda findOneById jest bardzo interaktywna. Dzięki takiemu fake’owi, w teście, możemy oczekiwać konkretnej zwrotki z repozytorium, dla konkretnego id użytkownika.

Szpieg – spy

Spy używany jest do śledzenia wywołań w naszej logice. Dzięki niemu możemy śledzić ile razy i z jakimi argumentami dana metoda się wywołała. Szpieg, to już trochę wyższa szkoła jazdy, ponieważ jego manualna implementacja może być bardziej skomplikowana. Zazwyczaj jednak, stosując dedykowane frameworki do pisania testów, otrzymujemy wraz z nimi mechanizmy do tworzenia szpiegów. Taki szpieg mógłby wyglądać następująco:

const findOneByIdDAOSpy = jest.spyOn(UserDao.prototype, 'findOneById');

test('something', async () => {
  const userData = { id: randomUUID(), name: 'John' };

  const result = await repo.findOneById(userData.id);

  expect(findOneByIdDAOSpy).toHaveBeenCalledTimes(1);
  expect(findOneByIdDAOSpy).toHaveBeenCalledWith(userData.id);
  expect(result).toEqual(new UserEntity(userData));
});

Skorzystałem z mechanizmów dostarczanych przez bibliotekę Jest, jednak jest to mało ważne, ponieważ w większości przypadków wygląda to podobnie. Ważniejszy jest fakt, że dzięki takiemu zabiegowi jesteśmy w stanie sprawdzić, że repozytorium wywołało jeden raz, konkretną metodę dao, z konkretnym argumentem.

Imitacja – mock

Na sam koniec, najbardziej znany typ dublera: mock. Jest to połączenie zarówno fake’a, jak i spy, a dodatkowo pozwala na dynamiczną implementację dublowanej logiki. Można więc rzec, iż mock jest bytem najbardziej skomplikowanym, który pozwala także na najwięcej. Z reguły, tak, jak w przypadku szpiega, odpowiednie mechanizmy dostarczane są wraz z framework-iem testowym. Spójrzmy na przykład mock-a:

const findOneByIdDAOMock = jest.spyOn(UserDao.prototype, 'findOneById');

test('something', async () => {
  const userData = { id: randomUUID(), name: 'John' };
  findOneByIdDAOMock.mockResolvedValueOnce(userData);

  const result = await repo.findOneById(userData.id);

  expect(findOneByIdDAOMock).toHaveBeenCalledTimes(1);
  expect(findOneByIdDAOMock).toHaveBeenCalledWith(userData.id);
  expect(result).toEqual(new UserEntity(userData));
});

W tym przypadku również użyłem biblioteki Jest. Jak możesz słusznie zauważyć, dodałem tylko dodatkową linię używającą mockResolvedValueOnce, względem przykładu dla spy. W moim przypadku, tak wygląda jeden z możliwych sposobów tworzenia mock-ów. Spełnienie interfejsu, możliwość szpiegowania i dynamicznego definiowania logiki.

Tylko mock?

Po przeczytaniu tego wpisu może nasunąć się pytanie czy warto stosować jakikolwiek innym mechanizm dublujący, skoro mamy mock-i? Wszystko na miarę potrzeb i wymagań. Jeżeli jest Ci łatwiej, możesz i warto w Twoim przypadku używać takich (ja to tak nazywam) „dzikich” mocker-ów, to śmiało. Pamiętaj tylko, że stub, spy, fake, bądź dummy, jeżeli występują, to powinny zostać odpowiednio nazwane w Twoim kodzie.

Z drugiej strony, mock-owanie wszystkiego, stosując mechanizmy biblioteki testującej, po prostu rozleniwia. Piszemy kod byle jak, bo z tyłu głowy wiemy, że zawsze będziemy mogli użyć magicznego mock-a. Personalnie, już od dłuższego czasu, jestem zwolennikiem pisania testowalnego kodu, gdzie wszystkie zależności testowanych bytów są łatwo wymienialne (wystarczy spełnić interfejs), a więc ich mock-owanie jest zbędne.

Podsumowanie

  • Dublery w testach służą do zaślepienia oryginalnej logiki.
  • Stosowanie mock-ów i innych dublerów pozwala na ograniczenie zasięgu naszych testów.
  • Pamiętaj, że nie wszystko jest mock-iem, a poprawne nazewnictwo dublerów jest potrzebne.
Autor wpisu

blog@orbisbit.com

Komentarze

Jedna odpowiedź do „Mock, stub, spy, fake… i co jeszcze?!”

  1. Awatar Tomek
    Tomek

    Ostatnie zdanie na temat pisania testowalnego kodu powinno moim zdaniem wybrzmieć dosadniej i już na początku wpisu. Poza tym, super treści, wielkie dzięki!

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *

Sprawdź również
  • CQRS – Command Query Responsibility Segregation

    Command Query Responsibility Segregation czyli CQRS. Jest to wzorzec projektowy, który rozdziela zadania odczytu i zapisu do osobnych modeli. Sprawdź ten wpis, aby dowiedzieć się kiedy i jak z niego skorzystać.

    Zobacz wpis

  • GRASP – kolejny zbiór zasad Clean Code do zapamiętania

    Pewnie większość z Was słyszała o zasadach SOLID. Są one bardzo rozpowszechnione i dosyć często stosowane, ale czy słyszeliście o GRASP? General Responsibility Assignment Software Patterns, to kolejna dawka zasad czystego kodu do zapamiętania.

    Zobacz wpis

  • Wzorzec strategia (strategy pattern)

    Jeżeli masz dość if-ologii w swoim kodzie, to konieczne sprawdź czym jest czynnościowy wzorzec projektowy strategia. Pozwala on mądrze obsługiwać różne scenariusze w procesie i jednocześnie być fancy pod względem zasad SOLID.

    Zobacz wpis