Testy jednostkowe

Koziołek

1 Wstęp
2 Rodzaje testów
3 Testy jednostkowe - Co i po co?
     3.1 Jak przeprowadzamy testy jednostkowe?
     3.2 Co testować?
     3.3 Struktura testów
          3.3.1 Niezależność
          3.3.2 Powtarzalność
          3.3.3 Jednoznaczność
          3.3.4 Jednostkowość
     3.4 Szczególne rodzaje testów jednostkowych
4 Tworzymy testy
     4.5 Konwencje nazewnicze JUnit 3
     4.6 Testowany interfejs
     4.7 Założenia testu
          4.7.5 Klasa <samp>AbstractSumatorTest</samp>, czyli szablony testów
          4.7.6 <a href="mailto:samp"></a>
          4.7.7 <a href="mailto:samp"></a>
          4.7.8 <a href="mailto:samp"></a>
          4.7.9 <a href="mailto:samp"></a>
5 <a href="mailto:samp"></a>

Wstęp

Tworząc program zazwyczaj opieramy się o jakieś założenia. Nawet jeżeli piszemy niewielki program, który ma ułatwiać nam codzienne czynności np. skrypty emulujące niektóre funkcje konsoli linuxowej w MS Windows to warto przetestować taki program. Nie ma jednolitego API języka dla testów w postaci specyfikacji JSR. Istnieje za to szereg bibliotek służących do tworzenia testów ich uruchamiania. W poniższym artykule prezentowana jest biblioteka JUnit w wersji 4. Przedstawione zostaną też najważniejsze elementy biblioteki w wersji 3.8 ponieważ nadal jest ona w użyciu. Inne narzędzia służące do przeprowadzania testów są omówione w osobnych artykułach.

Rodzaje testów

Proces testowania aplikacji obejmuje wiele etapów. Można je podzielić na dwie grupy. Pierwszą z nich stanowią testy "czarnoskrzynkowe". Określenie to dobrze oddaje naturę tych testów. Tester otrzymuje aplikację lub jej moduł i stara się ją popsuć. Nie wie jak testowany element jest zbudowany, a tylko wie jaką logikę powinien spełniać. Zaletą tego typu testów jest możliwość wyszukania nietypowych zachować użytkownika. W ten sposób przeprowadzane są testy integracyjne, testy interfejsu użytkownika (ang. - User Interface; UI), czy też testy akceptacyjne. Wadą tego podejścia jest niska automatyzacja procesu testowego. Niestety do wielu z nich trzeba zatrudnić ludzi, którzy ręcznie przejdą procedury testowe.
Druga grupa testów to testy "białoskrzynkowe". Zazwyczaj są przeprowadzane wewnątrz zespołu programistów. Mogą one obejmować zarówno całe moduły i być traktowane jako testy obciążeniowe/wydajnościowe, ale mogą też dotyczyć niewielkich fragmentów kodu takich jak pojedyncze klasy czy metody. W tym przypadku nazywamy je testami jednostkowymi.

Test jednostkowy to test, który sprawdza działanie pojedynczej jednostki kodu. Jednostką mogą być klasy, metody, ale też proste procesy w ramach pojedynczego modułu.

Testy jednostkowe - Co i po co?

Test jednostkowy służy do sprawdzenia pojedynczej jednostki kodu. Jednostką taką zazwyczaj jest metoda. Testy organizowane są w zestawy, które obejmują pojedyncze klasy. Czasami można spotkać się z testami jednostkowymi prostego procesu, który występuje w danej bibliotece kodu. Przykładem takiego procesu może być tworzenie obiektu za pomocą metody fabrykującej. W procesie wykonywanych jest kilka kroków, ale test sprawdza proces jako pojedynczą jednostkę aplikacji.

Jak przeprowadzamy testy jednostkowe?

Testy jednostkowe można przeprowadzać na kilka sposobów. Najprostszym i najczęściej stosowanym jest wypisywanie na standardowe wyjście stanu systemu w pewnych miejscach. Jest to szeroko stosowane przez osoby początkujące jak i profesjonalistów podejście czasami nazywane "słabym debuggowaniem". Jest ono o tyle wygodne, że pozwala na sprawdzenie stanu systemu w trakcie wykonywania metody. Bardziej doświadczeni programiści zamiast wypisywania używają debuggera. Wadą tego podejścia jest konieczność kompilacji kodu za każdym razem gdy wykonujemy test oraz po zakończeniu testu i usunięciu metod wypisujących. dodatkowo musimy kompilować kod po zakończeniu testów co oznacza, że może się on zmienić. Kolejną wadą jest brak możliwości automatyzacji wyników.
W języku Java nie ma możliwości kompilacji kodu bez dostarczenia wszystkich zależności. Oznacza to, że kod w procesie testowania musi mieć wszystkie zależności. Przy prezentowanej powyżej metodzie testów powoduje to konieczność kompilowania całego modułu przy każdej zmianie testu. Proces kompilacji może być długi i spowodować problemy związane z produktywnością testera.
Inną metodą jest napisanie mini aplikacji, która będzie wywoływała kilkukrotnie testowany kod i następnie weryfikowała wyniki. Takie podejście jest zdecydowanie lepsze ponieważ pozwala na automatyzację całego procesu testów. Zamiast jednak samemu pisać kod można użyć biblioteki JUnit, która zawiera już odpowiednie mechanizmy związane z przygotowaniem, uruchamianiem i weryfikowaniem wyników testu. Inną biblioteką jest TestNG. My jednak będziemy wykorzystywać popularniejszą, czyli JUnit.

Co testować?

Zanim przystąpimy do pisania przykładowej aplikacji należy zastanowić się nad tym co chcemy testować. Istnieją dwie szkoły. Pierwsza z nich mówi iż testujemy tylko publiczne API klasy. Metody prywatne nie mogą być wywołane przez klienta zatem nie ma potrzeby ich testowania. Zaletą takiego podejścia jest przejrzystość testów. Klient przeglądając kod testowy widzi jakie elementy może wykorzystać i jak będą się one sprawowały. Wadą jest niekompletność testów oraz brak możliwości sprawdzenia w razie niepowodzenia, który dokładnie element zawiódł.
Druga szkoła mówi, że testujemy wszystkie elementy kodu. Wymaga ona użycia odpowiednich narzędzi takich jak na przykład refleksja w celu uzyskania dostępu do prywatnych składowych kodu. Można użyć też narzędzi takich jak PMT, które pozwalają na wywołanie metod prywatnych za pomocą jednego polecenia. Zaletą tego podejścia jest znacznie większe pokrycie kodu testami. Można też znacznie dokładniej sprawdzić kod. Wadą jest konieczność pisania znacznie większej ilości testów oraz ujawnienie w nich prywatnego API klasy.
Wybór pomiędzy tymi metodykami uzależniony jest od tego co chcemy przetestować i jaki efekt osiągnąć.
Ważnym czynnikiem jest wielkość testowanego fragmentu kodu. Jeżeli metoda publiczna wywołuje kilka metod prywatnych to w celu lepszego sprawdzenia kodu warto wybrać drugie podejście. Jeżeli jednak mamy do czynienia z kodem, który nie korzysta zbyt intensywnie z prywatnych składników to warto rozważyć pierwsze podejście.

Struktura testów

Testy jednostkowe muszą spełniać kilka założeń.

Niezależność

Testy powinny być niezależne. Oznacza to, że dwa dowolne testy nie wpływają na siebie nawzajem. Nie istnieje konieczność utrzymania sztywnej kolejności wywołania testów. Nie zawsze można uzyskać taki stan, ale warto zadbać o jak największą niezależność testów.

Powtarzalność

Testy powinny być powtarzalne, łatwe do uruchomienia w dowolnym momencie i gwarantujące stabilność. Oznacza to, że pisząc test jednostkowy możemy w dowolnym momencie go uruchomić bez konieczności uruchamiania dodatkowych elementów. Powtarzalność oznacza też, że test daje taki sam wynik za każdym razem dla danego zestawu danych.

Jednoznaczność

Test jednostkowy powinien być jednoznaczny, czyli jasno odpowiadać na pytanie o poprawność działania testowanej funkcjonalności.

Jednostkowość

Test jednostkowy to test, który testuje jedną rzecz na raz. Nie wolno pisać testu w którym staramy się sprawdzić dwie funkcjonalności (to jest bardzo zły test). Nie należy też sprawdzać w jednym teście kilku zestawów danych. Taki test w przypadku niepowodzenia nie zwraca jednoznacznych wyników.

Szczególne rodzaje testów jednostkowych

Jak zawsze istnieją pewne wyjątki od reguły. Najtrudniejszym rodzajem testów jednostkowych są testy związane z komunikacją, współbieżnością i obsługa bazy danych. Testy tego typu wymagają użycia skomplikowanych narzędzi, których opis wykracza poza ten artykuł.

Tworzymy testy

Zanim przystąpimy do pisania kodu należy skonfigurować środowisko pracy. Jeżeli korzystasz na co dzień z narzędzia Apache Maven to po wygenerowaniu projektu zaimportuj go do IDE. Jeżeli jednak nie znasz tego narzędzia stwórz projekt w ulubionym IDE. Ja będę używał Eclipse 3.5 bez mavena. W tym celu tworzę nowy projekt java i do listy katalogów źródłowych dodaję katalog <samp>test</samp> oraz do listy bibliotek bibliotekę JUnit 4. Struktura pakietów w katalogu <samp>test</samp> odpowiada strukturze z <samp>src</samp>. Nie jest to wymagane, ale ułatwia zarządzanie testami. Ważna rzeczą jest skonfigurowanie różnych folderów docelowych dla testów i źródeł. Robi się to w Eclipse poprzez zaznaczenie opcji "Allow output folders for source folders" i konfigurację ścieżek. Nie zrobienie tego spowoduje, że kod testowy wymiesza się z kodem aplikacji.

Konwencje nazewnicze JUnit 3

Biblioteka JUnit w wersji 3 opiera się na zastosowaniu konwencji nazewniczych. Klasa zawierająca testy musi rozszerzać abstrakcyjną klasę <samp>junit.framework.TestCase</samp>. Nazwa metody testowej musi zaczynać się od słowa <samp>test</samp>. Przed każdym testem jest uruchamiana metoda <samp>void setUp()</samp>, po każdym teście uruchamiana jest metoda <samp>void tearDown()</samp>
W bibliotece JUnit 4 zastąpiono ten mechanizm za pomocą adnotacji.

Testowany interfejs

Poniżej prezentuje interfejs dla którego będziemy pisać testy.

package pl.koziolekweb.programmers.testy;
 
/**
 * Interfejs sumator służy do sumowania liczb.
 *
 * @author koziołek
 *
 */
public interface Sumator {
    /**
     * Dodaje do sumatora wskazaną liczbę.
     *
     * @param x
     *            czynnik
     */
    void dodaj(long x);
 
    /**
     * Odejmuje do sumatora wskazaną liczbę. Liczba nie może być ujemna.
     * 
     * @param x
     *            odjemnik
     * @throws IllegalArgumentException
     *             jeżeli odjemnik jest ujemny.
     */
    void odejmij(long x);
 
    /**
     * Zwraca aktualną wielkość sumatora. Dla pustego sumatora zwraca 0.
     *
     * @return wielkość sumatora.
     */
    long wielkość();
}

Testy w pierwszej kolejności powinny sprawdzać zachowanie implementacji interfejsów. Takie podejście jest zgodne z założeniami testowania tylko publicznych składników. Pozwala ono ponad to na sprawdzanie poprawności implementacji bez znajomości konkretnych klas implementujących. Interfejs bez zestawu testów jest niekompletny i nie powinien zostać przekazany do implementacji. Jest to też spełnienie założeń programowania obiektowego mówiących o tym, że obiekty porozumiewają się za pomocą interfejsów, a nie konkretnych implementacji. Dlatego też należy w pierwszej kolejności testować interfejsy. Testy konkretnych implementacji zazwyczaj powinny obejmować ich niepubliczne składniki.

Założenia testu

Pisząc testy należy przyjąć pewne założenia. Jeżeli wiem co dany interfejs ma robić należy określić przypadki typowe, przypadki błędne oraz przypadki graniczne i specyficzne.
Stwórzmy zatem klasę testową <samp>AbstractSumatorTest</samp> i dodając kolejne testy przyjrzyjmy się każdej z tych grup.

Klasa <samp>AbstractSumatorTest</samp>, czyli szablony testów

package pl.koziolekweb.programmers.testy;
 
import static org.junit.Assert.*;
 
import org.junit.After;
import org.junit.Before;
 
public abstract class AbstractSumatorTest {
 
    protected Sumator sumator;
 
    public abstract Sumator getSumator();
 
    @Before
    public void setUp(){
        this.sumator = getSumator();
    }
 
    @After
    public void tearDown(){
        this.sumator = null;
    }
//...
}

Klasa jest abstrakcyjna. Zawiera ona jedną metodę abstrakcyjną, która będzie dostarczana wraz z implementacją. Jej zadaniem będzie zwrócenie instancji sumatora potrzebnej do testów. Wszystkie testy publicznych składników zawarte będą w tej klasie. Jednocześnie jeżeli zaistnieje taka konieczność programista implementujący interfejs będzie mógł rozszerzyć tą klasę i dodać własne testy. W przypadku minimum musi tylko rozszerzyć tą klasę i dodać metodę fabrykującą instancję interfejsu. Zaletą takiego podejścia jest możliwość kontrolowani jakości implementacji. Można w łatwy sposób określić, którzy dostawcy spełniają założenia projektowe, a którzy nie. Jest to szczególnie istotne w przemysłowej produkcji oprogramowania, gdzie bardzo często trzeba zmusić do współpracy wielu dostawców.
Patrząc a ten kod należy zwrócić też uwagę na metody <samp>setUp()</samp> i <samp>tearDown()</samp>. Pierwsza z nich jest oznaczona adnotacją @Before</samp">samp>@Before</samp</a> oznacza to, że zostanie wywołana przed każdym testem znajdującym się w tej klasie. Druga oznaczona adnotacją

5 komentarzy

Tester nie psuje, tylko pokazuje to, co jest juz zepsute.

@cepa, myślałem o tym. O CI chyba nie tu a w inżynierii oprogramowania należy napisać. Biblioteki typu EasyMock, JMock to osobny temat i choć użycie jest dość proste to backend jest nietrywialny i trzeba by było o tym pomyśleć.

no w koncu cos na poziomie :)
Koziołek: dodalbym cos o mockach, stubach itp bo w praktyce sie przydaje
i moze cos o ciaglej integracji

Artykuł powstał ponad rok temu. Ja się trochę nauczyłem od tego czasu :) w wolnej chwili poprawię, albo nie... ty to popraw :) jak społeczność to społeczność.

Pozdrawiam

Dlaczego mieszasz JUnit 3.x z JUnit 4.x ?
Adnotacje @Before i @After to elementy JUnit 4 (pakiet org.junit.* ). Więc jeśli i tak używamy wersji 4 to możemy:

  • zrezygnować z rozszerzania junit.framework.TestCase,
  • zrezygnować z wykonywania super.setUp(); super.tearDown();
  • poprzedzić test adnotacją @Test (i nie musimy wtedy w nazwy metody testującej rozpoczynać od test
  • zaimportować statycznie org.junit.Assert.* i wykorzystywać aseracje JUnit 4