Rozdział 8 - Testowanie kodu - Pierwsze testy

Jak już wcześniej wspomniano, testy to metody, które wywołują testowaną metodę. Spróbujmy w takim razie napisać nasze pierwsze testy jednostkowe – obiektem naszych testów będzie metoda, która podnosi przesłaną do niej liczbę całkowitą do kwadratu i zwraca wynik. Zacznijmy od napisania tej metody:

Nazwa pliku: TestowanieDoKwadratu.java
public class TestowanieDoKwadratu {
  public static int doKwadratu(int x) {
    return x * x;
  }
}

Nasz program jest bardzo prosty – nie ma jeszcze nawet metody main, ale ma za to metodę doKwadratu, która przyjmuje argument typu int i zwraca wartość typu int – w tym przypadku, jest to wartość argumentu x podniesiona do kwadratu.

Jak możemy przetestować, że nasza metoda działa poprawnie? Moglibyśmy dopisać metodę main i w niej wywołać metodę doKwadratu i wypisać jej wynik na ekran:

Nazwa pliku: TestowanieDoKwadratu.java
public class TestowanieDoKwadratu {
  public static void main(String[] args) {
    System.out.println(doKwadratu(20));
  }

  public static int doKwadratu(int x) {
    return x * x;
  }
}

Program uruchamia się i wypisuje poprawną wartość na ekranie: 400. Metoda main testuje naszą metodę, jednak nie jest to rozwiązanie docelowe, ponieważ możemy je ulepszyć na trzy sposoby:

  1. Po pierwsze, chcielibyśmy umieszczać testy z różnymi danymi wejściowymi w osobnych metodach, a nie w metodzie main.
  2. Po drugie, lepiej byłoby, gdyby testy informowały nas tylko o przypadkach błędnych – gdy wszystko jest w porządku i testowana metoda zadziałała poprawnie, nie chcemy widzieć nic na ekranie. Interesują nas przypadki, gdy coś poszło nie tak.
  3. Po trzecie, chcielibyśmy mieć więcej przypadków testowych, w najlepszym przypadku pokrywających wszystkie możliwości, a na pewno przypadki szczególne.

Dostosujmy nasz program krok po kroku zgodnie z powyższymi wytycznymi.

Testy w osobnych metodach

Przenieśmy test do osobnej metody:

Nazwa pliku: TestowanieDoKwadratu.java
public class TestowanieDoKwadratu {
  public static void main(String[] args) {
    doKwadratu_wartoscDodatnia_wartoscPodniesionaDoKwadratu(); // (1)
  }

  public static int doKwadratu(int x) {
    return x * x;
  }

  // (2)
  public static void doKwadratu_wartoscDodatnia_wartoscPodniesionaDoKwadratu() {
    System.out.println(doKwadratu(20));
  }
}

Przenieśliśmy test do osobnej metody o bardzo długiej nazwie (2). Istnieją różne konwencje, które definiują, jak powinny być nazywane metody testowe – jedną z nich jest konwencja, wedle której metody powinny być nazywane w następujący sposób:

nazwaTestowanejMetody_daneWejściowe_spodziewanyWynik

  • Pierwszy człon nazwy metody testowej to nazwa metody, którą testujemy – w naszym programie testujemy metodę o nazwie doKwadratu, więc metoda, która ją testuje, zaczyna się właśnie od "doKwadratu".
  • W środku nazwy metody testowej umieszczamy krótki opis danych wejściowych – nasza metoda testowa testuje podnoszenie do kwadratu liczby dodatniej – stąd drugi człon nazwy to "wartoscDodatnia".
  • Na końcu umieszczamy krótki opis wyniku, jakiego się spodziewamy, że testowana metoda zwróci dla danych wejściowych – w naszym przypadku oczekujemy, że metoda doKwadratu dla wartości dodatniej zwróci "wartoscPodniesionaDoKwadratu".

Dodatkowo, skoro przenieśliśmy test metody doKwadratu do osobnej metody, to musimy teraz w jakiś sposób wywołać test tej metody. Test ten znajduje się teraz w nowej metodzie o nazwie doKwadratu_wartoscDodatnia_wartoscPodniesionaDoKwadratu, więc musimy wywołać tą metodę w metodzie main (1), aby przeprowadzić test metody doKwadratu.

W jednych z kolejnych rozdziałów tego kursu nauczymy się, jak wydzielać testy do osobnych plików i jak uruchamiać je w prosty sposób przy użyciu biblioteki JUnit.

Zwróćmy uwagę, że metoda testowa nic nie zwraca – jej zwracany typ to void. Metody testowe nie powinny zwracać wartości, ponieważ nie jest to do niczego wymagane.

Informowanie tylko o błędnym działaniu

Test jest już w osobnej metodzie – teraz zajmiemy się drugim opisanym powyżej punktem: chcielibyśmy, aby nasz test komunikował tylko informacje o przypadkach testowych, które nie przeszły testu – jeżeli wszystko poszło dobrze, to test powinien po prostu "siedzieć cicho".

Jak możemy to osiągnąć? Wystarczy, że sprawdzimy wynik metody – jeżeli będzie inny, niż się spodziewamy, wtedy wypiszemy na ekran komunikat informujący, że testowana przez nas metoda zwróciła dla danego argumentu inny wynik, niż oczekiwaliśmy:

Nazwa pliku: TestowanieDoKwadratu.java
public class TestowanieDoKwadratu {
  public static void main(String[] args) {
    doKwadratu_wartoscDodatnia_wartoscPodniesionaDoKwadratu();
  }

  public static int doKwadratu(int x) {
    return x * x;
  }

  public static void doKwadratu_wartoscDodatnia_wartoscPodniesionaDoKwadratu() {
    int wynik = doKwadratu(20); // (1)

    if (wynik != 400) { // (2)
      // (3)
      System.out.println(
          "Dla liczby 20 wyliczono nieprawidlowy kwadrat: " + wynik
      );
    }
  }
}

Nasz program nie wypisuje już po prostu wartości podniesionej do kwadratu. Zamiast tego:

  1. Najpierw wyliczamy kwadrat liczby 20 i zapisujemy wynik w zmiennej wynik (1).
  2. Następnie, w instrukcji warunkowej sprawdzamy otrzymany wynik – jeżeli jest inny, niż się spodziewamy (2), wypiszemy na ekran informację.
  3. Jeżeli metoda doKwadratu niepoprawnie wyliczy kwadrat liczby 20, to wypisujemy na ekran komunikat informujący, że testowana metoda zadziałała niepoprawnie (3), bo źle wykonała swoje zadanie (zwróciła błędny wynik). Świadczy to o tym, że test nie zakończył się sukcesem.

Jeżeli metoda doKwadratu zadziała poprawnie, to test nic nie wypisze. Dzięki temu będziemy informowani tylko w tych przypadkach, gdy coś w naszym kodzie nie działa bądź przestało działać.

Więcej przypadków testowych

Jeden test to zazwyczaj za mało, by przetestować wszystkie przypadki. Warto zastanowić się zawsze nad przypadkami szczególnymi, np.:

  • przypadkami, które wydaje nam się, że nigdy nie wystąpią,
  • przypadkami skrajnych danych wejściowych,
  • przypadkami z nieprawidłowymi danymi wejściowymi.

Poza tym testujemy też oczywiście metody używając "spodziewanych" argumentów.

Ważne jest także, aby nie duplikować przypadków testowych – jeżeli testujemy metodę doKwadratu i napisaliśmy test, który sprawdza, czy metoda ta dla argumentu 5 kończy się sukcesem (tzn. metoda doKwadratu działa poprawnie), to nie ma potrzeby pisania testu, który sprawdzi wynik metody doKwadratu dla liczby 10 – test z liczbą 5 pokrył już podobny przypadek.

Dopiszmy jeszcze dwa testy do metody doKwadratu:

  • sprawdzimy, jak metoda zachowa się dla argumentu, którym będzie liczbą ujemna,
  • sprawdzimy, co stanie się dla argumentu równego zero.
Nazwa pliku: TestowanieDoKwadratu.java
public class TestowanieDoKwadratu {
  public static void main(String[] args) {
    doKwadratu_wartoscDodatnia_wartoscPodniesionaDoKwadratu();
    doKwadratu_wartoscUjemna_wartoscPodniesionaDoKwadratu(); // (1)
    doKwadratu_liczbaZero_zero(); // (2)
  }

  public static int doKwadratu(int x) {
    return x * x;
  }

  public static void doKwadratu_wartoscDodatnia_wartoscPodniesionaDoKwadratu() {
    int wynik = doKwadratu(20);

    if (wynik != 400) {
      System.out.println(
           "Dla liczby 20 wyliczono nieprawidlowy kwadrat: " + wynik
       );
    }
  }

  // (3)
  public static void doKwadratu_wartoscUjemna_wartoscPodniesionaDoKwadratu() {
    int wynik = doKwadratu(-5);

    if (wynik != 25) {
      System.out.println(
          "Dla liczby -5 wyliczono nieprawidlowy kwadrat: " + wynik
      );
    }
  }

  // (4)
  public static void doKwadratu_liczbaZero_zero() {
    int wynik = doKwadratu(0);

    if (wynik != 0) {
      System.out.println(
          "Dla liczby 0 wyliczono nieprawidlowy kwadrat: " + wynik
      );
    }
  }
}

W ostatecznej wersji naszego programu dodaliśmy dwa nowe testy (3) (4) oraz wywołaliśmy je w metodzie main (1) (2). Po uruchomieniu nasz program nic nie wypisuje – świadczy to o tym, że nasza metoda doKwadratu działa poprawnie!

Czy możemy być pewni, że faktycznie tak jest? Czy nie zapomnieliśmy o jeszcze jakimś przypadku testowym? Do zastanowienia jako zadanie!

Duplikacja kodu

Wróćmy jeszcze na chwilę do finalnej wersji kodu z poprzedniego rozdziału – spójrzmy na kod naszych testów:

public static void doKwadratu_wartoscDodatnia_wartoscPodniesionaDoKwadratu() {
  int wynik = doKwadratu(20);

  if (wynik != 400) {
    System.out.println(
        "Dla liczby 20 wyliczono nieprawidlowy kwadrat: " + wynik
    );
  }
}

public static void doKwadratu_wartoscUjemna_wartoscPodniesionaDoKwadratu() {
  int wynik = doKwadratu(-5);

  if (wynik != 25) {
    System.out.println(
        "Dla liczby -5 wyliczono nieprawidlowy kwadrat: " + wynik
    );
  }
}

public static void doKwadratu_liczbaZero_zero() {
  int wynik = doKwadratu(0);

  if (wynik != 0) {
    System.out.println(
        "Dla liczby 0 wyliczono nieprawidlowy kwadrat: " + wynik
    );
  }
}

Widzimy, że każda metoda testowa napisana jest w podobny sposób – najpierw wywołujemy testowaną metodę, a potem sprawdzamy, czy wynik jest poprawny. Czy moglibyśmy w takim razie jakoś uprościć powyższe metody i pozbyć się duplikacji kodu?

Moglibyśmy napisać kolejną metodę, której zadaniem będzie sprawdzenie, czy przesłane do niej argumenty są sobie równe. Jeżeli nie, metoda wypisze na ekran komunikat. Następnie moglibyśmy tej nowej metody użyć w naszych testach. Spójrzmy na kolejną wersję naszego programu:

Nazwa pliku: TestowanieDoKwadratu2.java
public class TestowanieDoKwadratu2 {
  public static void main(String[] args) {
    doKwadratu_wartoscDodatnia_wartoscPodniesionaDoKwadratu();
    doKwadratu_wartoscUjemna_wartoscPodniesionaDoKwadratu();
    doKwadratu_liczbaZero_zero();
  }

  public static int doKwadratu(int x) {
    return x * x;
  }

  public static void doKwadratu_wartoscDodatnia_wartoscPodniesionaDoKwadratu() {
    int wynik = doKwadratu(20);
    assertEquals(400, wynik); // (1)
  }

  public static void doKwadratu_wartoscUjemna_wartoscPodniesionaDoKwadratu() {
    int wynik = doKwadratu(-5);
    assertEquals(25, wynik); // (2)
  }

  public static void doKwadratu_liczbaZero_zero() {
    int wynik = doKwadratu(0);
    assertEquals(0, wynik); // (3)
  }

  //                    (4)              (5)          (6)
  public static void assertEquals(int expected, int actual) {
    if (expected != actual) { // (7)
      System.out.println("Spodziewano sie liczby " + actual +
        ", ale otrzymano: " + expected);
    }
  }
}

Nowa metoda assertEquals (4) przyjmuje dwa parametry typu int o nazwach expected (5) oraz actual (6). Jej jedynym zadaniem jest wypisanie komunikatu, gdy liczby przesłane jako argumenty nie są sobie równe (7). Dzięki tej metodzie, mogliśmy znacząco skrócić nasze metody testowe – metody assertEquals używamy teraz do sprawdzenia wyniku działania metody doKwadratu (1) (2) (3).

Nazwa metody (assert – zapewnij) oraz nazwy jej argumentów nie są przypadkowe. Wiele bibliotek wspierających testy jednostkowe udostępnia metodę o właśnie takiej nazwie i takich argumentach – wykonują one bardzo podobne zadanie jak nasza powyższa metoda assertEquals (chociaż komunikuje nierówność argumentów w inny sposób, który poznamy w jednym z kolejnych rozdziałów).

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Nie musisz podawać swojego imienia, e-mailu, ani strony www, aby opublikować komentarz. Komentarze muszą zostać zatwierdzone, aby były widoczne na stronie.