Artykuł ten opisuje podstawy testów jednostkowych z wykorzystaniem biblioteki JUnit 4. Jeśli zapoznasz się już z JUnit 4 zapraszam Cię do artykułu na temat testów z JUnit 5.
Po co testujemy oprogramowanie
Oczywista odpowiedź jest prosta – żeby nie było błędów :). Błędy powodują frustrację użytkowników, a to jest coś czego chcemy uniknąć. Ile razy chciałeś rzucić myszką/klawiaturą/laptopem jak coś nie działało jak powinno? Brzmi znajomo? ;)
Wszystkie powody testowania komercyjnego oprogramowania sprowadzają się do pieniędzy. Im wcześniej wykryjemy błąd, tym niższy jest koszt jego naprawienia. Pisanie testów jednostkowych pozwala wykryć błędy w najwcześniejszej możliwej fazie, w trakcie pisania kodu programu. Dlatego każdy porządny programista powinien testować kod, który napisze. Oddając kod do użytku powinien być pewny, że działa jak powinien.
Pojawia się tu jednak pewien problem. Manualne testowanie to żmudna, czasochłonna i mozolna praca. Bardzo tu łatwo o drobne przeoczenie kończące się błędem w programie. Do tego w projektach IT wymagania zmieniają się bardzo często więc takie testy także muszą być bardzo często przeprowadzane.
W związku z tym programiści testują swój kod pisząc testy jednostkowe.
Czym jest test jednostkowy
Test jednostkowy (ang. unit test) to sposób testowania programu, w którym wydzielamy mniejszą jego część, jednostkę i testujemy ją w odosobnieniu. W naszym przypadku taką jednostką do testowania może być pojedyncza klasa czy metoda, którą napiszemy.
Testy jednostkowe można pisać bez bibliotek zewnętrznych jednak jest to uciążliwe. Dodatkowo warto używać istniejących bibliotek ponieważ IDE dobrze się z nimi integrują. W tym artykule użyłem biblioteki JUnit.
Spójrz na fragment kodu poniżej. Klasa ta reprezentuje zakres liczb, ma ona jedną metodę, która sprawdza czy liczba przekazana jako argument należy do danego zakresu.
public class Range {
private final long lowerBound;
private final long upperBound;
public Range(long lowerBound, long upperBound) {
this.lowerBound = lowerBound;
this.upperBound = upperBound;
}
public boolean isInRange(long number) {
return number >= lowerBound && number <= upperBound;
}
}
Poniżej przykład prostego testu jednostkowego, który sprawdza czy, liczba 15 jest w zakresie liczb od 10 do 20.
@Test
public void shouldSayThat15rIsInRange() {
Range range = new Range(10, 20);
Assert.assertTrue(range.isInRange(15));
}
Test jednostkowy to metoda testująca naszą jednostkę, metodę w innej klasie z dodaną adnotacją @Test
. shouldSayThat15IsInRange
jest testem, wewnątrz którego tworzę instancję klasy Range
i wywołuję metodę sprawdzającą czy 15 jest wewnątrz zakresu.
Wynik tej metody jest przekazywany do metody Assert.assertTrue()
, jest to tak zwana asercja. Asercje to metody dostarczone przez bibliotekę JUnit, które pomagają przy testowaniu.
W naszym przykładzie, jeśli metoda isInRange
zwróci false
, wówczas asercja assertTrue
rzuci wyjątek, który przez IDE zostanie zinterpretowany jak test jednostkowy, który pokazuje błąd działania testowanego kodu. Mówimy wówczas, że „test nie przeszedł”, „wywalił się” :).
Testy jednostkowe łączymy w klasy z testami, bardzo często nazywamy je tak samo jak klasy, które testujemy dodając do nich Test
na końcu. W naszym przypadku klasa z testami dla klasy Range
nazywa się RangeTest
.
Pobierz opracowania zadań z rozmów kwalifikacyjnych
Przygotowałem rozwiązania kilku zadań algorytmicznych z rozmów kwalifikacyjnych. Rozkładam je na czynniki pierwsze i pokazuję różne sposoby ich rozwiązania. Dołącz do grupy ponad 6147 Samouków, którzy jako pierwsi dowiadują się o nowych treściach na blogu, a prześlę je na Twój e-mail.
Przykłady użycia asercji
Po co używać asercji? Otóż gotowe asercje tworzą komunikaty błędów (w trakcie testów jednostkowych), które ułatwiają znalezienie błędu. Komunikaty te są bardziej czytelne niż standardowy wyjątek AssertionError
1.
Asercje w bibliotece JUnit to nic innego jak metody statyczne w klasie Assert
. Poniżej przedstawię Ci kilka najczęściej stosowanych asercji2.
assertTrue
sprawdza czy przekazany argument totrue
,assertFalse
sprawdza czy przekazany argument tofalse
,assertNull
sprawdza czy przekazany argument tonull
,assertNotNull
sprawdza czy przekazany argument nie jestnull
em,assertEquals
przyjmuje dwa parametry wartość oczekiwaną i wartość rzeczywistą, jeśli są różne rzuca wyjątek,assertNotEquals
przyjmuje dwa parametry wartość oczekiwaną i wartość rzeczywistą, rzuci wyjątek jeśli są równe.
Importy statyczne
Tutaj drobna dygresja, w języku Java musimy importować klasy z innych pakietów, które chcemy użyć w definicji naszej klasy. Poza standardową konstrukcją ze słowem kluczowym import
istnieją także tak zwane importy statyczne.
Import statyczny pozwala na zaimportowanie metody/wszystkich metod statycznych znajdujących się w definicji jakiejś klasy. Proszę spójrz na przykład poniżej.
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.*;
W pierwszej linijce importujemy metodę assertFalse
z klasy Assert
, druga linijka to importowanie wszystkich metod statycznych z tej klasy. Dzięki takim importom później w definicji klasy nie musimy używać nazwy klasy używając danej metody statycznej:
assertFalse(false);
assertTrue(true);
Z racji tego, że dużo metod pomocniczych (na przykład asercje) w przypadku pisania testów to metody statyczne, bardzo często używamy tam importów statycznych.
Testowanie metod rzucających wyjątki
Czasami zdarza się, że chcemy przetestować pewną sytuację wyjątkową. Na przykład nie powinniśmy móc utworzyć instancji klasy Range
z niepoprawnymi argumentami.
public Range(long lowerBound, long upperBound) {
if (lowerBound > upperBound) {
throw new IllegalArgumentException("lowerBound is bigger than upperBound!");
}
this.lowerBound = lowerBound;
this.upperBound = upperBound;
}
Wywołanie konstruktora w teście z niepoprawnymi argumentami kończyłoby się od razu rzuceniem wyjątku, czyli testem jednostkowym, który nie przeszedł.
Z pomocą w takiej sytuacji przychodzi element expected
adnotacji @Test
. Przykład jego użycia widzisz poniżej:
@Test(expected = IllegalArgumentException.class)
public void shouldThrownIllegalArgumentExceptionOnWrongParameters() {
new Range(20, 10);
}
Taki test jednostkowy nie przejdzie jeśli wyjątek nie zostanie rzucony. Mimo tego, że w teście nie ma żadnej asercji testuje on właśnie rzucenie wyjątku.
Istnieje też inny sposób. Możesz go użyć jeśli chcesz mieć dostęp do instancji rzuconego wyjątku. Pokazałem go w przykładzie poniżej:
@Test
public void shouldHaveProperErrorMessage() {
try {
new Range(20, 10);
fail("Exception wasn't thrown!");
}
catch (IllegalArgumentException exception) {
assertEquals("lowerBound is bigger than upperBound!", exception.getMessage());
}
}
Użyta tu statyczna metoda Assert.fail()
powoduje zakończenie testu niepowodzeniem. Zostanie ona wywołana wyłącznie jeśli wyjątek nie zostanie rzucony.
Przygotowanie testów i cykl życia testów
Czasami zdarza się, że kilka testów jednostkowych wymaga pewnego „przygotowania”. Na przykład trzeba utworzyć instancję, którą będziemy później testowali. Twórcy biblioteki JUnit przyszli nam z pomocą. Istnieje adnotacja @Before
, którą możemy dodać do metody w klasie z testami. Metoda ta zostanie uruchomiona przed każdym testem jednostkowym. Proszę spójrz na przykład poniżej.
public class RangeTest {
private Range range;
@Before
public void setUp() {
range = new Range(10, 20);
}
@Test
public void shouldSayThat15rIsInRange() {
assertTrue(range.isInRange(15));
}
@Test
public void shouldSayThat5IsntInRange() {
assertFalse(range.isInRange(5));
}
}
W naszym przykładzie metoda setUp
zostanie wywołana przed uruchomieniem każdego z testów. Dzięki temu nie musimy tworzyć instancji wewnątrz testu. Odpowiednie użycie tej adnotacji pomaga pisać krótsze testy jednostkowe.
Cykl życia klasy z testami jednostkowymi
Adnotacja @Before
jest jedną z czterech adnotacji, które pozwalają na wykonanie fragmentów kodu przed/po testach. Pozostałe trzy to:
@After
– metoda z tą adnotacją uruchamiana po każdym teście jednostkowym, pozwala na „posprzątanie” po teście,@AfterClass
– metoda statyczna z tą adnotacją uruchamiana jest raz po uruchomieniu wszystkich testów z danej klasy,@BeforeClass
– metoda statyczna z tą adnotacją uruchamiana jest raz przed uruchomieniem pierwszego testu z danej klasy.
Proszę spójrz na przykład poniżej:
public class TestLifecycle {
@Before
public void setUp() {
System.out.println("set up");
System.out.flush();
}
@After
public void tearDown() {
System.out.println("tear down");
System.out.flush();
}
@BeforeClass
public static void setUpClass() {
System.out.println("set up class");
System.out.flush();
}
@AfterClass
public static void tearDownClass() {
System.out.println("tear down class");
System.out.flush();
}
@Test
public void test1() {
System.out.println("test 1");
System.out.flush();
}
@Test
public void test2() {
System.out.println("test 2");
System.out.flush();
}
}
Jeśli uruchomisz tę klasę na konsoli pojawi się:
set up class
set up
test 1
tear down
set up
test 2
tear down
tear down class
Testy jednostkowe a testy automatyczne
Testy jednostkowe bardzo często są testami automatycznymi. Test automatyczny to taki, który możemy wykonywać automatycznie :) Zaletą takiego podejścia jest to, że w momencie zmiany kodu możemy raz napisany test uruchomić ponownie wiedząc od razu czy napisany wcześniej fragment działa poprawnie czy nie. Pomagają przy tym wcześniej omówione asercje.
Bardzo często testy jednostkowe uruchamiane są automatycznie podczas pracy nad projektem. Służą do tego osobne środowiska, w których testy te są uruchamiane.
Istnieją także mechanizmy, które w trakcie pracy programisty wykrywają zmiany w części klas i automatycznie uruchamiają dla tych klas testy jednostkowe informując programistę o wynikach. Dzięki temu bardzo szybko jesteśmy w stanie dowiedzieć się czy zmiany, które wprowadziliśmy nie popsuły wcześniejszej funkcjonalności.
Dobre praktyki przy pisaniu testów
Poniżej postaram się zebrać dla Ciebie kilka dobrych praktyk, do których warto się stosować w czasie pisania testów:
- Po pierwsze, pisz testy jednostkowe. Koniecznie. Zawsze.
- Staraj się pisać testy jednostkowe, które są małe i dotyczą małego wycinka funkcjonalności. Później o wiele łatwiej jest zrozumieć taki test.
- Nadawaj metodom z testem nazwy, które pomagają zrozumieć co dany test powinien sprawdzić.
- Kolejność testów jednostkowych w klasie nie powinna mieć znaczenia. Innymi słowy nie możemy polegać na tym, że jako pierwszy musi się uruchomić
test1
a po nimtest2
. Testy uruchomione w odwrotnej kolejności także powinny mieć dokładnie taki sam efekt. - Pisz testy jednostkowe tak, żeby nie zależały od Twojej lokalnej konfiguracji. Na przykład test jednostkowy czytający plik z Twojego dysku z katalogu
C:\mój\katalog\domowy
(czy/home/uzytkownik
) nie jest dobrym rozwiązaniem. - Pisz testy jednostkowe niezależne od zewnętrznych systemów. Innymi słowy testuj tylko „jednostkę”, nic ponadto. Jeśli klasa, którą testujesz potrzebuje dostępu np. do bazy danych użyj mocka czy stuba do jej zastąpienia w trakcie testów3 .
- Testuj warunki brzegowe i sytuacje wyjątkowe. Załóżmy, że masz metodę, która przyjmuje tablicę, która musi mieć maksymalnie trzy elementy. Napisz kilka testów:
- przekazując
null
zamiast tablicy, - przekazując pustą tablicę,
- przekazując tablicę z trzema elementami,
- przekazując tablicę z czterema elementami.
- przekazując
Dzięki takim testom będziesz pewien, jak zachowuje się Twoja metoda w sytuacjach wyjątkowych.
- Testowany kod nie powinien być w tym samym miejscu, w którym są testy. Sprowadza się to do tego, że kod umieszczamy w katalogu np.
src
, testy natomiast w katalogutest
. Oba katalogi pod spodem mają odpowiednią strukturę odzwierciedlającą pakiety. Jest to ważne ponieważ później przy większych projektach testy nie „mieszają się” z kodem programu. - Staraj się pisać testy, które są szybkie. Przy pierwszych programach nie jest to problemem, jednak przy większych projektach uruchamianie testów może być czasochłonne.
- Uruchamiaj testy jednostkowe możliwie często. Uwierz mi, to Ci się opłaci :). Punkt ten jest powiązany z punktem poprzednim – nie będziesz uruchamiał często testów, które trwają długo.
- Jeśli zauważysz, że część testów jednostkowych wymaga dokładnie takiego samego „przygotowania” wydziel je do osobnej klasy i użyj metod z adnotacją
@Before
lub@BeforeClass
.
Testy jednostkowe w IntelliJ Idea
Zacznijmy od utworzenia testu jednostkowego dla istniejącej klasy. Z pomocą przychodzi skrót klawiaturowy <Ctrl + Shift + T>
– naciśnij tę kombinację na nazwie klasy dla której chcesz utworzyć test. Pokaże się wówczas dialog pomagający utworzyć nową klasę testu.
IntelliJ jest na tyle mądry, że wykrywa brak biblioteki JUnit w projekcie. W oknie dialogowym widać wówczas przycisk „Fix it”, który automatycznie dodaję tę bibliotekę.
Kolejnym skrótem klawiaturowym, który może się przydać podczas pisania testów jednostkowych jest <Alt + Insert>
, naciśnięcie tego skrótu wewnątrz klasy grupującej testy pozwala nam w łatwy sposób stworzyć kolejny test.
W końcu kombinacja <Ctrl + Shift + F10>
pozwala na uruchomienie testów jednostkowych wewnątrz IDE. W zależności od tego na czym znajduje się nasz kursor myszy, ten skrót klawiaturowy może uruchomić pojedynczą metodę z testem, klasę grupującą testy czy pakiet z kilkoma klasami testowymi.
Zadanie do rozwiązania
Wykonanie zadania wymaga podstawowej znajomości kolekcji. Jeśli do tej pory nie udało Ci się pracować z kolekcjami zachęcam do przeczytania poświęconego im artykułu.
Napisz program, który będzie reprezentował koszyk w sklepie internetowym. Do koszyka reprezentowanego przez klasę Basket
możemy dodawać bądź usuwać kolejne przedmioty. Każdy przedmiot powinien mieć nazwę i cenę jednostkową. Koszyk powinien także pozwalać na dodanie/usunięcie od razu kilku egzemplarzy przedmiotu ze sklepu. Koszyk powinien także być w stanie policzyć sumaryczną wartość zamówienia oraz wyświetlić swoją zawartość. Pamiętaj o poprawnym obsłużeniu sytuacji wyjątkowych np. usunięcie elementów z pustego koszyka czy dodaniu ujemnej liczby przedmiotów.
Napisz zestaw testów jednostkowych potwierdzających poprawne działanie Twojego koszyka z zakupami.
Drobna podpowiedź z przykładowym zestawem klas, które mogą rozwiązać ten problem:
Item
, która posiada dwa atrybutydouble price
4 orazString name
,Basket
, który posiada atrybutMap orderedItems
reprezentujący zamówione towary wraz z ich ilością.
Przygotowałem też przykładowe rozwiązanie, znajduje się w repozytorium na githubie wraz z zestawem testów jednostkowych. Zachęcam jednak do samodzielnej próby rozwiązania zadania. Uwierz mi, że wtedy nauczysz się najwięcej :).
Jeśli nie udało Ci się wcześniej poznać klasy StringBuilder
zachęcam Cię do rzucenia okiem na artykuł na temat String
cache i klasy StringBuilder
. Jej znajomość nie jest konieczna, jednak użyłem jej w moim przykładowym rozwiązaniu.
Dodatkowe materiały do nauki
- Test jednostkowy na Wikipedii
- Strona biblioteki JUnit
- Dokumentacja biblioteki JUnit
- Kod źródłowy przykładów użytych w artykule
Podsumowanie
W artykule przeczytałeś o testach jednostkowych. Poznałeś zestaw dobrych praktyk dotyczących pisania testów, nauczyłeś się podstaw biblioteki JUnit. Wiesz czym jest test automatyczny i dlaczego takie testy są istotne. Całość przećwiczyłeś w sposób praktyczny rozwiązując zadanie końcowe.
Na koniec mam do Ciebie prośbę. Zależy mi na dotarciu do jak największej liczby czytelników – proszę podziel się linkiem do artykułu ze znajomymi. Jeśli nie chcesz ominąć kolejnych artykułów możesz polubić moją stronę na Facebook’u ;). Do następnego razu!
-
W języku Java istnieje także słowo kluczowe
assert
, po którym musi wystąpić wartość logiczna, jeśli jest ona fałszem kończy się to rzuceniem wyjątkuAssertionError
– np.assert false
rzuci wyjątek. ↩ -
Pominę tutaj metodę
assertThat
, którą omówię bardziej szczegółowo w kolejnych artykułach. ↩ -
O mockach czy stubach przeczytasz w kolejnych artykułach, jeśli jest to Twoja pierwsza styczność z testami możesz ten punkt pominąć. ↩
-
double
nie jest dobrym typem do reprezentowania cen, na potrzeby tego przykładu jednak wystarczy. Dlaczego tak się dzieje przeczytasz w osobnym artykule. ↩
Pobierz opracowania zadań z rozmów kwalifikacyjnych
Przygotowałem rozwiązania kilku zadań algorytmicznych z rozmów kwalifikacyjnych. Rozkładam je na czynniki pierwsze i pokazuję różne sposoby ich rozwiązania. Dołącz do grupy ponad 6147 Samouków, którzy jako pierwsi dowiadują się o nowych treściach na blogu, a prześlę je na Twój e-mail.
Zostaw komentarz