Zaczniemy od tego, co tak na prawdę robi AutoFixture? Według dokumentacji: pozwala nam w testach tworzyć anonimowe zmienne. Dzięki nim nie musimy ręcznie tworzyć obiektów, które nie są istotne w kontekście samego testu, ale są wymagane do skompilowania kodu.
Można go używać na kilka sposobów:
- deklarując odpowiednie zmienne w kodzie
- definiując odpowiednie zmienne w wywołaniu metody
Zajmijmy się najpierw tym pierwszym przypadkiem.
Podejście pierwsze – zmienne
Na samym początku można napisać prosty test:
[Fact]
public void IntroductoryTest()
{
// Arrange
Fixture fixture = new Fixture();
int expectedNumber = fixture.Create<int>();
TestClass sut = fixture.Create<TestClass>();
// Act
int result = sut.Echo(expectedNumber);
// Assert
Assert.Equal(expectedNumber, result);
}
W powyższym teście deklarujemy obiekt typu Fixture. Potem z wykorzystaniem go, deklarujemy anonimową zmienną expectedNumber o typie int, oraz zmienną sut, o typie TestClass. Jak widać, nie musimy nigdzie deklarować konkretnej wartości zmiennych
var expectedNumer = 123;
czy
var sut = new TestClass();
Co ważne – po wygenerowaniu przez AutoFixture obiektu, za każdym razem będzie on inny. Tyczy się to również jego właściwości.
Podejście drugie – atrybuty
Innym podejściem jest używanie atrybutów. Pozwala to oszczędzić trochę kodu:
[Theory]
[AutoData]
public void IntroductoryTest(int expectedNumber, TestClass sut)
{
// Act
int result = sut.Echo(expectedNumber);
// Assert
Assert.Equal(expectedNumber, result);
}
Przy takim podejściu nie mamy w ogóle części Arrange. Nie musimy tworzyć sztucznego obiektu Fixture, ani deklarować zmiennych. Wystarczy że użyjemy atrybutu AutoData i dodamy odpowiednie parametry metody. Całą resztę zrobi za nas AutoFixture.
Warto pamiętać, że obydwa atrybuty można umieścić w jednej linii:
[Theory, AutoData]
Według mnie jest jednak czytelniej, gdy mamy je zapisane osobno.
Wstrzykiwanie zależności i atrybut [Frozen]
Może okazać się jednak, że czasami potrzebujemy wstrzyknąć zależność. Np. możemy mieć klasę TestService, z której chcemy wywołać fizyczną metodę, ale może mieć ona zależność do ITimeService, którą chcemy już zmockować (żeby operowała na sztucznych danych dotyczących czasu).
public class TestService(ITimeService timeService)
{
\\ implementacja
}
Jak to napisać w teście?
[Theory]
[InlineAutoMoqData]
public void IntroductoryTest(
User expectedUser,
DateTime expectedModificationDate,
[Frozen] Mock timeService,
TestService sut)
{
// Arrange
timeService.Setup(m => m.GetUtcNow())
.Returns(expectedModificationDate);
// Act
await sut.UpdateUser(expectedUser);
// Assert
Assert.Equal(expectedModificationDate, expectedUser.ModificationDate);
timeService.Verify(s => s.GetUtcNow(), Times.Once);
}
Co za magia stała się w tle? Co naprawdę testujemy? Sprawdzamy, czy podczas wywołania metody UpdateUser() została wywołana metoda GetUtcNow() i w obiekcie User właściwość ModificationDate została ustawiona na wartość expectedModificationDate.
Dodatkowo sprawdzamy, czy ta metoda wywołała się tylko raz.
Co ważne, kolejność parametrów nie jest przypadkowa! Parametr z atrybutem Frozen musi poprzedzać parametr, do którego ma zostać wstrzyknięty. Widać to m.in. w tym prostym przykładzie na stringach.
Oczywiście AutoFixture ma o wiele szersze zastosowanie (np. generowanie losowego ciągu znaków o określonej długości). W tym poście opisałam tylko podstawy i konkretny przypadek, który wykorzystałam w mojej pracy.
Podoba Ci się to, co tworzę? Chcesz dostawać informacje o:
– wydarzeniach, które organizuję lub wspieram (np. konferencje, meetupy, webinary)
– inicjatywach, które organizuję lub wspieram (np. GeekWeekWro, DevAdventCalendar)
– moich prelekcjach, kursach i szkoleniach
– wyróżnionych artykułach z mojego bloga
0% SPAMu, 100% informacji! Krótko i na temat.