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 AbstractSumatorTest, czyli szablony testów
               4.7.6 Przypadki typowe
               4.7.7 Przypadki błędne
               4.7.8 Przypadki graniczne i specyficzne
               4.7.9 Testy usług zdalnych
     5 Podsumowanie

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 linuksowej w systemie MS Windows – to warto przetestować taki program. Nie ma jednolitego API języka Java dla testów w postaci specyfikacji JSR – istnieje za to szereg bibliotek służących do tworzenia testów i ich uruchamiania. W poniższym artykule prezentowana jest biblioteka JUnit w wersji 4. Przedstawione zostaną też najważniejsze elementy tej biblioteki w wersji 3.8, ponieważ biblioteka w tej wersji nadal jest w użyciu. Inne narzędzia służące do przeprowadzania testów w Javie są omówione w osobnych artykułach.

Rodzaje testów

Proces testowania aplikacji obejmuje wiele etapów. Etapy te można podzielić na dwie grupy. Pierwszą z nich stanowią testy "czarnoskrzynkowe". To określenie dobrze oddaje ich naturę. Tester otrzymuje aplikację lub jej moduł i stara się ją popsuć. Nie wie, jak testowany element jest zbudowany, zna jedynie logikę, jaką 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ą one 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 drugim przypadku nazywamy je testami jednostkowymi.

Test jednostkowy to test, który sprawdza działanie pojedynczej jednostki kodu. Jednostką może być klasa, metoda, 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 obejmujące pojedyncze klasy. Czasami można spotkać się również 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 podejście, zarówno przez osoby początkujące, jak i profesjonalistów; bywa nazywane "słabym debugowaniem". Jest ono o tyle wygodne, że pozwala na sprawdzenie stanu systemu w trakcie wykonywania metody. Bardziej doświadczeni programiści używają debugera zamiast bezpośredniego wypisywania. Wadą tego podejścia jest konieczność kompilacji kodu tak za każdym razem, gdy wykonujemy test, jak i po jego zakończeniu (i usunięciu metod wypisujących). Dodatkowo, musimy kompilować kod po zakończeniu wszystkich 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 musi mieć dostarczone wszystkie zależności już w procesie testowania. Przy prezentowanej powyżej metodzie testów powoduje to konieczność kompilowania całego modułu przy każdej zmianie każdego testu. Proces kompilacji może być długi i zmniejszyć produktywność testera.

Inną metodą niż wypisywanie stanu systemu jest napisanie mini-aplikacji, która będzie wywoływała kilkukrotnie testowany kod, a następnie weryfikowała wyniki. Takie podejście jest zdecydowanie lepsze, ponieważ pozwala na automatyzację całego procesu. Ponadto, zamiast samemu pisać kod, można użyć biblioteki JUnit – która zawiera już odpowiednie mechanizmy związane z przygotowaniem, uruchamianiem i weryfikowaniem wyników testów. Inną biblioteką jest na przykład 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 składowe API danej 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 od razu, jakie elementy może wykorzystać, i jak będą się one sprawowały. Wadą jest niekompletność testów oraz, w razie niepowodzenia, brak możliwości sprawdzenia, 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ładowych, to warto rozważyć pierwsze podejście.

Struktura testów

Testy jednostkowe powinny 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. Dzięki temu nie istnieje konieczność utrzymania sztywnej kolejności wywołania testów. Nie zawsze można uzyskać stan, by żadne dwa testy nie wpływały na siebie, 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 przy każdym uruchomieniu dla tego samego zestawu danych test daje taki sam wynik.

Jednoznaczność

Test jednostkowy powinien być jednoznaczny, czyli jasno odpowiadać na pytanie, czy testowana funkcjonalność działa poprawnie.

Jednostkowość

Test jednostkowy to test, który testuje jedną rzecz naraz. Nie należy 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. Najtrudniejszymi do napisania rodzajami testów jednostkowych są testy związane z komunikacją, współbieżnością i obsługą bazy danych. Takie testy 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 w nim projektu, zaimportuj projekt do IDE. Jeżeli jednak nie znasz tego narzędzia, to po prostu stwórz nowy projekt w swoim 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 test, a do listy bibliotek dodaję bibliotekę JUnit 4. Struktura pakietów w katalogu test odpowiada strukturze z src. Nie jest to wymagane, ale ułatwia zarządzanie testami. Ważną rzeczą jest skonfigurowanie innych folderów docelowych dla testów, a innych dla źródeł. W Eclipse robi się to poprzez zaznaczenie opcji "Allow output folders for source folders" i konfigurację ścieżek. Niezrobienie 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ę junit.framework.TestCase. Nazwa metody testowej musi zaczynać się od słowa test. Przed każdym testem jest uruchamiana metoda void setUp(), po każdym teście uruchamiana jest metoda void tearDown(). W bibliotece JUnit 4 zastąpiono ten mechanizm adnotacjami.

Testowany interfejs

Poniżej zaprezentowany jest 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 powinny w pierwszej kolejności sprawdzać zachowanie implementacji interfejsów. Takie podejście jest zgodne z założeniami testowania tylko publicznych składowych API. Pozwala ono na sprawdzanie poprawności implementacji bez znajomości konkretnych klas implementujących. Interfejs bez zestawu testów jest niekompletny i nie powinien być przekazywany 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ć niepubliczne składowe ich API.

Założenia testu

Pisząc testy, należy przyjąć pewne założenia. Jeżeli wiemy, co dany interfejs ma robić, to należy określić przypadki typowe, przypadki błędne oraz przypadki graniczne i specyficzne.

Stwórzmy zatem klasę testową AbstractSumatorTest i dodając kolejne testy przyjrzyjmy się każdej z tych grup przypadków.

Klasa AbstractSumatorTest, 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 jedną metodę abstrakcyjną getSumator(), która będzie dostarczana wraz z implementacją, a jej zadaniem będzie zwrócenie instancji sumatora potrzebnej do testów. Wszystkie testy publicznych składowych API 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 minimalnym przypadku musi ją tylko rozszerzyć i dodać metodę fabrykującą instancję interfejsu. Zaletą takiego podejścia jest możliwość kontrolowania 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 na ten kod, należy zwrócić też uwagę na metody setUp() i tearDown(). Pierwsza z nich jest oznaczona adnotacją @Before; oznacza to, że zostanie wywołana przed każdym testem znajdującym się w tej klasie. Druga, oznaczona adnotacją @After, będzie wywoływana po każdym teście. Metody te powinny zwracać void, nie powinny rzucać wyjątków oraz nie mogą przyjmować parametrów i nie mogą być statyczne.

Istnieją jeszcze dwie inne ważne adnotacje. Adnotacją @BeforeClass oznaczana jest metoda, którą wywołujemy jednokrotnie przed wszystkimi testami w danej klasie. Analogicznie, metoda z adnotacją @AfterClass jest wywoływana jednokrotnie po wykonaniu wszystkich testów. Jedyną różnicą (w stosunku do poprzednich dwóch metod) jest to, że te ostatnie metody muszą być statyczne.

Wszystkie wspomniane metody służą do konfigurowania środowiska. Może być wiele metod oznaczonych taką samą adnotacją, ale jedyną gwarancją zachowania kolejności ich wywołania jest sposób, w jaki mechanizm refleksji zwróci je do invokera biblioteki JUnit. Nie ma nigdzie gwarancji zachowania kolejności.

Warto zwrócić uwagę na statyczny import na początku listy. Pozwoli on nam na używanie metod porównujących (assertXXX) tak jak metod "własnych" klasy. W JUnit 3 rozszerzaliśmy klasę TestCase i otrzymywaliśmy zestaw tych metod z tej klasy. W JUnit 4 nie ma potrzeby rozszerzania klasy, ale mechanizm statycznego importowania składników bardzo znacząco ułatwia życie i skraca kod.

Przypadki typowe

Zacznijmy zatem implementować testy. Źródłem informacji o testach jest dokumentacja interfejsu. Co do zasady, osoba, która tworzy interfejs, powinna go szczegółowo udokumentować. Interfejs bez dokumentacji jest nic niewarty. Klasa bez dokumentacji jest uciążliwa w obsłudze, ale mając jej kod źródłowy możemy się domyślać, jak działa. Natomiast w przypadku interfejsu bez dokumentacji nie wiemy nic o tym, jak ma działać, nawet posiadając jego kod źródłowy.

Pierwszym wymaganiem co do implementacji jest to, by pusty sumator zwracał 0. Stwórzmy zatem test sprawdzający ten warunek. Instancję sumatora mamy stworzoną "gratis" w metodzie setUp(), odpadł nam więc problem jej tworzenia. Dodatkowo możemy od razu wykorzystać gotowy obiekt. Test zatem będzie składać się tylko ze sprawdzenia jednego warunku:

//...
@Test
public final void testEmpty() {
    assertEquals(0, this.sumator.wielkość());
}
//...

W JUnit 4 definiujemy test poprzez oznaczenie metody za pomocą adnotacji @Test. Metoda jest ponadto oznaczona jako finalna. Ma to na celu uniemożliwienie jej przesłonięcia przez programistę. Zabezpiecza nas to przed "nieautoryzowaną" zmianą procedury testowej. Metoda testowa musi zwracać void, nie może przyjmować parametrów i nie powinna rzucać wyjątków. W JUnit 4 nie ma obostrzeń co do nazwy metody, ale warto stosować konwencję JUnit 3, ponieważ wiele starszych narzędzi do analizy kodu wykorzystuje konwencję JUnit 3 przy generowaniu raportów.

Kolejnymi typowymi przypadkami testowymi jest dodawanie liczby dodatniej, ujemnej i zera. Poniższy zestaw testów sprawdza te przypadki:

@Test                                         
public final void testAddZero() {             
    assertEquals(0, this.sumator.wielkość()); 
    this.sumator.dodaj(0);                    
    assertEquals(0, this.sumator.wielkość());   
}                                             

@Test                                         
public final void testAddOverZero() {         
    assertEquals(0, this.sumator.wielkość()); 
    this.sumator.dodaj(1);                    
    assertEquals(1, this.sumator.wielkość());   
}                                             

@Test                                         
public final void testAddUnderZero() {        
    assertEquals(0, this.sumator.wielkość()); 
    this.sumator.dodaj(-1);                   
    assertEquals(-1, this.sumator.wielkość());
}
@Test                                         
public final void testDivOver() {             
    assertEquals(0, this.sumator.wielkość()); 
    this.sumator.odejmij(0);                  
    assertEquals(0, this.sumator.wielkość()); 
}                                             
@Test                                         
public final void testDivOverZero() {         
    assertEquals(0, this.sumator.wielkość()); 
    this.sumator.odejmij(1);                  
    assertEquals(-1, this.sumator.wielkość());
}                                                                            

Wszystkie powyższe testy zbudowane są w jednakowy sposób. W pierwszym kroku sprawdzają, czy testowana instancja sumatora jest "świeża". Jeżeli tak, to następuje właściwy test. Wywoływana jest testowana metoda, po czym stan sumatora jest weryfikowany z założeniami.

Testowanie przypadków typowych jest wbrew pozorom trudne. Nigdy nie ma pewności, że otrzymany wynik będzie prawidłowy. Za chwilę zajmiemy się przypadkami granicznymi i specyficznymi. Na razie zajmijmy się przypadkami z założenia błędnymi.

Przypadki błędne

Teoretycznie nasz interfejs jest już przetestowany. Wiemy, że istnieją dane spełniające założenia teoretyczne, i że po przekazaniu ich naszej testowanej implementacji otrzymujemy prawidłowe wyniki. Co jednak w przypadku przekazania danych nieprawidłowych? Dobra implementacja uwzględnia takie przypadki i potrafi się przed nimi obronić.

W naszym przypadku mamy tylko jeden taki przypadek. Nie można do metody odejmij przekazać liczby ujemnej. W takim przypadku zostanie zwrócony wyjątek IllegalArgumentException. Napiszmy zatem test, który zweryfikuje ten przypadek.

@Test(expected=IllegalArgumentException.class) 
public final void testDivUnderZero() {         
    assertEquals(0, this.sumator.wielkość());  
    this.sumator.odejmij(-1);                  
}

Adnotacja @Test w tym przypadku przyjmuje jako argument expected klasę wyjątku, którego się spodziewamy. W przypadku biblioteki JUnit 3 test ten wyglądałby tak:

@Test(expected=IllegalArgumentException.class) 
public final void testDivUnderZero() {         
    assertEquals(0, this.sumator.wielkość());  
    try{
        this.sumator.odejmij(-1);                  
        fail();
    }
    catch(IllegalArgumentException iae){
        return;
        }
    fail();
}

Metoda fail() w pierwszym przypadku oznaczałaby niewyrzucenie wyjątku przez metodę odejmij() po przekazaniu nieprawidłowych danych. W drugim przypadku oznacza, że wyrzucony wyjątek jest innego typu niż spodziewany.

Przypadki graniczne i specyficzne

Przypadkiem granicznym nazywamy przypadek, w którym testowy zestaw danych może zostać zinterpretowany albo jako prawidłowy, albo nieprawidłowy. W naszym przypadku przypadkiem granicznym jest 0 dla metody odejmij(). Nietrudno sobie wyobrazić sytuację, w której programista pisząc warunek nie domknie przedziału, który ma być domknięty, lub też na odwrót – domknie przedział otwarty. Nasz przypadek jest bardzo prosty, ale przy testowaniu różnego typu walidatorów może okazać się, że pominęliśmy jakiś zestaw warunków brzegowych i nasz kod będzie zachowywał się nieprawidłowo.

Poniżej test sprawdzający warunek brzegowy:

@Test                                        
public final void testDivZero() {            
    assertEquals(0, this.sumator.wielkość());
    this.sumator.odejmij(0);                 
    assertEquals(0, this.sumator.wielkość());
}                                            

Nie różni się on od zwykłego testu, ale warto go odpowiednio udokumentować.

Szczególnym przypadkiem są specyficzne (czytaj: patologiczne) zestawy danych. Zazwyczaj są to dane, które są prawidłowe, ale ze względu na takie, a nie inne zasady działania komputerów, będą zwracały nieprawidłowe wyniki. Do tej grupy zaliczamy przede wszystkim wszelkie dane zmiennoprzecinkowe. W ich przypadku należy zastosować testy uwzględniające błędy zaokrągleń lub – jeżeli błędów chcemy uniknąć – zamienić operacje na nich na operacje na obiektach BigDecimal lub typach całkowitoliczbowych.

Inną grupą są dane z końca danego przedziału liczb. Poniższy test pokazuje, że operowanie na wartości Long.MAX_VALUE może dać niespodziewane wyniki:

@Test                                                         
public final void testAddOverLongMAX() {                      
    assertEquals(0, this.sumator.wielkość());                 
    this.sumator.dodaj(Long.MAX_VALUE);                       
    assertEquals(Long.MAX_VALUE, this.sumator.wielkość());    
    this.sumator.dodaj(1);                                    
    assertEquals(Long.MAX_VALUE + 1, this.sumator.wielkość());
    assertEquals(Long.MIN_VALUE, this.sumator.wielkość());    
}                                                             

Po dodaniu do sumatora wartości Long.MAX_VALUE, a następnie liczby 1, otrzymaliśmy wartość Long.MIN_VALUE. Jest to zachowanie prawidłowe. Nastąpiło przekroczenie zakresu, a tym samym zmiana znaku. Popatrzmy jednak na ten sam przypadek z punktu widzenia poniższego programu:

@Test                                                                             
public final void testAddOverLongMAXAndBigDecmal() {                              
    assertEquals(0, this.sumator.wielkość());                                     
    this.sumator.dodaj(Long.MAX_VALUE);                                           
    assertEquals(Long.MAX_VALUE, this.sumator.wielkość());                        
    this.sumator.dodaj(1);                                                        
    BigDecimal expected = (new BigDecimal(Long.MAX_VALUE)).add(new BigDecimal(1));
    assertTrue(expected.equals(new BigDecimal(this.sumator.wielkość())));         
}                                                                                 

W tym przypadku wartość spodziewana jest prawidłowa, ponieważ używamy klasy BigDecimal, ale porównanie zwróci fałsz w wyniku przekroczenia zakresu w sumatorze i zmiany znaku. Oczywiście ten przypadek jest banalny, ponieważ nawet początkujący programista powinien zauważyć niezgodność typów. Wyobraźmy sobie jednak, że sumator zwraca wynik nie o typie long, ale String. W tym przypadku nie wiemy, jak dokonywana jest operacja dodawania ani nie jesteśmy w stanie określić, czy dane, które przekazujemy, nie spowodują na przykład przekroczenia zakresu.

Testy usług zdalnych

Załóżmy, że nasz sumator jest dostarczany jako usługa zdalna. Jeżeli chcemy go przetestować, należy założyć, że test nie powiedzie się w przypadku, gdy odpowiedź nie nastąpi w określonym czasie. W tym celu należy użyć argumentu timeout adnotacji @Test, podając czas oczekiwania na odpowiedź w milisekundach:

@Test(timeout = 1000L)
public final void testEmpty() {
    assertEquals(0, this.sumator.wielkość());
}

Należy uwzględnić jednak nie tylko narzut komunikacji, ale też czas trwania operacji. Jeżeli zależy nam na minimalnym czasie komunikacji, to należy udostępnić usługę w ramach pętli lokalnej przez localhost/127.0.0.1.

Podsumowanie

Nie przedstawiłem tu wszystkich aspektów biblioteki JUnit. Pominąłem zagadnienia związane z grupowaniem klas testowych w większe zestawy oraz nie omówiłem sposobu wykorzystania JUnita do tworzenia aplikacji monitorujących. Nie omówiłem też postępowania przy testowaniu funkcjonalności, które wymagają obiektów o określonym stanie dostarczanych z zewnątrz. To ostatnie zagadnienie opisane jest w osobnym artykule poświęconym tak zwanym mockom.

Najważniejszą rzeczą, jaką należy zapamiętać z dziedziny testów jednostkowych, jest to, że testy mogą potwierdzić istnienie błędu w aplikacji, ale nigdy, w żadnym przypadku nie potwierdzą, że działa ona prawidłowo. Taką pewność możemy uzyskać dopiero poddając ją testom z wykorzystaniem wszystkich elementów dziedziny – zarówno prawidłowych, jak i nieprawidłowych, występujących we wszystkich możliwych kombinacjach. A i tak zawsze zostaje jeszcze niepewność związana z tym, że wykorzystujemy nie w pełni przetestowaną platformę JVM i system operacyjny.

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