Kurs TDD cz. 19 — Mock, stub, fake, spy, dummy

Nomenklatura w świecie TDD, a w szczególności ta dotycząca tworzenia atrap, jest źródłem wielu niejasności. Powodem takiego stanu jest fakt, że definicje różnią się w zależności od źródła, tj. książki, lub frameworka. W poprzednich częściach poznaliśmy trzy najbardziej popularne frameworki do tworzenia atrap dla .NET, dla których:

  • Nie ma podziału na rodzaje atrap.
  • Atrapa (pod różną nazwą – mock, fake, substitute), poza subtelnymi różnicami, ma tę samą definicję i służy do jednakowych celów.

podzial definicji_2

Sprawa komplikuje się gdy mamy do czynienia z literaturą (książki i blogi) dotyczącą testów jednostkowych. Tutaj jest o tyle trudniej ponieważ:

  • Istnieje podział atrap ze względu na cel i zachowanie. Najbardziej popularnym podziałem jest (w moim subiektywnym odczuciu) podział wprowadzony przez Gerarda Meszarosa w książce xUnit Test Patterns na mock, stub, fake, test spy, dummy:

podzial definicji

  • Definicje atrap są różne, a niekiedy nawet wykluczające się:

Terminology Cross-Reference

W tym artykule przedstawię atrapy z xUnit Test Patterns napisane w Moq.

Dummy

Dummy (z ang. imitacja, marionetka) jest najprostszą z atrap, gdyż… nie robi absolutnie nic! Jego zadaniem jest tylko i wyłącznie spełnienie założeń sygnatury.

Dla przykładu, przyjrzyjmy się klasie Customer:

public class Customer
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
 
    public Customer(string firstName, string lastName)
    {
        if (firstName == null) throw new ArgumentNullException(nameof(firstName));
        if (lastName == null) throw new ArgumentNullException(nameof(lastName));
 
        FirstName = firstName;
        LastName = lastName;
    }
}

Chcąc przetestować pierwszego null-guard, musimy przekazać wartość null jako firstName. Nie przejmujemy się czym jest lastName, gdyż kod związany z tą zmienną nie będzie wykonywany.

Przykład testu:

[Test]
public void DummyExample()
{
   string firstName = null;
   string lastName = It.IsAny<string>(); // Dummy

   Action act = () => new Customer(firstName, lastName);

   act.ShouldThrow<ArgumentNullException>();
}

Sygnatura metody została spełniona, nasz dummy object nie robi absolutnie nic, poza tym że udaje jakikolwiek obiekt typu string. Alternatywnym sposobem tworzenia obiektów dummy jest wykorzystanie atrap z zachowaniem Strict. Oznacza to, że stworzona atrapa wyrzuci wyjątek przy wywołaniu którejś z jej składowych. Dla przypomnienia, zachowanie atrap dla Moq opisane zostało w części 15. kursu: “Wstęp do Moq”. Równie dobrze, zamiast obiektu dummy, możemy też przesłać null lub default(T).

Stub

Stub (z ang. zalążek) to nieco bardziej zaawansowany dummy. Dodatkowo jednak, stub potrafi zwracać zdefiniowane przez nas wartości, o ile o nie poprosimy. Stub też nie wyrzuci błędu, jeśli nie zdefiniowaliśmy danego stanu (np. metody void są puste, a niezdefiniowane wartości wyjścia zwracają wartości domyślne).

Przykładowy test:

[Test]
public void StubExample()
{
    var customerValidator = new CustomerValidator();
    var customer = Mock.Of<ICustomer>(c => c.GetAge() == 21); // Stub
 
    bool validate = customerValidator.Validate(customer);
 
    validate.Should().BeTrue();
}

Fake

Fake (z ang. podróbka, falsyfikat) jest z kolei wariancją stuba i ma na celu symulowanie bardziej złożonych interakcji. Jeśli atrapa posiada złożone interakcje, której symulacja przy pomocy stuba jest niewykonalna (ograniczenia frameworka) lub bardzo złożona, możemy wykorzystać fake‘a. Jest to z reguły własnoręcznie napisana klasa, która posiada minimalną funkcjonalność aby spełnić założenia interakcji.

Przykład: Rozważmy klasę, która generuje raporty z interfejsu ICustomerRepository.

public class CustomerReportingService
{
    private readonly ICustomerRepository _customerRepository;
 
    public CustomerReportingService(ICustomerRepository customerRepository)
    {
        _customerRepository = customerRepository;
    }
 
    public string GenerateReport()
    {
        return string.Join("\n", _customerRepository.AllCustomers.Select(x =>
          x.FirstName + " " + x.LastName));
    }

Gdzie ICustomerRepository to:

public interface ICustomerRepository
{
   IReadOnlyList<ICustomer> AllCustomers { get; }
   bool Add(ICustomer customer);
}

Aby przetestować metodę GenerateReport klasy CustomerReportingService potrzebny nam jest dostęp do kolekcji AllCustomers interfejsu ICustomerRepository. Jako że kolekcja ta jest niezmienna (immutable), stworzenie stuba dla interfejsu ICustomerRepository wiązałoby się z dość skomplikowanym kodem (a w przypadku niektórych bibliotek – stworzenie takiego stuba jest niemożliwe). Można zatem stworzyć klasę (w projekcie testowym!), która będzie wykonywać minimum do wykonania testu. Takim minimum jest w tym przypadku dodanie elementu ICustomer do kolekcji. Ostatecznie, nasz fake wygląda następująco:

internal class FakeCustomerRepository : ICustomerRepository
{
   private readonly List<ICustomer> _customers = new List<ICustomer>();

   public IReadOnlyList<ICustomer> AllCustomers => _customers.AsReadOnly();

   public bool Add(ICustomer customer)
   {
       _customers.Add(customer);
       return true;
   }
}

Natomiast test wygląda następująco:

[Test]
public void FakeExample()
{
   var customerRepository = new FakeCustomerRepository(); // Fake

   customerRepository.Add(Mock.Of<ICustomer>(c =>
     c.FirstName == "John" && c.LastName == "Kowalski"));

   customerRepository.Add(Mock.Of<ICustomer>(c =>
     c.FirstName == "Steve" && c.LastName == "Jablonsky"));

   var customerReportingService = new CustomerReportingService(customerRepository);

   string report = customerReportingService.GenerateReport();

   report.Should().Be("John Kowalski\nSteve Jablonsky");
}

Podstawowe pytanie jakie może się nasunąć to…: Czemu by nie użyć obiektu klasy CustomerRepository, która jest już zaimplementowana:

public class CustomerRepository : ICustomerRepository
{
   private readonly List<ICustomer> _allCustomers;
   private readonly ICustomerValidator _customerValidator;

   public IReadOnlyList<ICustomer> AllCustomers => _allCustomers.AsReadOnly();

   public CustomerRepository(ICustomerValidator customerValidator)
   {
       _allCustomers = new List<ICustomer>();

       _customerValidator = customerValidator;
   }

   public bool Add(ICustomer customer)
   {
       if (_customerValidator.Validate(customer))
       {
           _allCustomers.Add(customer);
           return true;
       }

       return false;
   }
}

Fake ma tutaj istotną przewagę nad implementacją produkcyjną; przede wszystkim wykonuje minimum funkcjonalności do spełnienia założeń testu. Oznacza to, że nie potrzebujemy redundantnych interakcji, takich jak np. z ICustomerValidator oraz walidacja przy dodawaniu. Uważny czytelnik zauważył też, że istnieje niebezpieczeństwo związane z pisaniem fake‘ów. Co w przypadku jeśli logika biznesowa fake‘a będzie różna od implementacji w taki sposób, że test jednostkowy nie wykryje błędu? Trzeba pamiętać o dwóch kwestiach:

  • Testy jednostkowe wykonywane są w izolacji od innych klas. Błędy interakcji pomiędzy klasami powinny być wykryte przez inny rodzaj testów, np. integracyjny lub akceptacyjny.
  • Fake‘i pisze się w tych szczególnych przypadkach, gdzie ciężko lub niemożliwym jest stworzenie stuba oraz implementacja danego zachowania jest przejrzysta i prosta. Takim przykładem jest kolekcja i dodawanie do niej elementów.

Alternatywnie, fake‘i można definiować przy użyciu funkcjonalności Callback, którą posiada większość frameworków do tworzenia atrap.

Mock

Mock (z ang. imitacja, atrapa) potrafi weryfikować zachowanie obiektu testowanego. Jego celem jest sprawdzenie czy dana składowa została wykonana.

Przykład:

[Test]
public void MockExample()
{
    var customerValidator = new CustomerValidator();
    var customer = Mock.Of<ICustomer>(c => c.GetAge() == 21); // Mock
 
    customerValidator.Validate(customer);
 
    Mock.Get(customer).Verify(x => x.GetAge()); // Verification of Mock
}

Spy

Spy (z ang. szpieg) to mock z dodatkową funkcjonalnoscią. O ile mock rejestrował czy dana składowa została wywołana, to spy sprawdza dodatkowo ilość wywołań.

Przykład:

[Test]
public void SpyExample()
{
    var customerValidator = new CustomerValidator();
    var customer = Mock.Of<ICustomer>(c => c.GetAge() == 21); // Spy
 
    customerValidator.Validate(customer);
 
    Mock.Get(customer).Verify(x => x.GetAge(), Times.Once); // Verification of Spy
}

W skrócie

  • Dummy

    • Jego celem jest “zaspokojenie sygnatury”.
    • Nie są wykonywane na nim żadne interakcje.
  • Stub

    • Minimalna implementacja do interakcji między obiektami.
    • Metody void są puste.
    • Wartości zwracane przez składowe są domyślne dla danego typu lub zdefiniowane (“hard-coded”).
  • Fake

    • Zawiera implementację logiki, która imituje kod produkcyjny, ale w możliwie najprostszy sposób.
  • Mock

    • Weryfikuje zachowanie poprzez rejestrowanie czy dana interakcja została wykonana.
  • Spy

    • Weryfikuje zachowanie poprzez rejestrowanie ilości wykonanych interakcji.

W Moq nie ma rozróżnienia na poszczególne typy, wszystko jest mockiem; aby zasymulować działanie poszczególnych rodzajów atrap możemy wykorzystać poszczególne elementy frameworka (lub języka C#):

  • Dummy

    • It.IsAny<T>()
    • MockBehavior.Strict
    • alternatywnie: null, default(T)
  • Stub

    • Setup()
    • MockBehavior.Loose
  • Mock

    • Verify()
  • Spy

    • Verify() + Times
  • Fake

    • ręcznie stworzna klasa Fake
    • Callback()

Podsumowanie

Pytanie jakie może się nasunąć, to —

Czy pomimo, że…:

  • Definicje atrap w zależności od źródła są różne, a niekiedy wykluczające się, oraz
  • W niektórych frameworkach nie ma rozróżnienia na rodzaje atrap, to…

…czy warto pamiętać podział i definicje atrap?

Odpowiedź zależna jest od kontekstu. Moim zdaniem:

  • Warto pamiętać o podstawach teoretycznych testów jednostkowych i atrap z xUnit Test Patterns. Jest to wiedza historyczna, ale jednocześnie pozwala na zrozumienie co dokładnie chcemy testować, zwłaszcza w kontekście weryfikacji stanu vs. zachowania.
  • Niektóre frameworki posiadają rozróżnienie na rodzaje atrap (np. Rhino Mocks).
  • Umiejętność pisania ręcznych fake‘ów może okazać się bardzo przydatna. Pomimo, że z punktu widzenia technicznego fake‘i można napisać przy użyciu frameworka (choć nie każdego i nie zawsze), to czytelność i zarządzanie jest niekiedy lepsza na korzyść ręcznie napisanej klasy.
  • Rodzaje atrap i ich różnice pozwalają na zrozumienie funkcjonalności frameworków w których takiego nie ma rozróżnienia.

Przypominam, że kod źródłowy całego kursu TDD, jak i tego rozdziału jest dostępny na GitHubie: https://github.com/dariusz-wozniak/TddCourse.

Źródła

P.S. All-in-one

Wszystkie typy w jednym miejscu:

[TestFixture]
public class TestDoubles
{
    [Test]
    public void DummyExample()
    {
        string firstName = null;
        string lastName = It.IsAny<string>(); // Dummy
 
        Action act = () => new Customer(firstName, lastName);
 
        act.ShouldThrow<ArgumentNullException>();
    }
 
    [Test]
    public void StubExample()
    {
        var customerValidator = new CustomerValidator();
        var customer = Mock.Of<ICustomer>(c => c.GetAge() == 21); // Stub
 
        bool validate = customerValidator.Validate(customer);
 
        validate.Should().BeTrue();
    }
 
    [Test]
    public void MockExample()
    {
        var customerValidator = new CustomerValidator();
        var customer = Mock.Of<ICustomer>(c => c.GetAge() == 21); // Mock
 
        customerValidator.Validate(customer);
 
        Mock.Get(customer).Verify(x => x.GetAge()); // Verification of Mock
    }
 
    [Test]
    public void SpyExample()
    {
        var customerValidator = new CustomerValidator();
        var customer = Mock.Of<ICustomer>(c => c.GetAge() == 21); // Spy
 
        customerValidator.Validate(customer);
 
        Mock.Get(customer).Verify(x => x.GetAge(), Times.Once); // Verification of Spy
    }
 
    [Test]
    public void FakeExample()
    {
        var customerRepository = new FakeCustomerRepository(); // Fake

        customerRepository.Add(Mock.Of<ICustomer>(c =>
          c.FirstName == "John" && c.LastName == "Kowalski"));

        customerRepository.Add(Mock.Of<ICustomer>(c =>
          c.FirstName == "Steve" && c.LastName == "Jablonsky"));
 
        var customerReportingService = new CustomerReportingService(customerRepository);
 
        string report = customerReportingService.GenerateReport();
 
        report.Should().Be("John Kowalski\nSteve Jablonsky");
    }
}
 
internal class FakeCustomerRepository : ICustomerRepository
{
    private readonly List<ICustomer> _customers = new List<ICustomer>();
 
    public IReadOnlyList<ICustomer> AllCustomers => _customers.AsReadOnly();
 
    public bool Add(ICustomer customer)
    {
        _customers.Add(customer);
        return true;
    }
}
Opublikowano 26 marca 2016

Blog o programowaniu
Dariusz Woźniak · GitHub · LinkedIn · Twitter · Goodreads