Był taki smutny czas, że struktura dziedziczenia w moich testach zdecydowanie przerastała stopniem skomplikowania dziedziczenie w testowanym kodzie. A bo jedna klasa bazowa dla testów umożliwiała na przykład kontakt z prawdziwą bazą. Inna – testy z bazą in-memory. Jeszcze inna – puszczanie requestów do systemu. I tak dalej.
Efekt był taki, że de facto wszystkie testy dziedziczyły ze wszystkiego. Fuj na maxa, z pryszczem jeszcze ohydnym.
Wszystko to uległo znaczącej poprawie, gdy nauczyłem się korzystać z interfejsu IUseFixture<T> w xunit. Celowo piszę “nauczyłem się korzystać” a nie “dowiedziałem się że istnieje”, ponieważ między tymi dwoma momentami minęło bez mała półtora roku.
Co to jest IUseFixture? Jest to prosty sposób na powiedzenie runnerowi: “testy w tej klasie potrzebują funkcjonalności oferowanej przez klasę T”.
Mógłbym dalej się “słownie” rozwodzić nad tym tematem, ale lepiej rozwieść się kodowo. Oto przykład “fixture” oferującej dostęp do nowej instancji “in memory database” z Simple.Data:
/// <summary> /// Fixture for tests using in-memory db. /// </summary> public class InMemoryDbFixture : IDisposable { public dynamic Db; public InMemoryDbFixture() { Reset(); } public void Dispose() { Database.StopUsingMockAdapter(); } public void Reset() { var inMemoryAdapter = new InMemoryAdapter(); configure_schema(inMemoryAdapter); Database.UseMockAdapter(inMemoryAdapter); Db = Database.Open(); } static void configure_schema(InMemoryAdapter adapter) { // keys / relations configuration } }
Jak to działa? Bardzo prosto: każda moja klasa z testami sprawdzającymi poprawność wykonywanych operacji wykorzystując bazę utworzoną w pamięci przez Simple.Data (co w kolejnej wersji będzie “full-blown database” a nie tylko słownikiem na słowniku jak jest teraz) posiada taki oto kod:
private InMemoryDbFixture _db; public void SetFixture(InMemoryDbFixture data) { _db = data; _db.Reset(); }
A dostęp do bazy mam przez “_db.Db”. Sweet.
A dlaczego zawsze w SetFixture mam wywołanie Reset()? Bo instancja InMemoryFixture jest tworzona tylko raz dla wszystkich testów z danej klasy. W pewnych okolicznościach ma to zastosowanie, a w innych: nie. To co robię jest faktycznym zasymulowaniem klasy bazowej posiadającej w swoim konstruktorze całą logikę inicjalizacji bazy. Z tą różnicą, że teraz mogę sobie dobierać dowolne “fixtures” z których moje testy korzystają. A tych “fixtures” mam parę.
Na koniec ciekawostka: aktualnie implementacja omawianego interfejsu wymusza stworzenie metody
public void SetFixture(T data)
, która to metoda weźmie parametr “data” i przypisze go prawdopodobnie do jakiegoś pola. A jej implementacja będzie identyczna w każdej klasie, która danej zależności potrzebuje. W v2.0 xunit ma się to zmienić: zależności testów będą przekazywane w konstruktorze. How cool is that?
Witam, dziękuję za wpis, jednak proponował bym wrzucić cały kod klasy testu. W tytule wpisu nazwa”IUseFixture”, więc szukam wykorzystania tego interfejsu w listingach i nigdzie nie ma. Dopiero zapytanie wujka googla pokazało o co tak naprawdę biega. Pisze to z punktu widzenia czytelnika, który nie korzysta na codzień z xunit.
Bart,
Faktycznie tego zabrakło. Teraz mieszać już nie będę bo posta pisałem ze 2 miesiące temu i musiałbym od nowa się wgryzać w kontekst, ale w przyszłości postaram się unikać takich sytuacji.
Kontynuując z twittera. Pierwsza myśl po przeczytaniu twojego wpisu była taka: ojoj, znowu niepotrzebnie odkryłem amerykę i w bólach doszedłem do podobnego rozwiązania dla NUnit. A wystarczyło się rozejrzeć… Druga myśl: przecież dokładnie takie API można zaimplementować na bazie NUnit i stąd machnąłem szybko: https://gist.github.com/orient-man/8039247 (z uproszczeniem, że cykl życia fikstury to 1 test – bo tego zwykle potrzebuję, BTW nie wiem jak zrobią “W v2.0 xunit ma się to zmienić: zależności testów będą przekazywane w konstruktorze” – przecież to zmieni semantykę, gdzie zawołasz Reset? No chyba, że dla każdego testu z klasy leci nowa instancja – nie znam xunit). Napisawszy powyższe doszedłem to tego, że to w sumie nic mi nie daje ;-). Moje obecne API do fikstur wygląda +/- tak:
class MyTests: TestsWithFixtures
…
[SetUp]
// nadklasa posprząta w TearDown
AddFixture(new InMemoryDatabaseFixture());
// tylko przykład, aby mieć > 1 fiksturę
AddFixture(new RedirectDebugToFile(“blabla.txt”));
var account = Examples.BasicClientAccount();
….
Takie API pozwala mi ustalić kolejność fikstur np.: gdyby druga wymagała już bazy danych (stąd moje pytanie na twitterze). Ponadto jest zwięźlejsze i IMHO czytelniejsze (nie zachodzi za kurtyną żadna magia). Co prawda faza SetUp łączy faktyczny setup z fazą “given” i to mi się nie podoba tj. co ma piernik (że jakiś mechanizm persystencji musi być) do wiatraka (że klient ma konto księgowe).
W każdym razie po raz kolejny upewniłeś mnie, że musze się pobawić xunit-em. Szczególnie, że na półce czeka cegła: http://www.amazon.com/xUnit-Test-Patterns-Refactoring-Code/dp/0131495054 :)
Mała wtopa, książka jest o xUnit dla Javy – zupełnie innym frameworku – dzięki @gutek ;)
IUseFixture | Maciej Aniserowicz o programowaniu…
Dziękujemy za dodanie artykułu – Trackback z dotnetomaniak.pl…