Jedną z największych trudności dla osoby zaczynającej przygodę z testami jednostkowymi są:
Metody i klasy statyczne.
Niederministyczne lub/i niepowtarzalne zależności.
W tym artykule przedstawię jedną ze strategii tworzenia atrap dla tego typu zależności.
Przyjmijmy, że chcemy przetestować metodę GetAge klasy AgeCalculator która, jak sama nazwa wskazuje, zwraca wiek danej osoby. Przykładowa implementacja (źródło) wygląda następująco:
public class AgeCalculator
{
public int GetAge(DateTime dateOfBirth)
{
DateTime now = DateTime.Now;
int age = now.Year - dateOfBirth.Year;
if (now.Month < dateOfBirth.Month ||
(now.Month == dateOfBirth.Month && now.Day < dateOfBirth.Day))
{
age--;
}
return age;
}
}
Oczywiście, nie jest to algorytm idealny i sam nie użyłbym go u siebie ze względu na brak wsparcia dla:
Algorytm jest jednak prosty i spełnia nasze założenia, tj. wywołuje metodę DateTime.Now
, która nie jest powtarzalna.
Jednym z najprostszych rozwiązań jest oddelegowanie kontroli nad daną funkcjonalnością do osobnej klasy. W naszym przypadku będzie to oddelegowanie wywołania DateTime.Now
:
public interface IDateTimeProvider
{
DateTime GetDateTime();
}
public class DateTimeProvider : IDateTimeProvider
{
public DateTime GetDateTime() => DateTime.Now;
}
Zmieniony kalkulator wykorzystujący providera wygląda następująco:
public class AgeCalculator
{
private readonly IDateTimeProvider _dateTimeProvider;
public AgeCalculator(IDateTimeProvider dateTimeProvider)
{
if (dateTimeProvider == null) throw new ArgumentNullException(nameof(dateTimeProvider));
_dateTimeProvider = dateTimeProvider;
}
public int GetAge(DateTime dateOfBirth)
{
DateTime now = _dateTimeProvider.GetDateTime();
// ...
}
}
Strategia ta pozwala na podmianę implementacji providera na testowy:
[Test]
public void Test()
{
var currentDate = new DateTime(2015, 1, 1);
var dateTimeProvider = Mock.Of<IDateTimeProvider>(provider =>
provider.GetDateTime() == currentDate);
var ageCalculator = new AgeCalculator(dateTimeProvider);
var dateOfBirth = new DateTime(1990, 1, 1);
int age = ageCalculator.GetAge(dateOfBirth);
age.Should().Be(25);
}
Podczas testu domyślna strategia pobierania daty zostaje podmieniona na testową, której wartość można dowolnie dostosowywać do założeń naszego testu.
Alternatywnie, można stworzyć provider typu generycznego, czyli IProvider<T>
.
W taki sam sposób możemy opakować (ang. wrap) wywołania klas lub/i metod statycznych. Lepszy sufiks dla takiego wzorca będzie “Wrapper”.
Na deser zostawiam kilka pytań czytelnikowi:
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.
Część I: Testy jednostkowe – wstęp
Część II: Atrapy obiektów
Część III: Teoria