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 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 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 test oraz do listy bibliotek bibliotekę JUnit 4. Struktura pakietów w katalogu test odpowiada strukturze z src. 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ę 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 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ą AbstractSumatorTest i dodając kolejne testy przyjrzyjmy się każdej z tych grup.

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 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 setUp() i tearDown(). Pierwsza z nich jest oznaczona adnotacją samp>@Before</samp oznacza to, że zostanie wywołana przed każdym testem znajdującym się w tej klasie. Druga oznaczona adnotacją samp>@After</samp będzie wywoływana po każdym teście. Metody te powinny zwracać void, nie powinny wyrzucać wyjątków oraz nie mogą przyjmować parametrów i nie mogą być statyczne.
Istnieją jeszcze dwie inne ważne adnotacje. samp>@BeforeClass</samp oznaczana jest metoda, którą wywołujemy jednokrotnie przed wszystkimi testami w danej klasie. Analogicznie samp>@AfterClass</samp jest wywoływana jednokrotnie po wykonaniu wszystkich testów. Jedyną różnicą jest to, że metody te muszą być statyczne.
Wszystkie te 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.
Zwróć 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. O ile klas bez dokumentacji jest uciążliwa w obsłudze to mając jej kod źródłowy możemy się domyślać jak działa. W przypadku interfejsu 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 "gratis" już stworzoną w metodzie setUp(), odpadł nam więc problem jej tworzenia, a 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 samp>@Test</samp. Metoda jest 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 zwracać 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 te 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 sprawdzany stan sumatora i 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ę takimi patologicznymi przypadkami. Na razie zajmijmy się przypadkami z założenia nieprawidłowymi.

Przypadki błędne

Teoretycznie nasz interfejs jest już przetestowany. Wiemy, że istnieją dane spełniające założenia teoretyczne i 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 samp>@Test</samp 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ła by nie wyrzucenie 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 jako prawidłowy albo nieprawidłowy. W naszym przypadku przypadkiem granicznym jest 0 dla metody odejmij(). Nie trudno sobie wyobrazić sytuację, w której programista pisząc warunek nie domknie przedziału, który ma być domknięty lub 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 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 Long.MAX_VALUE, a następnie 1 otrzymaliśmy Long.MIN_VALUE. Jest to prawidłowe zachowanie. 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 BigDecimal, ale w wyniku przekroczenia zakresu w sumatorze i zmiany znaku porównanie zwróci fałsz. 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 w nie w postaci long, ale String. W tym przypadku nie wiemy jak dokonywana jest operacja dodawania ani nie jesteśmy wstanie 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 samp>@Test</samp 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, ktore wymagają obiektów dostarczanych z zewnątrz o określonym stanie. To zagadnienie opisane jest w osobnym artykule poświęconym tak zwanym mockom.
Najważniejszą rzeczą jaką należy zapamiętać w dziedzinie 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ć 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 pewna niepewność związana z ty, że wykorzystujemy nie w pełni przetestowaną platformę JVM i systemu operacyjnego.

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