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.Strict
default(T)
Stub
Setup()
MockBehavior.Loose
Mock
Verify()
Spy
Verify()
+ Times
Fake
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