DBUnit - testy z bazą danych
Koziołek
Ktoś pracuje nad tą stroną, jej zawartość może się wkrótce zmienić. Prosimy o cierpliwość!
1 Wstęp
2 Problemy z testowanie zawartości bazy danych
2.1 Operacje CRUD trochę teorii testów
2.1.1 Create - dodawanie danych do bazy
2.1.2 Read - odczyt bazy, weryfikacja przy odczycie leniwym
2.1.3 Update - aktualizacja bazy danych
2.1.4 Delete - usuwanie danych
3 Konfiguracja DBUnit w Apache Maven
3.2 To z jakiej bazy korzystamy ma znaczenie.
3.2.5 Uwaga na temat Oracle i typu boolean
4 DBUnit - pierwsze kroki
4.3 Tworzymy plik danych
4.4 Konieczne prace - DTD
4.5 Inne sposoby dostarczania danych
4.6 Inicjalizacja bazy danych
5 DBUnit - testujemy operacje CRUD
5.7 Program przykładowy
5.8 Dodawanie danych do bazy
5.9 Wybieranie danych z bazy
5.10 Aktualizacja danych w bazie
5.11 Usuwanie danych z bazy
6 Problemy i ich rozwiązanie
6.12 JPA, cache i czyszczenie bazy
6.13 Jak sprawdzić skomplikowane zapytanie SQL
6.14 Problem dużej ilości danych
7 DBUnit, Junit 4 i TestNG - łączymy ogień z wodą
7.15 DBUnit jako moduł JUnit 3
7.16 DBUnit jako moduł JUnit 4
7.17 DBUnit w środowisku TestNG
8 Podsumowanie
Wstęp
Testy jednostkowe powinny obejmować jak największą część kodu. Niestety niektóre jego elementy takie jak graficzny interfejs użytkownika (GUI) czy też operacje na bazie danych są kłopotliwe w testowaniu. W tym artykule przyjrzymy się problemowi testowania modułów odpowiedzialnych za operacje na danych. Naszym głównym narzędziem będzie biblioteka DBUnit. Jest to rozwiązanie, które bardzo ułatwia proces "testowania bazy danych", czyli różnych operacji związanych z obsługą danych. W naszej pracy używać będziemy też Apache Maven ponieważ ułatwi ono nam zarządzanie różnymi zależnościami obecnymi w projekcie.
W pierwszej części omówione zostaną podstawowe problemy związane z testowaniem jednostkowym modułu obsługi danych. Następnie przyjrzymy się operacją CRUD pod kątem testów. W kolejnych częściach skonfigurujemy DBUnit, utworzymy zestaw danych testowych i napiszemy aplikację, która będzie testowana. Na koniec omówimy problemy charakterystyczne dla Javy. W ostatniej części pokazana będzie konfiguracja DBUnit z Junit 4 oraz z TestNG.
Problemy z testowanie zawartości bazy danych
Testy jednostkowe modułów aplikacji służących do zarządzania danymi[#]_ są co do zasady uznawane za bardzo trudne zagadnienie niezależnie od wykorzystywanej technologii. Z tego też powodu wielu programistów pomija je na etapie wytwarzania oprogramowania i dopiero w trakcie innych testów sprawdzana jest poprawność współpracy z baza danych. Wspomnianymi innymi testami są zazwyczaj testy GUI przeprowadzane przez człowieka - testera. Jego rolą staje się nie tylko wychwytywanie wad interfejsu użytkownika, ale też weryfikacja poprawnego zarządzania danymi. Przy dużej intensywności testów lub przy współdzieleniu testowanej aplikacji przez wielu testerów np. testowanie aplikacji w której GUI jest dostarczane jako strona www, może okazać się, że błędy nie zostały wykryte lub zostały niepoprawnie sklasyfikowane jako błędy GUI.
Poza ominięciem przez testera jakiegoś przypadku testowego bardzo często zdarza się, że tego typu testy bazy danych nie zapewniają maksymalnego pokrycia kodu. Wynika to z faktu, że tester ma za zadanie wykonanie testów z listy, a te mogą nie obejmować pewnych przypadków związanych z danymi. Przykładowy test GUI nie uwzględniający przypadków granicznych dla bazy danych:
Nazwa testu | Opis | Oczekiwany rezultat | Znane błędy |
Generowanie raportu | W oknie raportów wygenerować raport za dowolny okres. Podać datę początkową i końcową dla raportu. | raport z danymi na zadany okres | Brak |
Operacje CRUD trochę teorii testów
W przypadku baz danych mówimy o operacjach CRUD - Create, Read, Update, Delete. Stanowią one podstawowe cegiełki za pomocą których porozumiewamy się z bazą danych. Każda z tych operacji zależy od innych i jest to problem przy testowaniu. Test jednostkowy powinien być niezależny. W idealnych warunkach powinna być możliwość takiego skonfigurowania środowiska testowego by każdy test był przeprowadzany w ramach swojego kontekstu. W Przypadku testowania bazy danych oznacza to, że przed testem baza powinna być wprowadzana w wymagany stan (za pomocą zewnętrznych i niezależnych mechanizmów i narzędzi), a następnie po teście powinna być przywracana do stanu pierwotnego. W praktyce oznacza to tworzenie i kasowanie bazy, przynajmniej jej części, po każdym teście. Jeżeli mielibyśmy robić to ręcznie będzie to bardzo uciążliwe. Pozostaje jeszcze do rozwiązani problem weryfikacji poprawności testu. O ile w przypadku operacji czytania z bazy danych można zrobić to porównując otrzymane wyniki z oczekiwanymi rezultatami. Znacznie gorzej ma się sprawa innych operacji. Jeżeli chcielibyśmy weryfikować ich poprawność za pomocą operacji odczytu to może się okazać, że nie jesteśmy wstanie tego zrobić, bo operacja odczytu działa nieprawidłowo i cały test jest fałszywy. Dlatego też zalecaną metodą jest użycie wyspecjalizowanych bibliotek i narzędzi, które wykonaja odpowiednie zadania za nas. Ułatwiają one też konfigurację środowiska testowego uwzględniając tworzeni, kasowanie bazy danych, ale też na przykład resetowanie sekwencji.
Poniżej omówione są najpopularniejsze problemy związane z testowaniem poszczególnych operacji.
Create - dodawanie danych do bazy
Do najpopularniejszych problemów należy weryfikacja poprawności zapisanych danych. Należy przeprowadzać ją za pomocą nie związanego z naszym kodem narzędzia, które nie jest podatne na nasze błędy. Kolejnym problemem jest weryfikacja w przypadku złożonych operacji zapisu. Operacje tego typu zazwyczaj zmieniają wiele tabel w tym tabele łączące. Weryfikacja takiej zmiany jest czasochłonna. Ostatnim problemem jest uwzględnienie klucza głównego rekordu. Zazwyczaj jest on nadawany automatycznie za pomocą sekwencji lub podobnego mechanizmu. Należy sobie odpowiedzieć na pytanie czy w takim przypadku weryfikujemy jego poprawność. Jeżeli tak musimy uwzględniać konieczność resetowania sekwencji przed rozpoczęciem każdego z testów.
Read - odczyt bazy, weryfikacja przy odczycie leniwym
Odczyt danych jest najprostszy w weryfikacji, ale stajemy przed innymi problemami. Po pierwsze powinniśmy przed przystąpieniem do testu wypełnić bazę danych. Jeżeli chcemy zachować niezależność testów musimy zrobić to za pomocą niezależnego narzędzia, a nie własnego kodu wykonującego zapis. Jeżeli wykorzystamy własny kod może okazać się, że nie działa on prawidłowo, a otrzymane wyniki są fałszywe. Po drugie można rozróżnić dwa rodzaje operacji odczytu. Operacje proste, w których rezultat jest łatwy do przewidzenia na przykład odczyt pojedynczego rekordu z wyborem na podstawie klucza głównego lub odczyt grypy rekordów na podstawie podanego warunku. Operacje złożone to zazwyczaj zapytania, które mogą przyjmować więcej niż jeden warunek, a dodatkowo warunki są uzależnione od siebie. Przykładowo wspomniana wcześniej procedura raportująca do której przekazywane są daty. Test powinien uwzględniać sytuację w której do bazy danych zostaje przesłane zapytanie z zamienionymi datami (data DO jest wcześniejsza niż OD). Ważne jest też maksymalne pokrycie różnych kombinacji warunków dla rekordów. Przykładowo dla zapytania:
SELECT * FROM abc WHERE a = 'x' OR b='y' AND c ='z';
Należy uwzględnić rekordy, w których występuja wszystkie kombinacje dla pól a, b i c:
a | b | c | Czy pojawia się? |
x | y | z | + |
A | y | z | + |
x | B | z | + |
A | B | z | - |
x | y | C | - |
A | y | C | - |
x | B | C | - |
A | B | C | - |
Przy bardziej skomplikowanych warunkach odczytu tabelka ta będzie rosła zwiększając komplikacje testu. Jej ręczna weryfikacja będzie uciążliwa. DBUnit pozwalana porównywanie wyników ze wcześniej skonfigurowanym zestawem oczekiwanych rezultatów.
W przypadku korzystania z pośrednika może okazać się, że część testów kończy się niepowodzeniem ponieważ następują błędy w dostępnie do leniwie inicjowanych obiektów. W takim przypadku należy wyłączyć leniwą inicjację albo jeszcze raz przemyśleć strukturę testu, aby uwzględniała tego typu odczyt obiektów.
Update - aktualizacja bazy danych
Operacja aktualizacji jest w pewnym sensie połączeniem operacji odczytu i wstawiania rekordu. Niestety łączy też problemy tych operacji. W trakcie testowania należy uwzględnić nie tylko problem prawidłowego wstawienia danych, ale też to czy dane są wstawiane dla konkretnego obiektu. Jeżeli korzystamy z aktualizacji wykorzystując inne niż klucz główny warunki należy uwzględnić różne ich kombinacje. Powoduje to oczywiście znaczny przyrost stopnia trudności testu.
Delete - usuwanie danych
Operacje usuwania danych są stosunkowo proste w weryfikacji. Jednak tak jak w przypadku odczytu i aktualizacji jeżeli korzystamy z rozbudowanych warunków należy uwzględnić różne ich kombinacje. Dodatkowym problemem jest sposób wiązania obiektów. Przed usunięciem jakiegoś rekordu należy uwzględnić kaskadowe usuwanie innych rekordów. Weryfikacja tego mechanizmu jest bardzo ważna ponieważ może okazać się, że usunięcie jednego rekordu spowoduje usunięcie innych, które nie powinny zostać usunięte. Na przykład w systemie przywilejów i ról usunięcie roli nie powinno skutkować usunięciem przywilejów o ile przywileje mogą być przypisane do różnych ról.
Po tej małej dawce problemów przejdźmy do naszych testów.
Konfiguracja DBUnit w Apache Maven
Najprostszą metodą testowania bazy danych jest uruchomienie testów w ramach pakietu testów całej biblioteki. Najwygodniej jest zrobić to za pomocą Mavena. Poniższy fragment pliku pom.xml opisuje konfigurację DBUnit z bazą HSQLDB.
<dependency>
<groupId>org.dbunit</groupId>
<artifactId>dbunit</artifactId>
<version>2.4.7</version>
</dependency>
<dependency>
<groupId>hsqldb</groupId>
<artifactId>hsqldb</artifactId>
<version>1.8.0.7</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.5.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.14</version>
<scope>test</scope>
</dependency>
Wszystkie biblioteki są dodawane tylko do testów. Jest to istotne ponieważ log4j może być wymagany w różnych wersjach przez inne zależności. Różnice pomiędzy poszczególnymi wersjami tej biblioteki są dość znaczące i mogą mieć wpływ na środowisko testowe.
Kolejnym krokiem jest dodanie pliku log4j.xml w katalogu src/test/resources/:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE log4j:configuration SYSTEM "log4j.dtd">
<log4j:configuration xmlns:log4j="http://jakarta.apache.org/log4j/">
<appender name="DBUNIT.DEBUG.LOG" class="org.apache.log4j.DailyRollingFileAppender">
<param name="File" value="logs/dbunit.debug.log" />
<param name="Append" value="false" />
<param name="ImmediateFlush" value="true" />
<param name="Threshold" value="DEBUG" />
<layout class="org.apache.log4j.PatternLayout">
<param name="ConversionPattern" value="%d %-5p [%t] %c %C
(%F:%L) - %m\n" />
</layout>
</appender>
<logger name="com.company.sql" additivity="false">
<level value="DEBUG" />
<appender-ref ref="DBUNIT.DEBUG.LOG" />
</logger>
<root>
<level value="debug" />
<appender-ref ref="DBUNIT.DEBUG.LOG" />
</root>
</log4j:configuration>
To z jakiej bazy korzystamy ma znaczenie.
W najprostszych przypadkach rodzaj bazy danych wykorzystywanej w testach i w produkcji nie powinien mieć znaczenia. Odpowiednią abstrakcję zapewnia nam używany JPA. Uwalnia nas od znajomości konkretnych mechanizmów poszczególnych RDBMS. Jednakże w rzeczywistości gdy mamy do czynienia z bardziej skomplikowanymi zapytaniami lub wykorzystujemy pewne cech niektórych silników baz danych może się okazać, że musimy testować aplikację na takiej samej bazie jaka będzie użyta produkcyjnie.
Jeżeli używamy JDBC i samodzielnie piszemy albo generujemy zapytania zalecam użycie bazy takiej jak na produkcji. Należy poprosić administratora by udostępnił nam bazę o takich samych parametrach jak baza produkcyjna i dodatkowo utworzył użytkownika testowego, który będzie mógł zarządzać tabelami, sekwencjami czy indeksami. W ten sposób mamy pewną gwarancję, że testy będą przeprowadzane w środowisku zbliżonym do produkcyjnego.
Uwaga na temat Oracle i typu boolean
W przypadku bazy Oracle pojawia się dodatkowy bardzo poważny problem. Mianowicie Oracle nie posiada typu danych boolean. Dlatego dla bezpieczeństwa należy korzystać w trakcie testów z bazy Oracle jeżeli taką bazę będziemy używać produkcyjnie. Pozwoli to uniknąć przykrych wpadek. Jest to ekstremalnie ważne jeżeli nie wykorzystujemy JPA, a bezpośrednie JDBC. Możemy w ten sposób wykryś wszystkie nieprawidłowości w naszych zapytaniach (po prostu nie zostaną wykonane).
DBUnit - pierwsze kroki
Przygotowanie testu DBUnit można podzielić na trzy kroki. W pierwszym przygotowujemy dane testowe. W drugim przygotowujemy środowisko testowe, czyli przede wszystkim źródło połączeń JDBC i narzędzia wspomagające tworzenie testów. Trzeci krok to przygotowanie samych testów.
Tworzymy plik danych
Najprostszym sposobem zapisu danych jest użycie pliku XML. Plik przetwarzany przez DBUnit musi spełniać kilka wymagań.
- Główny element musi mieć nazwę dataset.
- Nazwa każdego elementu odpowiada nazwie tabeli.
- Nazwa atrybutu odpowiada nazwie kolumny.
- Element w dokumencie odpowiada rekordowi w bazie.
Jest to intuicyjny sposób zapisu danych. Dodatkowo dzięki temu, że jest to plik XML to można bardzo swobodnie nim manipulować. Łącznie z użyciem DBUnit jako narzędzia do ładowania danych do bazy.
Przykładowy plik datasource.xml zawiera opis kliniki weterynaryjnej:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE dataset SYSTEM "data.dtd">
<dataset>
<owners id="1" name="jan kowalski" address="ul. Lini 2" />
<pets id="1" name="puszek" type_id="1" owner_id="1" />
<visits id="1" pet_id="1" date="piątek 13" />
<visits id="2" pet_id="1" />
<types id="1" name="sierściuch" />
</dataset>
Niestety nie wszystko jest tak idealne. Sam plik xml to nie wszystko. DBUnit wykorzystuje DTD przy pracy z plikami xml. Musimy zatem stworzyć plik data.dtd.
Konieczne prace - DTD
Tworzenie pliku DTD dla pliku z danymi choć wydaje się trudne to jest tylko pracochłonne. Przede wszystkim dokument nie jest skomplikowany, a zatem nie ma potrzeby tworzenia skomplikowanego pliku DTD z wykorzystaniem dodatkowych elementów jak na przykład Zbiory wartości. Choć czasami gdy używamy na przykład słowników to jest to bardzo pomocne. Poniżej plik data.dtd, który opisuje strukturę dokumentu datasource.xml:
<?xml version="1.0" encoding="UTF-8"?>
<!ELEMENT dataset (owners*, pets*, visits*, types*)>
<!ELEMENT owners EMPTY>
<!ATTLIST owners
id CDATA #REQUIRED
name CDATA #REQUIRED
address CDATA #REQUIRED
>
<!ELEMENT pets EMPTY>
<!ATTLIST pets
id CDATA #REQUIRED
owner_id CDATA #REQUIRED
type_id CDATA #REQUIRED
name CDATA #REQUIRED
>
<!ELEMENT visits EMPTY>
<!ATTLIST visits
id CDATA #REQUIRED
pet_id CDATA #REQUIRED
date CDATA #IMPLIED
>
<!ELEMENT types EMPTY>
<!ATTLIST types
id CDATA #REQUIRED
name CDATA #REQUIRED
>
Użycie oznaczenia atrybutu #IMPLIED pozwala na umieszczenie w bazie danych wartości NULL.
Inne sposoby dostarczania danych
Pliki xml nie są jedynym sposobem opisu danych testowych. DBUnit pozwala na pobieranie danych z plików CSV za pomocą klasy org.dbunit.dataset.csv.CsvProducer. W takim przypadku każda tabela jest opisana w osobnym pliku .csv. Jeszcze inną metodą jest użycie plików Excela i klasy org.dbunit.dataset.excel.XlsDataSet. Metoda ta jest szczególnie dobra jeżeli chcemy uzyskać pewną dodatkową właściwość. Otóż dane kontrolne można przygotować całkowicie poza zespołem programistów. Następnie grupa testerów wprowadza w trakcie testowania GUI dane i na koniec następuje ich weryfikacja za pomocą DBUnit. Jest to wygodne rozwiązanie jeżeli chcemy przesunąć testy bazy danych na czas testów GUI. Kolejną metodą jest tworzenie danych testowych z wykorzystaniem tylko Javy. Wymaga to jednak odpowiedniego przygotowania klas fabrykujących, które będą zwracały implementacje interfejsu org.dbunit.dataset.ITable oraz klasy org.dbunit.dataset.Column. Jest to dość trudne, ale pozwala na budowanie nietypowych struktur danych.
Inicjalizacja bazy danych
DBUnit - testujemy operacje CRUD
Program przykładowy
Dodawanie danych do bazy
Wybieranie danych z bazy
Aktualizacja danych w bazie
Usuwanie danych z bazy
Problemy i ich rozwiązanie
JPA, cache i czyszczenie bazy
Jak sprawdzić skomplikowane zapytanie SQL
Problem dużej ilości danych
DBUnit, Junit 4 i TestNG - łączymy ogień z wodą
DBUnit jako moduł JUnit 3
DBUnit jako moduł JUnit 4
DBUnit w środowisku TestNG
Podsumowanie
.. [#] Od tego miejsca dla uproszczenia języka będę pisał o testach jednostkowych z bazą danych albo o testach jednostkowych bazy danych. Nie należy jednak mylić tego z testowaniem RDBMS.