Wybieranie strategii testowania

Jak wspomniano w sekcji Przegląd, podstawową decyzją, którą należy podjąć, jest to, czy testy będą obejmować system produkcyjnej bazy danych — podobnie jak w przypadku aplikacji — czy testy będą uruchamiane względem testu dwukrotnie, co 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:

  1. 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).
  2. 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.
  3. 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 w rzeczywistej bazie danych deweloperzy są często zachęcani do korzystania z podwajań testowych i mają niezawodny zestaw testów, który mogą często uruchamiać na swoich maszynach; natomiast testy z udziałem bazy danych powinny być wykonywane znacznie rzadziej, a w wielu przypadkach również zapewniają 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ć:

  1. Większość baz danych można obecnie łatwo zainstalować na komputerze dewelopera. Technologie oparte na kontenerach, takie jak Platforma Docker, mogą być bardzo łatwe, a 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ż przetestować bazę danych LocalDB w systemie Windows lub łatwo skonfigurować obraz platformy Docker w systemie Linux.
  2. 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. Sam program EF Core zawiera ponad 30 000 testów względem samego programu SQL Server; są one niezawodne w ciągu kilku minut, wykonywane w ciągłej integracji w każdym zatwierdzeniu i są bardzo często wykonywane przez deweloperów lokalnie. Niektórzy deweloperzy zwracają się do bazy danych w pamięci ("fałszywe") w przekonaniu, że jest to potrzebne do szybkości - to prawie nigdy nie jest tak naprawdę.
  3. 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 tych tematach w temacie Testowanie pod kątem produkcyjnego systemu bazy danych).

Powyższe nie jest przeznaczone do dysparowania testów podwaja lub argumentować 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 w naszym doświadczeniu użytkownicy często niechętnie od testowania bazy danych z powyższych powodów, wierząc, że jest to powolne, trudne lub zawodne, gdy niekoniecznie tak jest. 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 podwajań testów

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:

  1. Użyj narzędzia SQLite (w trybie w pamięci) jako fałszywej bazy danych, zastępując produkcyjny system bazy danych.
  2. Użyj dostawcy ef Core w pamięci jako fałszywej bazy danych, zastępując produkcyjny system bazy danych.
  3. Makiety lub wycinków DbContext i DbSet.
  4. Wprowadzenie warstwy repozytorium między platformą EF Core i kodem aplikacji oraz pozorowanie lub wycinkę tej warstwy.

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 (np. programu 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 zostaną pomyślnie wykonane względem programu SQLite, w którym zakończy się niepowodzeniem względem programu 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 względem produkcyjnego systemu bazy danych, stosunkowo łatwo jest rozpocząć pracę z bazą danych SQLite i tak 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 napisanie testów względem rzeczywistej bazy danych lub użycie testu podwójnej jest absolutną koniecznością, dołączając koszt wzorca repozytorium, jak opisano poniżej.

Aby uzyskać informacje na temat używania biblioteki SQLite do testowania, zobacz tę sekcję.

W pamięci jako fałszywe 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 zniechęcone: ponieważ fałszywe bazy danych, w pamięci występują takie same problemy jak SQLite (patrz powyżej), ale ponadto mają 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ć nieprzetworzonego języka SQL, o ile baza danych SQL działa w taki sam sposób jak sqlite i produkcyjna baza danych.
  • Dostawca w pamięci nie został zoptymalizowany pod kątem wydajności i zazwyczaj działa wolniej niż SQLite w trybie w pamięci (a nawet w produkcyjnym systemie bazy danych).

Podsumowując, w pamięci wszystkie wady SQLite, a także kilka innych - i nie oferuje żadnych korzyści 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ę.

Pozorowanie lub stubbing DbContext i DbSet

Takie podejście zwykle używa makiety struktury, aby utworzyć test podwojenie DbContext wartości i DbSetoraz testy pod kątem tych podwajań. Pozorowanie DbContext może być dobrym podejściem do testowania różnych funkcji niezwiązanych z zapytaniami , takich jak wywołania lub AddSaveChanges(), co pozwala sprawdzić, czy kod został wywołany w scenariuszach zapisu.

Jednak prawidłowe wyśmiewanie DbSetfunkcji zapytania nie jest możliwe, ponieważ zapytania są wyrażane za pośrednictwem operatorów LINQ, które są statycznych wywołań metod rozszerzeń za pośrednictwem IQueryablemetody . W rezultacie, gdy niektórzy ludzie mówią o "kpieniu DbSet", co naprawdę oznaczają, jest to, że tworzą kopię zapasową DbSet kolekcji w pamięci, a następnie oceniają operatory zapytań względem tej kolekcji w pamięci, podobnie jak proste IEnumerable. Zamiast pozorować, jest to w rzeczywistości rodzaj fałszywej, gdzie kolekcja w pamięci zastępuje prawdziwą bazę danych.

Ponieważ tylko DbSet sam element jest fałszywy, a zapytanie jest oceniane w pamięci, takie podejście jest bardzo podobne do użycia dostawcy ef Core w pamięci: obie techniki wykonują operatory zapytań na platformie .NET za pośrednictwem 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że niezawodnie wykonać w innym miejscu bez problemów.

Aby uzyskać prawidłowy, niezawodny test dwukrotnie, 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 stubbed lub wyśmiewany bez konieczności wykonywania rzeczywistych zapytań LINQ, skutecznie usuwając program EF Core ze stosu testowego i pozwalając testom skoncentrować się tylko na kodzie aplikacji.

Poniższy diagram porównuje fałszywe podejście bazy danych (SQLite/in-memory) ze wzorcem repozytorium:

Comparison of fake provider with repository pattern

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 wyprzedywanie 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 zapytania LINQ z możliwością testowania w metodzie zwracanej przez IEnumerable, nakłada dodatkową warstwę architektury na aplikację i może ponieść znaczne koszty implementacji i konserwacji. Ten koszt nie powinien być dyskontowany podczas wyboru sposobu testowania aplikacji, zwłaszcza jeśli testy na rzeczywistej bazie danych będą nadal potrzebne w przypadku zapytań uwidocznionych przez 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 Wzorzec repozytorium Testowanie względem bazy danych
Przetestuj podwójny typ Fałszywe Fałszywe Fałszywe Mock/stub Real, bez podwójnego
Nieprzetworzone dane SQL? Nie. Zależy Nie. Tak Tak
Transakcji? Nie (ignorowane) Tak Tak Tak Tak
Tłumaczenia specyficzne dla dostawcy? Nie. VAT Nie Tak Tak
Dokładne zachowanie zapytania? Zależy Zależy Zależy Tak Tak
Czy można używać LINQ w dowolnym miejscu w aplikacji? Tak Tak Tak Nr* Tak

* Wszystkie testowalne zapytania LINQ bazy danych muszą być hermetyzowane w metodach IEnumerable-returning repozytorium, aby można je było wyśmiewać/wyśmiewać.

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ęcie tam, a w razie potrzeby dodanie testów przy użyciu podwaja testów później, zgodnie z potrzebami.
  • Jeśli zdecydujesz się użyć testu podwójnego, zalecamy zaimplementowanie wzorca repozytorium, który umożliwia wyśmiewanie lub wyśmiewanie warstwy dostępu do danych powyżej platformy EF Core, zamiast używania fałszywego dostawcy ef Core (Sqlite/in-memory) lub przez wyśmiewanie DbSet.
  • Jeśli z jakiegoś powodu wzorzec repozytorium nie jest realną opcją, rozważ użycie bazy danych SQLite w pamięci.
  • Unikaj dostawcy w pamięci na potrzeby testowania — jest to zniechęcane i obsługiwane tylko w przypadku starszych aplikacji.
  • Unikaj pozorowania DbSet na potrzeby wykonywania zapytań.