Maj 15, 2019

Wstęp do testów jednostkowych

Seria o testach jednostkowych

  1. Wstęp do testów jednostkowych
  2. Test Driven Development w praktyce
  3. Czy dependency injection ułatwia testowanie?
  4. Mocki w testach jednostkowych
  5. Mockito spy

 

Testy jednostkowe

W inżynierii oprogramowania – czyli całym cyklu wytwarzania oprogramowania wyróżniamy wiele typów testów. Oprogramowanie testujemy na wielu etapach, aby dostarczyć klientowi, użytkownikowi końcowemu aplikację w jak najlepszej jakości. Jak najmniej podatną na przeróżne błędy – błędy, których i tak nigdy nie da się uniknąć, ale za to można zmniejszyć ich ilość.

Jednym z rodzajów testów są właśnie testy jednostkowe. Są to testy, które często piszemy jeszcze przed napisaniem właściwego kodu aplikacji. Są to najbardziej podstawowe testy, jakie programista może stworzyć – dlaczego? Już tłumaczę.

Przeznaczeniem testów jednostkowych jest sprawdzenie działania tylko jednej małej jednostki w aplikacji – tą jednostką jest po prostu metoda. Każda metoda powinna być traktowana jak czarna skrzynka – nie powinno interesować nas co się dzieje w środku, interesuje nas tylko co „wchodzi” do metody oraz co z niej „wychodzi”. Bardzo ważne jest to, że jeden test jednostkowy powinien sprawdzać działanie dokładnie jednej metody.

Weźmy za przykład metodę, która ma za zadanie zsumować dwie liczby – przetestujmy ją. Jak będzie wyglądał taki test? Na początku musimy w głowie sobie ułożyć „scenariusz” testu – czyli na początku musimy wymyśleć dwie liczby, następnie podać je do metody sum i sprawdzić czy logika metody dodaje obie liczby poprawnie. Okres weryfikacji wyniki jest banalny – wystarczy porównać wynik z metody z tym oczekiwanym w naszym scenariuszu testu.

Jeśli nie do końca wyobraziłeś sobie powyższy teoretyczny przykład to spróbujmy go zaimplementować. Bez żadnych dodatkowych bibliotek lub innych udziwnień.

Na samym początku mamy naszą metodę sum.

public double sum(double a, double b) {
    return a + b;
}

Napiszmy teraz jeden test do tej metody. Według wcześniejszego przykładu musimy teraz wybrać dwie liczby – ja wybiorę liczby 2 oraz 3.

public static void main(String[] args) {
    SumMainTest main = new SumMainTest();
    
    double a = 2;
    double b = 3;
}

Następnie muszę wywołać testowaną metodę z wybranymi argumentami.

double result = main.sum(a, b);

A na koniec porównać wyniki – wynik z metody z wynikiem oczekiwanym przeze mnie czyli z liczbą 5.

if (result == 5d) {
    System.out.println("Test zaliczony!");
} else {
    System.out.println("Test niezaliczony!");
}

I cały nasz kod wygląda tak:

public class SumMainTest {
    public double sum(double a, double b) {
        return a + b;
    }

    public static void main(String[] args) {
        SumMainTest main = new SumMainTest();

        double a = 2;
        double b = 3;

        double result = main.sum(a, b);

        if (result == 5d) {
            System.out.println("Test zaliczony!");
        } else {
            System.out.println("Test niezaliczony!");
        }
    }
}

Dzięki prostemu mechanizmowi instrukcji warunkowej if udało nam się przetestować metodę sum. Skoro już wiesz jak wyglądają testy jednostkowe to możemy pójść krok dalej…

Zalety

Osobiście lubię wykonywać czynności, które mają jakiś cel – pewne uzasadnienie po co coś takiego w ogóle robimy. Poniżej przedstawię Ci zalety jakie widzę w testowaniu jednostkowym.

Prostsze znajdowanie błędów w kodzie

Przypuśćmy, że mamy spory flow w naszej aplikacji np. wywołujemy po kolei 10-15 różnych metod, a końcowy wynik tego flow jest niepoprawny. Co musimy wtedy robić? Musimy wtedy uruchomić debugger lub zasypać kod logowaniem, aby zidentyfikować metodę, w której wystąpił błąd. Musimy przejść krok po kroku po kodzie, aby znaleźć błąd.

Co w przypadku, gdy mamy napisane testy jednostkowe dla tych wszystkich metod? Na 80% jeśli mamy napisane „dobre” (o tym jeszcze podyskutujemy) testy to właśnie na etapie uruchamiania testów dowiemy się, w której metodzie nasze flow jest zaburzone. Następnie wystarczy poprawić wadliwy fragment kodu, aż do momentu gdy testy będą palić się na zielono.

Zabezpieczanie kodu

Zazwyczaj nad projektem pracuje kilku programistów, wszyscy dopisują swój nowy kod do aplikacji – często ingerując już z istniejącym kodem, niekoniecznie tylko ze swoim kodem. Taka ingerencja może powodać problem – np. dodając nową funkcjonalność do aplikacji pod nasze potrzeby zmodyfikujemy lekko już istniejącą metodę i okaże się, że inna już istniejąca funkcjonalność przestanie działać.

I w tym momencie przychodzą z pomocą testy jednostkowe – jeśli tworząc swój kod napiszemy do niego testy to każda kolejna zmiana tego kodu musi być przemyślana. Jeśli inny programista lub nawet my w przyszłości będziemy edytować daną metodę i testy jednostkowe dla tej metody nie zakończą się sukcesem to już na etapie pisania kodu będziemy wiedzieć, że coś schrzaniliśmy.

Wady

Wiadomo, że każde podejście ma swoje wady i zalety. Teraz powiem trochę o wadach.

Pisanie testów jest nużące

Niestety pisanie testów jednostkowych nie służy do najprzyjmniejszych zajęć. Czynność ta szybko nudzi programistów, dlatego często nie chcą oni pisać testów do swojego kodu, co czasami przekłada się w przyszłości do występowania większej ilości błędów.

Pisanie testów jest czasochłonne

Pisanie testów jednostkowych pochłania sporo czasu, dlatego też niektórzy klienci (zlecający pisanie oprogramowania) nie pozwalają developerom ich pisać, ponieważ wolą inwestować swoje pieniądze w tworzenie nowych funkcjonalności aniżeli w testy, które nie są istotne dla samego biznesu.

Betonowanie kodu

Niestety większość testów jednostkowych jest pisanych tylko po to, aby były albo obrany scenariusz testu nie przynosi ze sobą żadnych korzyści (nie sprawdzają nic istotnego). Przez właśnie takie testy „betonujemy” swój kod i każda kolejna zmiana w kodzie wymaga od nas zmian w testach i tak w kółko – dochodzimy do momentu, że pisząc jedną linię kodu poprawiamy pięć linii w testach.

O jakiej sytuacji teraz mówię? Np. o nagminnych sprawdzaniu czy metoda X wywołała się N razy po wywołaniu metody Y. Nie oszukujmy się – w 99% takie testy nic nam nie mówią, betonujemy się tylko w kodzie. Skupiamy się na tym co sie dzieję w środku metody, a nie skupiamy się na samym rezultacie działania co jest ogromnym błędem.

Pisanie testów w praktyce

Przedstawiłem Ci sporo teorii dotyczącej testów jednostkowych, czas zobaczyć jak w praktyce pisać takie testy. Do pisania testów korzystam z biblioteki JUnit4, dzięki niej pisanie testów jest dużo prostsze aniżeli miałbym to robić tak samo jak w przykładzie na początku wpisu.

Aby dodać ową bibliotekę do swojego projektu wystarczy dodać poniższą zależność do pliku pom.xml (jeśli masz maven projekt).

<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.12</version>
    <scope>test</scope>
</dependency>

Zaś same testy będe umieszczał w folderze: /src/test/java.

Na początku, abyś szybko przyswoił jak pisać testy jednostkowe zaczniemy od najprostszego przykładu jakim można się posłużyć – czyli kalkulator. Napisałem na szybko prostą klasę, która obsługuje cztery operacje matematyczne: dodawanie, odejmowanie, dzielenie oraz mnożenie.

public class Calculator {
    public double add(double a, double b) {
        return a + b;
    }

    public double subtract(double a, double b) {
        return a - b;
    }

    public double divide(double a, double b) {
        if (b == 0.0d) {
            throw new IllegalArgumentException("Second number can't be a zero.");
        }

        return a / b;
    }

    public double multiply(double a, double b) {
        return a * b;
    }
}

Naszym zadaniem jest przetestowanie tej klasy. W tym celu w folderze test tworzymy nową klasę o nazwie: CalculatorTest, wraz z polem kalkulatorem, czyli obiektem, który będziemy testować.

public class CalculatorTest {
    private final Calculator calculator = new Calculator();

}

Musimy stworzyć kilka testów, aby przetestować każdą metodę – zacznijmy od metody add. Na początku tworzymy metodę typu void, której nazwa powinna oznaczać co testujemy i jakiego rezultatu oczekujemy. Następnie metodę powinniśmy oznaczyć adnotacją @Test, aby wskazać JUnit, że jest to test, który powinien zostać uruchomiony.

@Test
public void shouldCorrectAddTwoNumbers() {
    
}

Czyli podchodzimy do sprawy tak jak wcześniej – zaczynamy od przygotowania danych wejściowych:

@Test
public void shouldCorrectAddTwoNumbers() {
    // given
    double a = 3;
    double b = 2;
}

Następnie wywołujemy metodę:

@Test
public void shouldCorrectAddTwoNumbers() {
    // given
    double a = 3;
    double b = 2;

    // when
    double result = calculator.add(a, b);
}

I na koniec przy użyciu gotowych assercji porównujemy wyniki.

@Test
public void shouldCorrectAddTwoNumbers() {
    // given
    double a = 3;
    double b = 2;


    // when
    double result = calculator.add(a, b);
    
    // then
    Assert.assertEquals(5, result, 0.001);
}

Asercja assertEquals jako pierwszy argument przyjmuje wynik oczekiwany, drugi wynik aktualny, zaś trzeci argument to tzw. delta czyli do jakiego miejsca po przecinku liczby zmiennoprzecinkowe mają być porównywane. 😉

Po uruchomieniu testu np. w IntelliJ Idea powinniśmy zobaczyć zielonego ticka, który oznacza zaliczony test.

Na podobnej zasadzie napiszmy testy dla reszty metod z klasy Calculator.

@Test
public void shouldCorrectSubtractTwoNumbers() {
    // given
    double a = 3;
    double b = 2;

    // when
    double result = calculator.subtract(a, b);

    // then
    Assert.assertEquals(1, result, 0.001);
}

@Test
public void shouldCorrectDivideTwoNumbers() {
    // given
    double a = 4;
    double b = 2;

    // when
    double result = calculator.divide(a, b);

    // then
    Assert.assertEquals(2, result, 0.001);
}

@Test
public void shouldCorrectMultiplyTwoNumbers() {
    // given
    double a = 3;
    double b = 2;

    // when
    double result = calculator.multiply(a, b);

    // then
    Assert.assertEquals(6, result, 0.001);
}

Nasze testy obsługują teraz prawie wszystkie przypadki, oprócz jednego – dzielenia przez zero. Aby zabezpieczyć nasz kod na przyszłość powininśmy również przetestować ten przypadek, na szczęście przy użyciu JUnit4 można to zrobić bardzo łatwo.

@Test(expected = RuntimeException.class)
public void shouldThrowExceptionWhenDividerIsZero() {
    // given
    double a = 3;
    double b = 0;

    // when
    calculator.divide(a, b);

}

Tym razem usunęliśmy assercję i w adnotacji Test dodaliśmy właściwość expected – czyli mówimy JUnit, że ten wykonywany kod powinien rzucić wyjątkiem RuntimeException.

Przecież to jest zbyt proste!

 

Wszystko co Ci pokazałem powyżej to tylko niespełniony sen programisty – nigdy nam w realnym świecie nie przyjdzie do testowania tak banalnego kodu. Ten kod tak naprawdę nie wymaga żadnych testów, jest on tylko idealnym przykładem do pokazania jak działa testowanie jednostkowe.

Niestety jest za bardzo oderwany od rzeczywistości. Później nadchodzi czas prawdziwego projektu, w którym mamy sporo zależności, musimy o wielu rzeczach pamiętać, aby nasze testy w ogóle miały prawo zadziałać. I nadchodzi w tym momencie starcie – między osobą, która defacto wie jak działają testy jednostkowe, a realnym projektem, który już nie jest tak łatwy do przetestowania jak wspomniany wcześniej kalkulator.

Abyś uniknął takich sytuacji jak ja kiedyś, przygotowałem dla Ciebie krótki kod aplikacji –  w takim kodzie już coś się dzieje. Mamy więcej możliwości zakończenia naszego kodu, dlatego też napisanie testów staje się trudniejsze. Przeanalizujmy go i przejdźmy później do napisania do niego testów jednostkowych.

Mamy najzwyklejszą klasę User przechowującą informację o użytkowniku:

import java.util.Objects;

public class User {
    private Long id;
    private String login;
    private String password;

    public User(String login, String password) {
        this.login = login;
        this.password = password;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public Long getId() {
        return id;
    }

    public String getLogin() {
        return login;
    }

    public String getPassword() {
        return password;
    }

    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", login='" + login + '\'' +
                ", password='" + password + '\'' +
                '}';
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        User user = (User) o;
        return Objects.equals(id, user.id) &&
                Objects.equals(login, user.login) &&
                Objects.equals(password, user.password);
    }

    @Override
    public int hashCode() {
        return Objects.hash(id, login, password);
    }
}

Następnie klasę UserDao, która ma za zadanie być warstwą bazy danych opartą o pamięć – czyli wszystkie dane będą zapisywane w HashMapie.

import java.util.HashMap;
import java.util.Map;
import java.util.Optional;

public class UserDao {
    private final Map<Long, User> users = new HashMap<>();
    private Long userId = 0L;

    public User create(User user) {
        Long id = userId;
        userId += 1;

        user.setId(id);
        users.put(id, user);
        return user;
    }

    public Optional<User> getById(Long id) {
        return Optional.ofNullable(users.get(id));
    }

    public boolean delete(Long id) {
        return Optional.ofNullable(users.remove(id)).isPresent();
    }
}

Mamy kolejną klasę UserValidator, która ma za zadanie walidować dane użytkownika – jeśli coś nie jest z nimi nie tak, powinna rzucić wyjątkiem.

public class UserValidator {
    public void validate(User user) {
        if (isPasswordTooShort(user.getPassword())) {
            throw new RuntimeException("Password is too short.");
        }

        if (isLoginTooShort(user.getLogin())) {
            throw new RuntimeException("Login is too short.");
        }
    }

    private boolean isLoginTooShort(String login) {
        return login.length() < 3;
    }

    private boolean isPasswordTooShort(String password) {
        return password.length() < 6;
    }
}

Wszystkie te klasy są użyte jako pola w klasie UserService, dzięki której możemy tworzyć i usuwać użytkowników.

public class UserService {
    private final UserDao userDao = new UserDao();
    private final UserValidator userValidator = new UserValidator();

    public User createUser(User user) {
        userValidator.validate(user);

        return userDao.create(user);
    }

    public void removeUser(Long userId) {
        boolean result = userDao.delete(userId);

        if (!result) {
            throw new RuntimeException("User with id: " + userId + " doesn't exist.");
        }
    }
}

Naszym zadaniem teraz jest przetestowanie trzech powyższych klas: UserService, UserValidator oraz UserDao. Cały kod możesz znaleźć tutaj – polecam go ściągnąć na dysk, otworzyć w swoim IDE i przeklikać się przez niego. Zobacz jak się prezentuje kod w całości, jak jest ze sobą połączony itd.

Zacznijmy od przetestowania klasy UserValidator, mamy tam jedną validate, która ma trzy możliwe scenariusze:

  • walidacja przechodzi poprawnie,
  • zostaje rzucony wyjątek, gdy login jest zbyt krótki,
  • zostaje rzucony wyjątek, gdy hasło jest zbyt krótkie.

Niby jedna krótka metoda, a już ma trzy możliwe przypadki zakończenia. A uwierz mi na słowo, że w prawdziwym projekcie może być ich jeszcze więcej. Przejdźmy, więc do przetestowania tych trzech przypadków.

Na początek znowu tworzymy klasę na testy: UserValidatorTest. Dobrą praktyką jest trzymanie wszystkich testów dla danej klasy w jednej klasie odpowiednio oczywiście nazwanej.

Tak jak wcześniej – traktujemy metodę jak czarną skrzynkę, interesuje nas tylko to co wchodzi i wychodzi z funkcji. Nic więcej, nie sprawdzamy czy dana metoda powoduje jakieś efekty poboczne – na etapie testów jednostkowych nas to nie interesuje.

import org.junit.Test;

public class UserValidatorTest {
    @Test
    public void shouldNoThrowAnyExceptionWhileValidationCorrectUser() {
        // given
        User user = new User("pablo", "password");
        UserValidator userValidator = new UserValidator();

        // when
        userValidator.validate(user);
    }

    @Test(expected = RuntimeException.class)
    public void shouldThrowExceptionWhileValidationUserWithTooShortLogin() {
        // given
        User user = new User("as", "password");
        UserValidator userValidator = new UserValidator();

        // when
        userValidator.validate(user);
    }

    @Test(expected = RuntimeException.class)
    public void shouldThrowExceptionWhileValidationUserWithTooShortPassword() {
        // given
        User user = new User("pablo", "pass");
        UserValidator userValidator = new UserValidator();

        // when
        userValidator.validate(user);
    }
}

Następnie przetestujmy klasę UserDao – ponownie tworzymy osobną klasę na testy czyli UserDaoTest i umieszczamy tam swoje testy.

Dla metody create mamy tylko jeden scenariusz – za każdym razem zostanie zapisany do listy użytkownik z przypisanym ID.

@Test
public void shouldCorrectCreateUser() {
    // given
    UserDao userDao = new UserDao();
    User user = new User("pablo", "pablo123");

    // when
    User resultUser = userDao.create(user);
    User expectedUser = userDao.getById(0L).get();

    // then
    Assert.assertEquals(expectedUser, resultUser);
}

Spójrz, że porównuję użytkownika z metody create z użytkownikiem z metody getById, aby upewnić się, że metoda create dodała użytkownika do mapy (bazy danych).

Następnie musimy przetestować metodę getById – ma ona dwa scenariusze:

  • jeśli użytkownik istnieje to zwróci Optionala wraz z użytkownikiem,
  • jeśli użytkownik nie istnieje to Optional będzie pusty.
@Test
public void shouldReturnUserWhileGetById() {
    // given
    UserDao userDao = new UserDao();
    User user = new User("pablo", "pablo123");
    User expectedUser = new User("pablo", "pablo123");
    expectedUser.setId(0L);

    // when
    userDao.create(user);
    Optional<User> resultUser = userDao.getById(0L);

    // then
    Assert.assertEquals(Optional.of(expectedUser), resultUser);
}
@Test
public void shouldReturnEmptyOptionalWhileGetById() {
    // given
    UserDao userDao = new UserDao();

    // when
    Optional<User> resultUser = userDao.getById(0L);

    // then
    Assert.assertEquals(Optional.empty(), resultUser);
}

Zauważ, że tym razem w jednym z testów musimy przygotować odpowiednio obiekt do sytuacji – chodzi mi dokładnie o test shouldReturnUserWhileGetById, gdzie przed wyciągnieciem użytkownika najpierw go tworzymy.

Na koniec przetestujmy klasę UserService, zacznijmy od metody createUser – zanim zaczniemy cokolwiek robić wypiszmy sobie kilka możliwych scenariuszy:

  • użytkownik zostanie poprawnie utworzony i zostanie zwrócony ten sam obiekt z przypisanym ID,
  • użytkownik ma zbyt krótkie hasło i zostanie rzucony wyjątek,
  • użytkownik ma zbyt krótki login i zostanie rzucony wyjątek.
@Test
public void shouldCorrectAddNewUser() {
    // given
    UserService userService = new UserService();
    User user = new User("pablo", "pablo123");
    User expectedUser = new User("pablo", "pablo123");
    expectedUser.setId(0L);

    // when
    User resultUser = userService.createUser(user);

    // then
    Assert.assertEquals(expectedUser, resultUser);
}

@Test(expected = RuntimeException.class)
public void shouldThrowLoginExceptionWhileCreatingNewUser() {
    // given
    UserService userService = new UserService();
    User user = new User("pa", "pablo123");

    // when
    userService.createUser(user);
}

@Test(expected = RuntimeException.class)
public void shouldThrowPasswordExceptionWhileCreatingNewUser() {
    // given
    UserService userService = new UserService();
    User user = new User("pablo", "pass");

    // when
    userService.createUser(user);
}

Na każdy możliwy sposób już przetestowaliśmy metodę createUser, czas się zabrać za removeUser.

W metodzie removeUser mamy dwa scenariusze:

  • jeśli użytkownik istnieje to użytkownik ma zostać usunięty,
  • jeśli użytkownik nie istnieje to zostanie rzucony wyjątek.

Podobnie jak w poprzednim przykładzie piszemy testy w klasie UserServiceTest.

@Test
public void shouldCorrectRemoveUser() {
    // given
    UserService userService = new UserService();
    User user = new User("pablo", "pablo123");
    userService.createUser(user);


    //when
    userService.removeUser(0L);
}
@Test(expected = RuntimeException.class)
public void shouldThrowExceptionWhenUserDoesntExistWhileRemoving() {
    // given
    UserService userService = new UserService();

    //when
    userService.removeUser(1L);
}

Jaki jest zysk?

Napisaliśmy właśnie testy do trzech klas, które stworzyłem na początku. Narobiliśmy się trochę – łącznie napisaliśmy 11 testów. Czas stracony, trochę się człowiek ponudził – czy w ogóle coś zyskaliśmy?

Sprawdźmy to w bardzo prosty sposób – przypuśćmy, że jeden programista stwierdził, że trochę zmieni implementancję metody create z:

public User create(User user) {
    Long id = userId;
    userId += 1;

    user.setId(id);
    users.put(id, user);
    return user;
}

na:

public User create(User user) {
     userId += 1;

     user.setId(userId);
     users.put(userId, user);
     return user;
 }

Nieświadomie popsuł kod poprzedniego programisty myśląć, że właśnie zoptymalizował kod. Na szczęście poprzedni programista napisał testy do tej metody i uruchamiając testy jednostkowe…

Nie przejdą nam, aż 4 testy – wszystkie, które zależą od ID w hashMapie. Dokładnie są to testy związane z tworzeniem użytkowników. Otrzymując czerwony komunikat, programista natychmiast powinien zorientować się, że coś popsuł i musi to jakoś naprawić. I to właśnie dzięki testom jednostkowym bardzo szybko będzie mógł zlokalizować błąd.

Co by było gdybyśmy nie mieli napisanych testów?

Błąd mógłby się przedrzeć na dalsze etapy produkcji, co prawda zasymulowany przeze mnie błąd mógłby być szybki do zidentyfikowania, ale co w przypadku, gdy nasz projekt jest bardzo skomplikowany i każde flow jest zależne od wielu czynników?

Poziom trudności wzrasta…

Zauważ, że powyższy przykład kodu jest już trochę trudniejszy do przetestowania aniżeli kalkulator. Wynik działania testowanej metody zależy od większej ilości czynników, przez co musimy przetestować jedną metodę kilkukrotnie zależnie od obranego scenariusza (np. login zbyt krótki itd.).

Choć przykład, który Ci teraz pokazałem nie jest jeszcze aż tak bardzo skomplikowany jak dzieje się to w realnym świecie. Często w flow metod wchodzą wyniki pochodzące z realnej bazy danych lub zewnętrznych serwisów, które dostarczają nam dane poprzez protokół HTTP. Wtedy skomplikowanie całego procesu nam rośnie, musimy coś w końcu zrobić z bazą danych, którą co prawda możemy mieć w czasie testów, ale co w takiej sytuacji z zewnętrznymi serwisami? Nie mamy w końcu pewności, że podczas testów akurat będą dostępne.

Co musisz zapamiętać?

Po przestudiowaniu tego artykułu bardzo bym chciał, żebyś zapamiętał według mnie najważniejsze rzeczy:

  • jeden test powinien sprawdzać tylko jeden scenariusz,
  • testy nie powinny być od siebie w jakikolwiek sposób zależne,
  • nie zawsze warto testować każdą metodę nieprywatną – wszystko oczywiście zależy od specyfikacji projektu,
  • do testowania można użyć biblioteki JUnit,
  • testy w praktyce są trudniejsze do napisania niż słynne przykłady z internetu!

Podsumowanie

W pierwszym artykule dotyczącym testowania jednostkowego pokazałem Ci czym są testy jednostkowe oraz jak można takie testy napisać bez żadnych dodatkowych bibliotek. Poznałeś wady i zalety takiego podejścia, a następnie pokazałem Ci jak przy użyciu biblioteki JUnit pisać testy jednostkowe.

Na sam koniec chciałem podkreślić, że pisanie testów jednostkowych może być trudne – nienawidzę przykładów gdzie ludzie pokazują testy jednostkowe wraz z kalkulatorem, a takich przykładów w internecie i na prezentacjach są setki. Wtedy tak to wszystko pięknie wygląda – później pojawia się wielki mur i laik nie wie jak go ominąć.

W następnych artykułach poznasz kolejne zagadnienia związane z testowaniem jednostkowym i będziesz napotykał na sytuacje, które są z pewnością trudniejsze od słynnego przykładu z kalkulatorem.