Autofixture

Dlaczego

Gdy testuje kod często / zawsze pojawia się potrzeba generowania jakichś danych, czasem mają one sens, czasem są zupełnie niepotrzebne z punktu widzenia testu, a jednocześnie wymagane przez kod.
Pojawiają się wtedy najczęściej zapisy “foo”, “bar”,”dupa”, “not-important”, “asdqweadasdf0923409” czy inne.
Zamiast takich wartości można mieć inne, jakie? Nie ważne! Co?! 😲

Gdy testujesz kod, to potrzebujesz zawsze sprawdzić, czy tekst to “jakiś_tekst” jest równy “jakiś_tekst”? Czy może po prostu jest taki jaki powinien być nie ważne, jaką będzie mieć wartość? (to samo tyczy się wartości liczbowych)

Skąd brać dane

Jarek, cwaniaku, to jak? Ano można przez pisanie tego inline w teście, co spowoduje, że testy osiągną rozmiary amerykańskie 😉 i będą duplikowane w każdy teście.
Można przez mother object pattern (nie ma sensownego linka, poszukajcie różnych opisów) – to taki wielki obiekt, który zajmuje się generowaniem takich danych.
Można przez: autofixture 👈😲👏

Tomek G.

Teraz popatrzcie wszyscy na Tomka G. i powiedzcie dzięki, to dzięki tobie. Bo to Tomek mnie to pokazał i zmusił do korzystania. Dzięki!

Jak się to robi

Co to ten autofixture? Otóż to taki ułożony i przemyślany mother object, który ułatwia generowanie danych, które są potrzebne dla programisty piszącego testy. Część danych jest w stanie wygenerować automatycznie, część rzeczy jest w stanie ułatwić.

Dygresja

Pokaże kod, który ma dwa style tworzenia, opisywania metod, stare i nowe, natomiast jak zawsze stylówa to kwestia gustu szefa projektu.👨‍🍳

Mięso, które będę testować:

Powyżej klasa, w konstruktorze sprawdzenie nulla (tutaj dlaczego: Jak wstrzykiwać zależności w azure functions )
Potem właściwa część kodu:
1. odczytanie elementu z kolejki
2. zaciągniecie z sieci na podstawie danych z obiektu
3. sprawdzenie, czy już mam podobne
4. jeśli nowy to wygenerowanie ścieżki
5. upload ściągniętych danych
6. 😘

Kod testu

No to teraz jak wyglądają testy do takiego kodu

Tłusto, ale mogło być tłuściej. Co tutaj się dzieje?
Korzystam z XUnit oraz AutoFixture.
AutoFixture działa także z NUnitem, ale AFAIK nie lubi się z mstestem – takie życie.
Każda metoda testowa jest oznaczona atrybutami [Theory] oraz [AutoData] – wole jak są w osobnych liniach i rozdzielone, ale warto zobaczyć, jak brzydko wyglądają razem.
Wyjątek na końcu, gdzie nie potrzebowałem generowania danych, wtedy [Fact] – nieważne w tym przypadku.

To właśnie [AutoData] generuje naszą fixture i inne parametry, które są w metodach, które to w dodatku dla mnie są nieważne (nie interesuje mnie ich konkretna wartość).

Arrange

Samo fixture zaprogramowane przeze mnie dostarcza / konfiguruje dla mnie bańkę otaczającą moją klasę, w sposób możliwie najbardziej opisowy:

await workerFixture
.SerializerReturnsValidObject()
.DownloaderReturnsEmptyString()
.DownloadsAlreadyExists()
.GetWorker()
.Run(string.Empty);


Lub:

var worker = workerFixture
.SerializerReturnsValidObject(channelGuid, url)
.DownloaderReturns(downloadContent)
.DownloadDoesNotExits()
.GenerateValidBlobUploadPath(uploadPath)
.GetWorker();

Czy wreszcie mój nowy (lepszy/czytelniejszy IMHO) zapis:

var sut = fixture
.WithSerializer_ReturnsValidObject(channelGuid, url)
.WithDownloader_Returns(downloadContent)
.WithDownloadsReader_ConfirmingExistanceOfDownload()
.GetWorker();

Tak nazwane metody opisują środowisko, w którym mój system_under_test (sut) będzie testowany, w tych kilku linijkach zamyka się moja część arrange.
Przekazuje parametry (ponownie, których aktualna wartość często dla mnie jest zupełnie bez znaczenia!) i kończę wywołaniem GetWorker()

Czasem jest to GetService(), GetWorker(), GetUploader(), GetXXXX(), a czasem GetSystemUnderTest() – choć skłaniam się ku dedykowanym get’om.

Act

Aktu nie będę opisywać, bo jaki 🐴 każdy widzi.

Assert

Pozostaje tylko część asertowania / asercji (odmień poprawnie sam), tutaj wyciągam mocka z fixture i w teście, podkreślam: w teście sprawdzam poprawność wykonania.
Fixture służy za źródło danych, konfiguracje środowiska zewnętrznego dla klasy którą testuje. Logika i założenie pozostaje w teście.

W asercji znajdziecie dwa podejścia
* fixture.PathGenerator
* fixture.MockPathGenerator
Tutaj preferuje drugi zapis, z prefixem, co poradzę, przeszedłem na szarpa z cplusa, to lubię długie zapisy. Tak, używam this’a.

Jak działają mock setup oraz verify nie będę tłumaczyć, bo jest to dostępne w moim kursie na udemy, tutaj link ze zniżka, jak wygaśnie to pinguj aby wygenerować na nowo.
Udemy: https://www.udemy.com/course/wprowadzenie-do-testowania-dla-programistow-net/?couponCode=ZBLOGA
Jeśli potrzeba czegoś więcej z testowania pisz śmiało, będzie pan zadowolony.

Właściwa treść posta

Jak wygląda implementacja fixture?

Praktycznie wszędzie zwracam this, aby mieć fluenta, wyjątek to GetWorker(), który kończy imprezę.
Mock tworze w konstruktorze, ale konfigurowane są już w dedykowanych metodach.
Cała reszta to ustawienie (setup) mocków, każda z metody zajmuje się tylko jednym mockiem

Zadanie domowe

  1. Sprawne oko może powiedzieć, że właściwości się powtarzają, tak – to specjalnie, aby pokazać dwa podejścia do nazewnictwa.
  2. Sprawne oko może powiedzieć, że SerializerReturnsValidObject można zoptymalizować parametrami.
  3. Sprawne oko może powiedzieć, że tworzenie obiektu z właściwościami można uprościć.
  4. Sprawne oko – co jeszcze?

TL;DR;

Nie generuj danych bez potrzeby, nie wymyślał wartości, które są do niczego niepotrzebne. Nie wymyślaj koła na nowo. Korzystaj z gotowych rozwiązań, weź na tapetę AutoFixture i pozwól, aby dane do testów zostały dostarczone, Ty skup się na czytelniejszych testach.

One thought on “Autofixture

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.