Notatka
Dostęp do tej strony wymaga autoryzacji. Może spróbować zalogować się lub zmienić katalogi.
Dostęp do tej strony wymaga autoryzacji. Możesz spróbować zmienić katalogi.
Jak wspomniano w sekcji Przegląd, podstawową decyzją, którą należy podjąć, jest to, czy Twoje testy będą obejmować produkcyjny system bazy danych — tak jak Twoja aplikacja — czy też będą uruchamiane względem zastępnika testowego, który zastępuje produkcyjny system bazy danych.
Testowanie rzeczywistego zasobu zewnętrznego — zamiast zastępowania go testem podwójnym — może obejmować następujące trudności:
- W wielu przypadkach po prostu nie jest możliwe ani praktyczne przetestowanie rzeczywistego zasobu zewnętrznego. Na przykład aplikacja może wchodzić w interakcje z niektórymi usługami, których nie można łatwo przetestować (ze względu na ograniczanie szybkości lub brak środowiska testowego).
- Nawet jeśli istnieje możliwość zaangażowania rzeczywistego zasobu zewnętrznego, może to być zbyt powolne: uruchomienie dużej ilości testów w usłudze w chmurze może spowodować, że testy będą trwać zbyt długo. Testowanie powinno być częścią codziennego przepływu pracy dewelopera, dlatego ważne jest, aby testy działały szybko.
- Wykonywanie testów względem zasobu zewnętrznego może obejmować problemy z izolacją, w przypadku których testy zakłócają wzajemnie działanie. Na przykład wiele testów uruchomionych równolegle z bazą danych może modyfikować dane i powodować niepowodzenie siebie na różne sposoby. Użycie testu dwukrotnie pozwala uniknąć tego, ponieważ każdy test jest uruchamiany względem własnego zasobu w pamięci i dlatego jest naturalnie odizolowany od innych testów.
Jednak testy, które przechodzą pod kątem testu dwukrotnie, nie gwarantują, że program działa w przypadku działania względem rzeczywistego zasobu zewnętrznego. Na przykład test bazy danych podwójnie może wykonywać porównania ciągów z uwzględnieniem wielkości liter, podczas gdy system produkcyjnej bazy danych wykonuje porównania bez uwzględniania wielkości liter. Takie problemy są ujawniane tylko wtedy, gdy testy są wykonywane względem rzeczywistej produkcyjnej bazy danych, co czyni te testy ważną częścią każdej strategii testowania.
Testowanie bazy danych może być łatwiejsze niż się wydaje
Ze względu na powyższe trudności z testowaniem na rzeczywistej bazie danych, deweloperzy są często zachęcani do korzystania z test doubles i powinni posiadać niezawodny zestaw testów, który mogą często uruchamiać na swoich maszynach; natomiast testy z wykorzystaniem bazy danych powinny być wykonywane znacznie rzadziej, a w wielu przypadkach zapewniają również znacznie mniejsze pokrycie. Zalecamy bardziej przemyślanie tego drugiego i sugerujemy, że bazy danych mogą być znacznie mniej dotknięte powyższymi problemami niż ludzie wydają się myśleć:
- Większość baz danych można obecnie łatwo zainstalować na komputerze dewelopera. Technologie oparte na kontenerach, takie jak Platforma Docker, mogą sprawić, że będzie to bardzo łatwe, a biblioteki, takie jak Testcontainers , mogą pomóc zautomatyzować cykl życia konteneryzowanych baz danych w testach. Technologie takie jak Obszary robocze usługi GitHub i Kontener deweloperski konfigurują całe środowisko programistyczne (w tym bazę danych). W przypadku korzystania z programu SQL Server można również przeprowadzić testy z bazą danych LocalDB w systemie Windows lub łatwo skonfigurować dockerowy obraz na systemie Linux.
- Testowanie w lokalnej bazie danych — przy rozsądnym zestawie danych testowych — jest zwykle bardzo szybkie: komunikacja jest całkowicie lokalna, a dane testowe są zwykle buforowane w pamięci po stronie bazy danych. EF Core zawiera ponad 30 000 testów z samym SQL Serverem; te testy kończą się niezawodnie w ciągu kilku minut, są uruchamiane w ramach ciągłej integracji przy każdym zatwierdzeniu i są bardzo często wykonywane lokalnie przez deweloperów. Niektórzy deweloperzy zwracają się do baz danych w pamięci (tzw. "symulowane") w przekonaniu, że jest to potrzebne do zwiększenia szybkości - to prawie nigdy nie jest naprawdę potrzebne.
- Izolacja jest rzeczywiście przeszkodą podczas uruchamiania testów względem rzeczywistej bazy danych, ponieważ testy mogą modyfikować dane i zakłócać wzajemnie. Istnieją jednak różne techniki zapewniania izolacji w scenariuszach testowania bazy danych; skoncentrujemy się na nich w Testowaniu przeciwko produkcyjnemu systemowi bazy danych.
Powyższe nie ma na celu oczerniania dublery testowe ani wnoszenia argumentów przeciwko ich używaniu. Po pierwsze, testy podwójne są niezbędne w niektórych scenariuszach, których nie można przetestować w przeciwnym razie, takich jak symulowanie awarii bazy danych. Jednak z naszego doświadczenia wynika, że użytkownicy często unikają testowania swojej bazy danych z powyższych powodów, wierząc, że jest ono powolne, trudne lub zawodne, choć wcale nie musi tak być. Testowanie pod kątem systemu produkcyjnej bazy danych ma na celu rozwiązanie tego problemu, udostępniając wskazówki i przykłady dotyczące szybkiego, izolowanego testowania bazy danych.
Różne typy dublery testowe
Test doubles to szeroki termin, który obejmuje bardzo różne podejścia. W tej sekcji omówiono niektóre typowe techniki obejmujące podwójne testy na potrzeby testowania aplikacji EF Core:
- Użyj narzędzia SQLite (w trybie w pamięci) jako fałszywej bazy danych, zastępując produkcyjny system bazy danych.
- Użyj dostawcy EF Core w pamięci jako testowej bazy danych w celu zastąpienia produkcyjnego systemu bazy danych.
- Zmockuj lub stubbing
DbContextiDbSet. - Wprowadź warstwę repozytorium między EF Core a kodem aplikacji oraz zamockuj lub zestubbuj tę warstwę.
Poniżej dowiemy się, co oznacza każda metoda, i porównamy ją z innymi. Zalecamy zapoznanie się z różnymi metodami, aby uzyskać pełną wiedzę na temat każdego z nich. Jeśli zdecydujesz się napisać testy, które nie obejmują produkcyjnego systemu bazy danych, warstwa repozytorium jest jedyną metodą umożliwiającą kompleksowe i niezawodne wyśmiewanie/pozorowanie warstwy danych. Jednak takie podejście ma znaczny koszt w zakresie implementacji i konserwacji.
SQLite jako fałszywe bazy danych
Jedną z możliwych metod testowania jest zamiana produkcyjnej bazy danych (e.g. SQL Server) na SQLite, efektywnie używając jej jako "fałszywej". Oprócz łatwości konfiguracji sqlite ma funkcję bazy danych w pamięci, która jest szczególnie przydatna do testowania: każdy test jest naturalnie izolowany we własnej bazie danych w pamięci i nie trzeba zarządzać rzeczywistymi plikami.
Jednak przed wykonaniem tej czynności ważne jest, aby zrozumieć, że w programie EF Core różni dostawcy baz danych zachowują się inaczej — program EF Core nie podejmuje próby abstrakcji każdego aspektu bazowego systemu bazy danych. Zasadniczo oznacza to, że testowanie pod kątem sqlite nie gwarantuje tych samych wyników co w przypadku programu SQL Server ani żadnej innej bazy danych. Oto kilka przykładów możliwych różnic behawioralnych:
- To samo zapytanie LINQ może zwracać różne wyniki dla różnych dostawców. Na przykład program SQL Server domyślnie porównuje ciągi bez uwzględniania wielkości liter, natomiast funkcja SQLite uwzględnia wielkość liter. Może to sprawić, że testy przejdą w SQLite, gdzie nie powiodą się w SQL Server (lub odwrotnie).
- Niektóre zapytania, które działają na serwerze SQL Server, po prostu nie są obsługiwane w języku SQLite, ponieważ dokładna obsługa języka SQL w tych dwóch bazach danych różni się.
- Jeśli zapytanie będzie używać metody specyficznej dla dostawcy, takiej jak program SQL Server
EF.Functions.DateDiffDay, to zapytanie zakończy się niepowodzeniem w programie SQLite i nie będzie można go przetestować. - Nieprzetworzona baza DANYCH SQL może działać lub może zakończyć się niepowodzeniem lub zwrócić różne wyniki, w zależności od tego, co jest wykonywane. Dialekty SQL są różne na wiele sposobów w różnych bazach danych.
W porównaniu z uruchamianiem testów na produkcyjnym systemie bazodanowym, stosunkowo łatwo jest rozpocząć pracę z bazą danych SQLite, co robi wielu użytkowników. Niestety powyższe ograniczenia zwykle stają się problematyczne podczas testowania aplikacji EF Core, nawet jeśli nie wydają się być na początku. W związku z tym zalecamy pisanie testów bezpośrednio z rzeczywistą bazą danych, lub jeśli użycie dublera testowego jest absolutną koniecznością, wziąć pod uwagę koszty wzorca repozytorium, jak opisano poniżej.
Aby uzyskać informacje na temat używania biblioteki SQLite do testowania, zobacz tę sekcję.
Pamięć operacyjna jako symulacja bazy danych
Alternatywą dla SQLite jest również platforma EF Core dostarczana z dostawcą w pamięci. Mimo że ten dostawca został pierwotnie zaprojektowany do obsługi wewnętrznych testów platformy EF Core, niektórzy deweloperzy używają go jako fałszywej bazy danych podczas testowania aplikacji EF Core. Jest to zdecydowanie odradzane: jako fałszywa baza danych, w pamięci borykają się z tymi samymi problemami co SQLite (zobacz wyżej), jednak dodatkowo posiadają następujące dodatkowe ograniczenia:
- Dostawca w pamięci zwykle obsługuje mniej typów zapytań niż dostawca SQLite, ponieważ nie jest to relacyjna baza danych. Więcej zapytań zakończy się niepowodzeniem lub będzie działać inaczej w porównaniu z produkcyjną bazą danych.
- Transakcje nie są obsługiwane.
- Nieprzetworzony program SQL jest całkowicie nieobsługiwany. Porównaj to z SQLite, gdzie można używać surowego SQL, pod warunkiem że działa on w taki sam sposób w SQLite i w bazie danych produkcyjnej.
- Dostawca pamięci nie został zoptymalizowany pod kątem wydajności i zazwyczaj będzie działać wolniej niż SQLite w trybie pamięci (lub nawet twój produkcyjny system bazy danych).
Podsumowując, in-memory ma wszystkie wady SQLite, a także kilka dodatkowych, i nie oferuje żadnych zalet w zamian. Jeśli szukasz prostej, fałszywej bazy danych w pamięci, użyj narzędzia SQLite zamiast dostawcy w pamięci; należy jednak rozważyć użycie wzorca repozytorium, jak opisano poniżej.
Aby uzyskać informacje na temat używania w pamięci do testowania, zobacz tę sekcję.
Mockowanie lub stubowanie DbContext i DbSet
Takie podejście zwykle używa ramy makietowej, aby utworzyć obiekty testowe DbContext i DbSet, oraz przeprowadza testy w oparciu o te obiekty testowe. Pozorowanie DbContext może być dobrym podejściem do testowania różnych funkcji niezwiązanych z zapytaniami, takich jak wywołania do Add lub SaveChanges(), co pozwala sprawdzić, czy twój kod je wywołał w scenariuszach zapisu.
Jednak prawidłowe mockowanie funkcjonalności zapytania nie jest możliwe, ponieważ zapytania są wyrażane za pośrednictwem operatorów LINQ, które są statycznymi wywołaniami metod rozszerzeń za pośrednictwem IQueryable. W rezultacie, gdy niektórzy ludzie mówią o "mockowaniu DbSet", co naprawdę mają na myśli, to stworzenie DbSet wspieranej przez kolekcję w pamięci, a następnie testowanie operatorów zapytań na tej kolekcji, tak jak w przypadku prostego IEnumerable. Zamiast imitacji, jest to w rzeczywistości pewna forma imitacji, w której kolekcja w pamięci zastępuje prawdziwą bazę danych.
Ponieważ tylko sam DbSet jest fałszywy, a zapytanie jest wykonywane w pamięci, takie podejście jest bardzo podobne do użycia dostawcy EF Core dla pamięci: obie techniki wykonują operatory zapytań na platformie .NET na kolekcji w pamięci. W związku z tym ta technika ma również te same wady: zapytania będą zachowywać się inaczej (np. w przypadku uwzględniania wielkości liter) lub po prostu zakończy się niepowodzeniem (np. z powodu metod specyficznych dla dostawcy), nieprzetworzone dane SQL nie będą działać, a transakcje będą w najlepszym razie ignorowane. W związku z tym ta technika powinna być zwykle unikana do testowania dowolnego kodu zapytania.
Wzorzec repozytorium
Powyższe podejścia próbowały zamienić dostawcę produkcyjnej bazy danych platformy EF Core na fałszywego dostawcę testowania lub utworzyć bazę danych wspieraną DbSet przez kolekcję w pamięci. Te techniki są podobne, ponieważ nadal oceniają zapytania LINQ programu — zarówno w SQLite, jak i w pamięci — i jest to ostatecznie źródło trudności opisanych powyżej: zapytanie przeznaczone do wykonania względem określonej produkcyjnej bazy danych nie można wykonać niezawodnie bez problemów gdzie indziej.
Aby uzyskać prawidłowy, niezawodny zastępczy obiekt testowy, rozważ wprowadzenie warstwy repozytorium, która pośredniczy między kodem aplikacji a platformą EF Core. Implementacja produkcyjna repozytorium zawiera rzeczywiste zapytania LINQ i wykonuje je za pośrednictwem platformy EF Core. Podczas testowania abstrakcja repozytorium jest bezpośrednio stublowana lub zamockowana bez konieczności wykonywania rzeczywistych zapytań LINQ, skutecznie usuwając EF Core ze stosu testowego i pozwalając testom skupić się jedynie na kodzie aplikacji.
Poniższy diagram porównuje podejście z fałszowaniem bazy danych (SQLite/in-memory) ze wzorcem repozytorium.
Ponieważ zapytania LINQ nie są już częścią testowania, możesz bezpośrednio dostarczyć wyniki zapytania do aplikacji. Innymi słowy, poprzednie podejścia pozwalają mniej więcej na odcięcie danych wejściowych zapytań (np. zastępowanie tabel programu SQL Server przy użyciu tych w pamięci), ale następnie wykonywanie rzeczywistych operatorów zapytań w pamięci. Natomiast wzorzec repozytorium umożliwia bezpośrednie zastępowanie danych wyjściowych zapytań, co pozwala na znacznie bardziej zaawansowane i ukierunkowane testowanie jednostkowe. Należy pamiętać, że aby to działało, repozytorium nie może uwidocznić żadnych metod zwracanych przez funkcję IQueryable, ponieważ te metody po raz kolejny nie mogą zostać wycofane; Zamiast tego należy zwrócić wartość IEnumerable.
Jednak ponieważ wzorzec repozytorium wymaga hermetyzacji każdego testowalnego zapytania LINQ w metodzie zwracającej IEnumerable, dodaje kolejną warstwę architektoniczną do aplikacji i może wiązać się ze znacznymi kosztami implementacji i konserwacji. Ten koszt nie należy lekceważyć podczas wyboru sposobu testowania aplikacji, zwłaszcza że testy z wykorzystaniem rzeczywistej bazy danych będą nadal potrzebne w przypadku zapytań wykonywanych z repozytorium.
Warto zauważyć, że repozytoria mają zalety poza testowaniem. Zapewniają one skoncentrowanie całego kodu dostępu do danych w jednym miejscu, a nie rozpowszechnianie się wokół aplikacji, a jeśli aplikacja musi obsługiwać więcej niż jedną bazę danych, abstrakcja repozytorium może być bardzo przydatna do dostosowywania zapytań między dostawcami.
Aby zapoznać się z przykładem przedstawiającym testowanie za pomocą repozytorium, zobacz tę sekcję.
Ogólne porównanie
Poniższa tabela zawiera szybki, porównawczy widok różnych technik testowania i pokazuje, które funkcje można przetestować w ramach tego podejścia:
| Funkcja | W pamięci | SqLite w pamięci | Mock DbContext (symulowany obiekt DbContext) | Wzorzec repozytorium | Testowanie względem bazy danych |
|---|---|---|---|---|---|
| Przetestuj podwójny typ | Fałszywy | Fałszywy | Fałszywy | Mock/stub | Liczby rzeczywiste, bez podwójnej precyzji |
| Nieprzetworzone dane SQL? | Nie. | Zależy | Nie. | Tak | Tak |
| Transakcji? | Nie (ignorowane) | Tak | Tak | Tak | Tak |
| Tłumaczenia specyficzne dla dostawcy? | Nie. | Nie. | Nie. | Tak | Tak |
| Precyzyjne zachowanie zapytania? | Zależy | Zależy | Zależy | Tak | Tak |
| Czy można używać LINQ w dowolnym miejscu w aplikacji? | Tak | Tak | Tak | Nie* | Tak |
* Wszystkie testowalne zapytania LINQ bazy danych muszą być zamknięte w metodach zwracających IEnumerable repozytoriów, aby można je było zamockować/udawać.
Podsumowanie
- Zalecamy, aby deweloperzy mieli dobry zakres testowy aplikacji działającej względem rzeczywistego produkcyjnego systemu bazy danych. Zapewnia to pewność, że aplikacja rzeczywiście działa w środowisku produkcyjnym, a przy odpowiednim projektowaniu testy mogą działać niezawodnie i szybko. Ponieważ te testy są wymagane w każdym przypadku, dobrym pomysłem jest rozpocząć od nich, a w razie potrzeby później dodać testy z użyciem substytutów testowych, zgodnie z potrzebami.
- Jeśli zdecydujesz się użyć dubla testowego, zalecamy zaimplementowanie wzorca repozytorium, który umożliwia tworzenie atrap lub symulacji warstwy dostępu do danych nad warstwą dostępu do danych w EF Core, zamiast używania atrapy dostawcy EF Core (Sqlite/in-memory) lub symulując
DbSet. - Jeśli z jakiegoś powodu wzorzec repozytorium nie jest realną opcją, rozważ użycie bazy danych SQLite w pamięci.
- Unikaj dostawcy pamięci na potrzeby testowania — jest to odradzane i obsługiwane tylko w przypadku starszych aplikacji.
- Unikaj mockowania
DbSetna potrzeby wykonywania zapytań.