Test łosia- powtarzalny test, który musi przejść każde nowe auto, sprawdzający stabilność pojazdu podczas wykonywania gwałtownych manewrów

Zanim przejdziemy do konkretów, ustalmy jeden dogmat. Testowanie służy do weryfikacji, czy dane oprogramowanie/algorytm działa poprawnie w ramach założeń, które są ustalane podczas tworzenia testów. To znaczy, że nie sprawdza ono programu pod każdym możliwym względem. Oprogramowanie jest przetestowane tak dobrze, jak dobrze zostały stworzone testy, w ramach których jest ono weryfikowane. To znaczy, że nawet przetestowany kod może zawierać skazę.

Testy jednostkowe (ang. unit test) polegają na tworzeniu zestawu danych wejściowych do konkretnej funkcji/metody wraz z odpowiadającym im zestawem danych wyjściowych- oczekiwanych rezultatów po uruchomieniu funkcji z takimi parametrami.

Dla przykładu stworzmy funkcję, która będzie liczyć pole prostokąta. Funkcja niby banalna.

/**
Oblicza pole prostokąta,
@int szerokosc - szerokość prostokąta w milimetrach
@int dlugosc - długość prostokąta w milimetrach
Zwraca wynik większy od zera, oznaczające pole powierzchni w milimetrach kwadratowych, jeżeli dane wejściowe są poprawne. Zwraca -1 gdy podano nieprawidłową wartość szerokości. Zwraca -2, gdy podano nieprawidłową wartość długości.
 */
int poleProstokata(unsigned int szerokosc, unsigned int dlugosc) {
  if (szerokosc == 0) {
    return -1;
  }

  If (dlugosc == 0) {
    return -2;
  }

  return szerokosc * dlugosc;
}

Jak widać, ta funkcja to pewien przerost formy nad treścią, przecież można po prostu pomnożyć dwie zmienne przez siebie, aby otrzymać wynik. Jest to jednak laboratoryjny okaz, rzadko spotykany, służyć będzie nam zamiast szczura.

Z opisu funkcji wynika, że podając 0 jako długość lub szerokość funkcja zwróci kolejno -2 lub -1. W innym przypadku powinna zwrócić poprawny wynik - pole powierzchni w milimetrach kwadratowych. Tworzymy testy jednostkowe dla tej funkcji. Dobieramy parametry wejściowe dla argumentów oraz oczekiwane wartości: Długość 1, szerokość 1, wynik = 1; Długość 1, szerokość 2, wynik = 2; Długość 2, szerokość 2, wynik = 4; Długość 0, szerokość 23, wynik = -2; Długość 12, szerokość 0, wynik = -1;

Oczywiście jesteśmy leniwi, potrzeba więc pewnego mechanizmu automatyzującego nasze testy. Do tego celu służy funkcja assert() albo jakiś jej odpowiednik (w zależności od języka, kompilatora, etc). Stworzymy prymitywną wersję assert:

void assert(int wartosc, int oczekiwanaWartosc, const char *nazwaTestu) {

  printf("Testuje- %s: ", nazwaTestu);

  if (wartosc != oczekiwanaWartosc) {
    printf("blad! Podano: %d, oczekiwano: %d"\n, wartosc, oczekiwanaWartosc);
  }
  else {
    printf("OK\n");
  }

}

I tyle, funkcja po prostu sprawdza czy podane wartości do siebie pasują i wyświetla informacje na ekranie. Stworzenie serii testów, odpowiadających naszym wymaganiom wyglądałoby następująco:

assert(poleProstokata(1, 1), 1, "Wartosci 1, 1");
assert(poleProstokata(1, 2), 2, "wartosci 1, 2");
assert(poleProstokata(2, 2), 4, "Dwie wartosci 2, 2");
assert(poleProstokata(23, 0), -2, "Podchwytliwie sprawdzam zerowa dlugosc");
assert(poleProstokata(0, 12), -1, "Podobnie jak wyzej, ale teraz szerokosc jest nie ten teges");

Po uruchomieniu tego kodu powinniśmy dostać informację, że wszystko jest OK. W momencie, gdy zmienimy coś w naszej funkcji liczącej pole prostokąta, po uruchomieniu testów otrzymamy potwierdzenie, że po drodze nic nie popsuliśmy.

Powyższy laboratoryjny przykład pewnie nie przekona Ciebie do zasadności tworzenia testów jednostkowych- pokazuje on jedynie podejście do ich realizacji. Wyobraź sobie, że obliczenia nie dotyczą pola prostokąta a długość fali, z jaką należy nadać sygnał, aby zakodować w eterze konkretny znak alfabetu (albo coś innego, równie skomplikowanego). Danych wejściowych do sprawdzenia ich poprawności byłoby całkiem sporo i zdecydowanie łatwiej w takiej funkcji o przeoczenie jakiegoś warunku sprawdzającego. Niechciana zmiana w takiej newralgicznej funkcji może być fatalna w skutkach, stąd przygotowanie odpowiednich testów jest jak najbardziej wskazane.

Najlepsza sytuacja zachodzi wtedy, gdy testy przygotowuje osoba, która nie pisze kodu, do którego testy są przygotowane. Działa to dlatego, że pominięte jest ryzyko, że osoba będzie układać testy pod warunki sprawdzające, które sama pisała.

Dobrą zasadą jest pisanie testów, które skupiają się bardziej na sytuacjach brzegowych, tj. takich, których raczej się nie spodziewamy, albo nie chcemy. Sytuacje, w których coś mogłoby pójść nie tak. Lipne dane, skrajne wartości, osobliwe zestawy danych. Zadaniem jest rozkołysać algorytm i patrzeć, czy podczas wyginania nie przewróci się, tylko zachowa fason. Nie mniej jednak warto poświęcić kilka testów na tzw. szczęśliwe scenariusze, czyli sytuacje najbardziej pożądane i prawdopodobne.

Może narażę się pisząc to, ale moim zdaniem testy jednostkowe najlepiej się sprawdzają, gdy projekt przechodzi do stabilnej fazy swojego życia (nie dotyczy to oczywiście wszystkich projektów, a jedynie większości, z którymi mam obecnie do czynienia). Otóż na początku energię i zapał pracy lepiej skoncentrować na stworzeniu działającego projektu, chociażby miałby się trzymać tylko na zapałkach, a następnie, gdy już robi to co ma robić, zabezpieczyć jego dalszy rozwój za pomocą testów jednostkowych. Po prostu uważam, że lepiej jest pokazać coś niedoskonałego, ale realizującego podstawowe funkcje i później to usprawnić niż w tkwić w nieskończonej próbie osiągnięcia ideału.

Jakie jest Twoje doświadczenie z testami jednostkowymi? Masz na koncie jakieś testy czy tworzysz bezbłędny kod?

Dobry kontent!

Piotr Poźniak

Piotr Poźniak

Programuję od ponad 15 lat. Prowadzę software house. Angażuję i zachęcam wszystkich do programowania w ramach inicjatywy Programowanie jest łatwe.

Twoja opinia

Komentarzy: 1

Avatar użytkownika Rouch
Rouch · 4 lata temu

Moim zdaniem każdy powinien pisać, i programista i tester, testy jednostkowe, poświęcenie tych dodatkowych 10 minut może zaoszczędzić godziny debbugowania.