JUnit - Testy jednostkowe w Javie
Spis treści
Wprowadzenie do JUnitów
Przez całe studia napisałem już sporo aplikacji, lecz zawsze pomijałem aspekt ich testowania. Muszę się przyznać, że dopiero od niedawna zacząłem używać JUnitów, które wbrew pozorom znacznie przyspieszają pracę.
JUnit to test jednostkowy, który docelowo sprawdzają, czy dana funkcjonalność działa zgodnie z jej przeznaczeniem. Dobrą praktyką jest dzielenie JUnitów, tak aby testowały one tylko wybraną metodę lub funkcjonalność, zaczynając od tych najprostszych, do coraz bardziej zaawansowanych.
Instalacja
Jeśli korzystamy z eclipse, to nie jest wymagana żadna dodatkowa wtyczka, ani biblioteka. Jeśli natomiast korzystasz z innego narzędzia, będziesz musisz sobie niestety poradzić samodzielnie :)
Przykład użycia - gra w życie
Pokażę Wam przykład podejścia do JUnitów na przykładzie gry w życie. Opis gry dostępny jest na wikipedii.
Gra odbywa się w turach, na nieskończenie wielkiej planszy. Każde pole planszy może być "żywe" albo "martwe". Stan pola może zmienić się po turze w zależności od stanu jego sąsiadów:
-
Jeśli żywa komórka ma mniej niż 2 sąsiadów ginie z samotności
-
Jeżeli żywa komórka ma 2 albo 3 żywych sąsiadów przeżyje do następnej tury
-
Jeżeli żywa komórka ma więcej niż 3 sąsiadów ginie z przeludnienia
-
Jeśli martwa komórka ma dokładnie 3 sąsiadów, ożywa
Bardzo proste zasady, świetnie można zastosować dla nich JUnity. Implementacji może być całkiem sporo, niżej przestawią przykładową.
Cell.java
package game;
public class Cell implements Cloneable {
private boolean alive;
public Cell(boolean alive) {
this.setAlive(alive);
}
public boolean isAlive() {
return alive;
}
public void setAlive(boolean alive) {
this.alive = alive;
}
@Override
public Cell clone() {
return new Cell(alive);
}
public void changeState(int neighboursCount) {
if (alive) {
if (neighboursCount < 2) {
alive = false;
} else if (neighboursCount > 3) {
alive = false;
}
} else {
if (neighboursCount == 3) {
alive = true;
}
}
}
}
Board.java
package game;
public class Board {
private Cell[][] cells;
private int width;
private int height;
public Board(int width, int height) {
this.width = width;
this.height = height;
cells = new Cell[width][height];
resetAll();
}
// reset calej planszy
private void resetAll() {
for (int i = 0; i < width; i++) {
for (int j = 0; j < height; j++) {
cells[i][j] = new Cell(false);
}
}
}
// ustawienie wartosci komorki
public void setCellValue(int x, int y, boolean isAlive) {
if (x >= 0 && y >= 0 && x < width && y < height) {
cells[x][y].setAlive(isAlive);
} else {
throw new IndexOutOfBoundsException();
}
}
// pobranie wartosci komorki
public boolean getCellValue(int x, int y) {
if (x >= 0 && y >= 0 && x < width && y < height) {
return cells[x][y].isAlive();
} else {
throw new IndexOutOfBoundsException();
}
}
// wykonanie aktualnej tury
public void nextCycle() {
// kopiowanie aktualnego stanu
Cell[][] newBoard = new Cell[width][height];
for (int i = 0; i < width; i++) {
for (int j = 0; j < height; j++) {
newBoard[i][j] = cells[i][j].clone();
}
}
// wykonanie akcji dla danej komorki
for (int i = 0; i < width; i++) {
for (int j = 0; j < height; j++) {
int neighboursCount = countAliveNeighbours(i, j);
newBoard[i][j].changeState(neighboursCount);
}
}
cells = newBoard;
}
// liczenie zywych sasiadow
public int countAliveNeighbours(int i, int j) {
int startX = Math.max(i - 1, 0);
int startY = Math.max(j - 1, 0);
int endX = Math.min(i + 1, width - 1);
int endY = Math.min(j + 1, height - 1);
int aliveNeighbours = 0;
for (int x = startX; x <= endX; x++) {
for (int y = startY; y <= endY; y++) {
if (cells[x][y].isAlive()) {
aliveNeighbours++;
}
}
}
// nie liczymy komorki ktorej sasiadow sprawdzamy
if (cells[i][j].isAlive()) {
aliveNeighbours--;
}
return aliveNeighbours;
}
}
Kod programu raczej mało skomplikowany i nie wymaga dalszego komentarza. Teraz przejdźmy do jego testowania.
Stwórz nowy pakiet dla testów, kliknij na niego prawym przyciskiem myszy i wybierz New->Other...
Wygenerowana zostanie klasa z jedną metodą. Sami musimy stworzyć odpowiednie metody testujące.
Testy muszą spełnić zasady:
- oznaczone znacznikiem @org.junit.Test
- oznaczone jako metody publiczne (public)
- zaczynać się od test
Korzystamy z asercji takich jak: assertEquals(), assertNotEquals(), assertNull(), assertNotNull() itd. Metod tych jest naprawdę sporo, a ich nazwy dosyć łatwo skojarzyć z funkcją którą pełnią.
Poniżej moja klasa testowa.
Test.java
package test;
import game.Board;
import junit.framework.Assert;
public class Test {
@org.junit.Test
public void testCreate() {
int width = 10;
int height = 10;
Board board = new Board(width, height);
Assert.assertNotNull(board);
for (int i = 0; i < width; i++) {
for (int j = 0; j < height; j++) {
Assert.assertEquals(false, board.getCellValue(i, j));
}
}
}
@org.junit.Test
public void testSetValue() {
Board board = new Board(10, 10);
board.setCellValue(5, 5, true);
Assert.assertEquals(true, board.getCellValue(5, 5));
board.setCellValue(0, 0, true);
Assert.assertEquals(true, board.getCellValue(0, 0));
board.setCellValue(0, 1, true);
Assert.assertEquals(true, board.getCellValue(0, 1));
board.setCellValue(1, 0, true);
Assert.assertEquals(true, board.getCellValue(1, 0));
board.setCellValue(1, 1, true);
Assert.assertEquals(true, board.getCellValue(1, 1));
}
@org.junit.Test
public void testDieLonely() {
// zero sasiadow
Board board = new Board(5, 5);
board.setCellValue(0, 0, true);
Assert.assertEquals(0, board.countAliveNeighbours(0, 0));
board.nextCycle();
Assert.assertEquals(false, board.getCellValue(0, 0));
// jeden sasiad
Board board2 = new Board(5, 5);
board2.setCellValue(0, 0, true);
board2.setCellValue(0, 1, true);
Assert.assertEquals(1, board2.countAliveNeighbours(0, 0));
board2.nextCycle();
Assert.assertEquals(false, board2.getCellValue(0, 0));
}
@org.junit.Test
public void testSurvive() {
// dwoch sasiadow
Board board2 = new Board(5, 5);
board2.setCellValue(0, 0, true);
board2.setCellValue(0, 1, true);
board2.setCellValue(1, 0, true);
Assert.assertEquals(2, board2.countAliveNeighbours(0, 0));
board2.nextCycle();
Assert.assertEquals(true, board2.getCellValue(0, 0));
// trzech sasiadow
Board board3 = new Board(5, 5);
board3.setCellValue(0, 0, true);
board3.setCellValue(0, 1, true);
board3.setCellValue(1, 0, true);
board3.setCellValue(1, 1, true);
Assert.assertEquals(3, board3.countAliveNeighbours(0, 0));
board3.nextCycle();
Assert.assertEquals(true, board3.getCellValue(0, 0));
}
@org.junit.Test
public void testDieOverpopulation() {
// czterech sasiadow
Board board = new Board(5, 5);
board.setCellValue(2, 2, true);
board.setCellValue(2, 1, true);
board.setCellValue(2, 3, true);
board.setCellValue(1, 2, true);
board.setCellValue(3, 2, true);
Assert.assertEquals(4, board.countAliveNeighbours(2, 2));
board.nextCycle();
Assert.assertEquals(false, board.getCellValue(2, 2));
}
@org.junit.Test
public void testRessurection() {
Board board = new Board(5, 5);
board.setCellValue(2, 2, false);
board.setCellValue(2, 1, true);
board.setCellValue(2, 3, true);
board.setCellValue(1, 2, true);
Assert.assertEquals(3, board.countAliveNeighbours(2, 2));
board.nextCycle();
Assert.assertEquals(true, board.getCellValue(2, 2));
}
}
Jak zapewne zauważyliście zaczynam od testowania funkcji podstawowych, i przechodzę do testowania tych bardziej zaawansowanych (chociaż trudno mówić o zaawansowaniu w tym przypadku). Warto zwrócić uwagę na to, że dla każdej zasady gry tworzę osobny test sprawdzający jej poprawność.
Dobre praktyki, Test Driven Development
Doświadczenie zdobyte w firmie w której pracuję pomogło mi lepiej zrozumieć sposób w jaki powinny być tworzone JUnity.
- Każdy test powinien być niezależny od innego. W przykładzie powyżej możecie zauważyć, że dla każdego testu tworzę osobną planszę. Ma to na celu uniknięcie sytuacji w której mała zmiana w poprzednim teście powoduje wywalenie się testu który korzystał z tych samych danych.
- Często dochodzi do przypadków, gdzie danych testowych nie jesteśmy w stanie wypisać "z ręki" tak jak w przykładzie. Korzystamy wtedy z danych z bazy danych, serwera itd. Tutaj starajmy się zakładać, że nie wiemy ile tych danych będzie. Niepoprawne jest założenie, że powinno być np. 2 testowe konta użytkowników i sprawdzając ich unikatowość wystarczy je porównać. Co w przypadku, gdy ktoś doda, bądź usunie dane z bazy? Test się wywali i będzie trzeba szukać tego przyczyny, co czasami może być bardzo czasochłonne.
Można również pomyśleć o zastosowaniu TDD (Test Driven Development). Polega to w skrócie na tym, że najpierw koncentrujemy się na pisaniu JUnitów, a następnie zajmujemy się ich implementacją. Trochę ciężko się na przestawić, głównie ze względu na to, że pisanie testów jest żmudne i nudne, a w przypadku nowych projektów od razu się chce coś tworzyć. Mogę jednak z własnego doświadczenia zapewnić, że na dłuższą metę takie rozwiązanie jest zdecydowanie bardziej opłacalne nie tylko pod względem zaoszczędzonego czasu, ale przede wszystkim nerwów przy debugowaniu jakieś literówki.
Tyle na temat JUnitów z mojej strony. Jest to oczywiście bardzo obszerny temat, ale sądzę, że na początek taki krótki zarys wystarczy.
Dyskusja i komentarze
Masz pytania do tego wpisu? Może chcesz się podzielić spostrzeżeniami? Zapraszamy dyskusji na naszej grupie na Facebooku.
Poniżej znajdziesz archiwalne wpisy z czasów, gdy strona była jeszcze hobbystycznym blogiem.
Oskar
Bardzo ciekawy temat, chociaż pewnie musi trochę poczekać zanim zyska na popularności, gdyż większość ludzi, którzy odwiedzają ten serwis mało ma wspólnego z programowaniem, więc testowanie trochę mało ich interesuje.. bardziej czy coś działa czy nie.
Marcin Kunert
Dokładnie tak. Do testowania trzeba dojrzeć. Sam dopiero od niedawna się do tego przekonuję.
Damian
Bardzo fajna sprawa, zaoszczędzi sporo czasu i nerwów :). Dzięki!
Bartek
Gdy wykonałem test powyższego programu to wyskoczyły mi błedy w metodzie resetAll. Dokładnie w lini: cells[i][j] = new Cell(false); Screen z pulpitu: http://s22.postimg.org/8nfwywxrl/blad.png PS. nie pisze sie Ressurection tylko Resurrection
Bartek
Blad znalazlem, w drugim forze mialem "i" zamiast "j".
kaine
Wszystko wygląda ładnie i pięknie z TDD i unit testami jeśli przychodzi nam implementować metody mające dobrze zdefiniowany output (najlepiej - skalarne wartości). Wtedy łatwo jest napisać test - zainicjować obiekt, wywołać metodę i parę assertów. Niestety większość metod pisanych w realnych aplikacjach nie dostarcza takiej wygody. W rzeczywistych aplikacjach metoda będąca obiektem testu zwykle wywołuje parę innych metod i zwraca całe void. Jak wtedy napisać test? Jak przetestować wyjście? Pisać produkcyjne metody specjalnie na potrzeby testów? Bez mockowania się nie obejdzie. Jeśli zastosujemy paskudne mockowanie to z kolei zaszywamy logikę metody wewnątrz testu (bo tworząc mocki zakładamy, że dane metody / klasy zostaną użyte wewnątrz metody testowanej). Później refaktoryzując "pięknie" otestowaną metodę musimy niestety poprawiać też testy a to przeczy zupełnie jednej z fundamentalnych zalet unit testów - ułatwienia refaktoryzacji. Skoro nie można refaktoryzować bez poprawiania testów to czy taki test jest do czegoś potrzebny? Dla mnie osobiście unit testy to mit. Zobaczcie sobie na książki o unit testach. Czy któraś z nich na swoich kartach podaje bardziej skomplikowany przykład wykraczający ponad testowanie klasy realizującej zadania dodawania i mnożenia liczb całkowitych? Otóż - nie. A dlaczego? Ponieważ nie da się napisać unit testu do metody jaką opisałem parę zdań wyżej, a właśnie takie są zwykle metody w realnych aplikacjach. Ktoś może napisać, że możemy przecież mockować. Mockowanie to niestety testowanie programisty a nie faktycznego działania metody. Mija się zupełnie z celem. Poza tym - jak można testować klasy jednostkowo? Czy mając pewność, że każda klasa działa jednostkowo ich połączenie w kompletny program będzie działać równie poprawnie? Nie ma takiej pewności. O wiele więcej pożytku jest z testów integracyjnych. I takie też polecam pisać, a z unit testów lepiej zrezygnować gdyż jest to strata czasu i sprzeczność sama w sobie. Dziękuję za uwagę i pozdrawiam:)
&dzej
nie wiem czy przeglądałeś inne działy z tej strony ale widac że jest to na dość prymitywnym poziomie. Ogólnie obecnie jedynie co się robi to breakingpointy i odpowiednie logowanie kodu i te logi się dopiero "testuje". A podrugie strona nie jest żadnym ewenementem a raczej jest to źródło dla leniwych studentów, przecież ona jedynie próbuje tłumaczyć jakieś anglojęzyczne poradniki a w niektórych miejscach nawet dokumentacje WOW.
&dzej
a btw. jest coś takiego jak Selenium i WebDrive więc o czym mowa...
Sławek Ludwiczak
Zawsze się cieszę, gdy z otchłani internetu na naszą bezużyteczną stronę. Nie wiem co masz na myśli pisząc o "brakingpointach" i testowaniu, testy jednostkowe nie mają wiele związku z testowaniem na wyższym poziomie (Selenium i "WebDrive"). Rzucanie pustymi frazesami i słowami, których się nie rozumie, które przeczytało się w internecie naprawdę widać i aż kłuje w oczy. Nie wiem też co masz na myśli pisząc o specjaliście SOA, bo żaden z autorów tej strony nim nie jest. Pozdrawiam serdecznie i zachęcam do dalszego udzielania "porad". Rozumiem, że strona nie każdemu musi przypaść do gustu, natomiast zupełnie nie rozumiem dlaczego w takim razie nie szkoda Ci cennego czasu, żeby tu zaglądać i się udzielać. BTW wcześniej nie widziałem, ale bardzo fajny komentarz @kaine - dzięki!
futw
Odpowiedział specjalista SOA ! what a bullshit! Typowa cebula nie napisze jak cam coś robi tylko dowali komuś bo napisał z czym ma doświadczenie w branży. Jak ty testujesz soft? Pochwal się, JUnitem? ? WTF! Co do reszty zawartości strony i mojej nie skromnej uwagi, no chyba nie powiesz że to nie jest prawdą? Przepisujecie po prostu tech doc-a Javy i Andka + jakiś przykład centralnie bezużyteczny.
Marcin Kunert
Zastanawiam się czy trollujesz, czy faktycznie jesteś przekonany do tego co wypisujesz. SOA - service oriented architecture, natomiast Sławek jest specjalistą od SEO, ale do rzeczy. Etapy testowania: 1. Algorytmy i podstawowe funkcjonalności testuje się JUnitami, pisane najczęściej przez osobę która tworzyła implementację. Ciekawe jest również podejście TDD, gdzie najpierw pisze się testy, a później implementację. 2. Tam gdzie nie jest możliwe napisanie JUnitów bez wykorzystania zewnętrznych serwisów korzysta się z tzw. mocków. 3. Testy funkcjonalne i integracyjne opierają się na testach GUI (tutaj czasami można również użyć wspomnianego selenium) Odnoszą się do breakingpointów - z taką nazwą się nie spotkałem, pewnie chodziło o breakpointy. Są to zatrzymania wykonywania działania programu, służą do debugowania, a nie testowania kodu. Logów się nie testuje, można je analizować, poszukując przyczyny błędu. Jeśli chodzi o treść strony, to faktycznie - korzystamy z dokumentacji Javy i Androida. Nie wymyślamy nowych języków programowania, tylko staramy się dobrze wytłumaczyć to co znamy. Treści dostępne są za darmo, poświęciliśmy sporo czasu na ich stworzenie, zawsze jednak musi się znaleźć ktoś taki, kto narzeka (tutaj nawiązanie do "typowej cebuli").
Kamil
Fajnie się czytało, ale gdy zacząłem pisać to okazało się, że "The type Assert is deprecated". Czy to oznacza, że przedstawiona tutaj metoda jest już przestarzała?
Sławek Ludwiczak
W Nowej bibliotece JUnita zaimportuj klasę z innego pakietu, konkretnie org.junit.Assert - reszta bez zmian.