Zalążki artykułów

DBUnit - testy z bazą danych

  • 2010-10-31 18:27
  • 0 komentarzy
  • 1489 odsłon
  • Oceń ten tekst jako pierwszy
Artykuł  został umieszczony na liście Zalążków artykułów. Jeżeli możesz rozbuduj go!




Strona w budowie
Ktoś pracuje nad tą stroną, jej zawartość może się wkrótce zmienić. Prosimy o cierpliwość!



Spis treści

     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.1 To z jakiej bazy korzystamy ma znaczenie.
               3.1.1 Uwaga na temat Oracle i typu boolean
     4 DBUnit - pierwsze kroki
          4.1 Tworzymy plik danych
          4.2 Konieczne prace - DTD
          4.3 Inne sposoby dostarczania danych
          4.4 Inicjalizacja bazy danych
     5 DBUnit - testujemy operacje CRUD
          5.1 Program przykładowy
          5.2 Dodawanie danych do bazy
          5.3 Wybieranie danych z bazy
          5.4 Aktualizacja danych w bazie
          5.5 Usuwanie danych z bazy
     6 Problemy i ich rozwiązanie
          6.1 JPA, cache i czyszczenie bazy
          6.2 Jak sprawdzić skomplikowane zapytanie SQL
          6.3 Problem dużej ilości danych
     7 DBUnit, Junit 4 i TestNG - łączymy ogień z wodą
          7.1 DBUnit jako moduł JUnit 3
          7.2 DBUnit jako moduł JUnit 4
          7.3 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 danymi1 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 testuOpisOczekiwany rezultatZnane błędy
Generowanie raportuW oknie raportów wygenerować raport za dowolny okres. Podać datę początkową i końcową dla raportu.raport z danymi na zadany okresBrak

Test wydaje się na pierwszy rzut oka prawidłowy. Rzeczywiście jeżeli walidator danych z formularza umie wychwycić sytuację, w której data końcowa jest wcześniejsza niż początkowa to cały test będzie zachowywał się prawidłowo. Nie mamy jednak odpowiedzi na pytanie jak zachowa się nasze DAO w momencie gdy otrzyma nieprawidłowe dane. Nie mamy też możliwości sprawdzenia takiej sytuacji ponieważ tester nie ma możliwości weryfikacji takiej sytuacji. Jego dane będą odrzucane przez walidator i nie będą dostarczane do DAO.
Innym problemem może być korzystanie z pośrednika w dostępie do bazy danych. Pośrednikiem może być usługa lub narzędzie ORM. W takim wypadku może on stosować cache i dane mogą nie być utrwalone, ale będą dostępne w aplikacji ponieważ pośrednik wykorzysta cache. Mechanizm cache można zazwyczaj wyłączyć, ale w trakcie testów nie wskazane jest ciągłe zmienianie konfiguracji. Testy jednostkowe umożliwiają uruchamianie każdego testu w odosobnionym kontekście z własną konfiguracją. Dzięki czemu można wyłączyć niektóre mechanizmy pośrednika.
Kolejnym podejściem do problemu testowania aplikacji z bazą danych jest ręczna weryfikacja stanu bazy danych. Jest to dość popularne podejście stosowane przez wielu programistów. Polega ono na wywołaniu odpowiedniej funkcji, a następnie ręcznym sprawdzeniu czy stan bazy danych odpowiada naszym oczekiwaniom. Jest to przede wszystkim bardzo pracochłonna metoda. Poza tym w przypadku skomplikowanych transakcji w których aktualizowane jest wiele rekordów w różnych tabelach musimy używać wielu prostych zapytań. Pozostaje jeszcze pytanie czy zapytania, które konstruujemy w celu weryfikacji stanu bazy danych są poprawne. Szczególnie wrażliwe są zapytania nietrywialne, które bardzo często pojawiają się w modułach raportowych.
Problemy te są niezależne od języka. W praktyce nie istnieje język lub technologia, która w swojej architekturze gwarantuje bezproblemową współpracę z bazą danych. Dochodzi tu jeszcze czynnik ludzki, bo to człowiek tworzy zapytania SQL i jako, że jest omylny mogą one zawierać błędy.
W języku Java też nie ma gwarancji poprawnego wykonania operacji na bazie danych. Dlatego ważne jest dokładne przetestowanie kodu wykonującego tego typu operacje. Testy takie można zautomatyzować, a tym samym uprościć dzięki wykorzystaniu odpowiednich narzędzi. Zanim jednak zajmiemy się jednym z nich przyjrzyjmy się najpopularniejszym problemom, przed którymi staniemy w trakcie testów.

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:

abcCzy pojawia się?
xyz+
Ayz+
xBz+
ABz -
xyC-
AyC-
xBC-
ABC-


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ń.

  1. Główny element musi mieć nazwę dataset.
  2. Nazwa każdego elementu odpowiada nazwie tabeli.
  3. Nazwa atrybutu odpowiada nazwie kolumny.
  4. 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



[1]  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.

User:Koziołek