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:
Sprawa komplikuje się gdy mamy do czynienia z literaturą (książki i blogi) dotyczącą testów jednostkowych. Tutaj jest o tyle trudniej ponieważ:
W tym artykule przedstawię atrapy z xUnit Test Patterns napisane w Moq.
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 (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 (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:
Alternatywnie, fake‘i można definiować przy użyciu funkcjonalności Callback, którą posiada większość frameworków do tworzenia atrap.
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 (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
}Dummy
Stub
Fake
Mock
Spy
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.Strictdefault(T)Stub
Setup()MockBehavior.LooseMock
Verify()Spy
Verify() + TimesFake
Callback()Pytanie jakie może się nasunąć, to —
Czy pomimo, że…:
…czy warto pamiętać podział i definicje atrap?
Odpowiedź zależna jest od kontekstu. Moim zdaniem:
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.
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;
}
}Część I: Testy jednostkowe – wstęp
Część II: Atrapy obiektów
Część III: Teoria