Go dla początkujących - Część 7: Testy jednostkowe

2/4/2025 Kurs Go

Mateusz Kędziora

image

Cześć! Jeśli zaczynasz swoją przygodę z językiem Go, to ten artykuł jest dla Ciebie. Dzisiaj zajmiemy się czymś niezwykle ważnym w programowaniu – testowaniem jednostkowym. Testy jednostkowe pozwalają nam upewnić się, że poszczególne części naszego kodu działają tak, jak tego oczekujemy. Dzięki nim możemy szybciej wykrywać błędy i tworzyć bardziej niezawodne aplikacje.

W Go testowanie jest bardzo proste dzięki wbudowanemu pakietowi testing. Nie musisz instalować żadnych dodatkowych bibliotek. W tym artykule pokażemy Ci, jak korzystać z testing, aby pisać testy dla różnych rodzajów kodu: funkcji, obsługi błędów i kodu współbieżnego. Nauczymy się też, jak stosować testowanie tabelaryczne i benchmarki, które pomogą nam ulepszyć nasze testy.

Co to jest testowanie jednostkowe?

Zanim przejdziemy do kodu, warto zrozumieć, czym jest testowanie jednostkowe. Wyobraź sobie, że budujesz dom. Testowanie jednostkowe to jak sprawdzanie, czy każda cegła, każdy element konstrukcyjny, jest solidny i dobrze wykonany, zanim przejdziesz do kolejnych etapów.

W programowaniu, test jednostkowy sprawdza działanie jednej, małej części kodu – najczęściej funkcji lub metody. Celem jest upewnienie się, że ta konkretna część działa poprawnie w różnych warunkach. Testy jednostkowe pomagają nam:

  1. Wykrywać błędy na wczesnym etapie: Łatwiej jest znaleźć i naprawić błąd w małej funkcji, niż szukać go w całym programie.
  2. Zwiększać pewność siebie: Mając dobrze przetestowany kod, możesz wprowadzać zmiany z większą pewnością, że nie zepsujesz czegoś innego.
  3. Ułatwiać refaktoryzację: Testy dają pewność, że zmiany w kodzie nie zepsują jego funkcjonalności.
  4. Dokumentować kod: Testy pokazują, jak dana funkcja powinna działać i w jakich warunkach.

Pakiet testing - Twój pomocnik w testowaniu

Pakiet testing w Go jest Twoim najlepszym przyjacielem, jeśli chodzi o pisanie testów. Zawiera wszystko, czego potrzebujesz, aby tworzyć i uruchamiać testy jednostkowe. Aby rozpocząć, musisz:

  1. Utworzyć plik testowy: Plik testowy ma taką samą nazwę jak plik, który testujesz, z dodatkiem _test. Na przykład, jeśli masz plik math.go, Twój plik testowy będzie się nazywał math_test.go.
  2. Zaimportować pakiet testing: Na początku pliku testowego dodaj import pakietu testing.
  3. Napisać funkcję testową: Funkcje testowe mają nazwy zaczynające się od Test i przyjmują argument typu *testing.T.

Przykład: Testowanie prostej funkcji

Załóżmy, że mamy prostą funkcję w pliku math.go:

// math.go
package main

// Add dodaje dwie liczby całkowite.
func Add(a, b int) int {
    return a + b
}

Teraz stwórzmy plik testowy math_test.go:

// math_test.go
package main

import "testing"

// TestAdd sprawdza funkcję Add.
func TestAdd(t *testing.T) {
    result := Add(2, 3)
    if result != 5 {
        t.Errorf("Add(2, 3) returned %d, expected %d", result, 5)
    }
}

Omówienie kodu:

  • package main: Plik testowy jest w tym samym pakiecie, co testowany kod.
  • import "testing": Importujemy pakiet testing.
  • func TestAdd(t *testing.T) { ... }: Definiujemy funkcję testową o nazwie TestAdd.
  • result := Add(2, 3): Używamy funkcji Add, którą testujemy.
  • if result != 5 { ... }: Sprawdzamy, czy wynik jest taki, jakiego oczekujemy.
  • t.Errorf(...): Jeżeli wynik jest niepoprawny, funkcja t.Errorf wypisuje komunikat o błędzie.

Uruchamianie testów

Aby uruchomić test, otwórz terminal w katalogu zawierającym pliki math.go i math_test.go, a następnie wykonaj polecenie:

go test

Jeśli test przejdzie pomyślnie, nie zobaczysz żadnych komunikatów. W przypadku niepowodzenia, zobaczysz informacje o błędach.

Testowanie obsługi błędów

W prawdziwym świecie funkcje często zwracają błędy. Musimy więc testować, czy nasza funkcja poprawnie obsługuje błędy. Załóżmy, że mamy funkcję dzielącą dwie liczby, która zwraca błąd, gdy dzielnik jest równy zero:

// math.go
package main

import "errors"

// Divide dzieli dwie liczby całkowite, zwraca błąd, gdy b jest równe 0.
func Divide(a, b int) (int, error) {
    if b == 0 {
        return 0, errors.New("dzielenie przez zero")
    }
    return a / b, nil
}

Oto jak możemy przetestować tę funkcję:

// math_test.go
package main

import (
    "errors"
    "testing"
)

// TestDivide sprawdza funkcję Divide.
func TestDivide(t *testing.T) {
    // Test poprawny wynik dzielenia
    result, err := Divide(10, 2)
    if err != nil {
        t.Errorf("Oczekiwano nil, a wystąpił błąd: %v", err)
    }
    if result != 5 {
        t.Errorf("Divide(10, 2) returned %d, expected %d", result, 5)
    }

    // Test dzielenie przez zero
    _, err = Divide(10, 0)
    if err == nil {
        t.Errorf("Oczekiwano błędu dzielenia przez zero, ale go nie otrzymano")
    }
    if !errors.Is(err, errors.New("dzielenie przez zero")){
         t.Errorf("Oczekiwano błędu 'dzielenie przez zero' a otrzymano: %v", err)
    }
}

Omówienie kodu:

  • Testujemy zarówno poprawny wynik dzielenia, jak i sytuację, gdy dzielimy przez zero.
  • Sprawdzamy, czy funkcja Divide zwraca oczekiwany błąd (dzielenie przez zero) gdy dzielimy przez zero.
  • Wykorzystujemy errors.Is do sprawdzenia konkretnego rodzaju błędu.

Testowanie kodu współbieżnego

Go jest znany ze swojego wsparcia dla współbieżności (ang. concurrency). Testowanie kodu, który wykorzystuje gorutyny i kanały, może być trudniejsze, ale pakiet testing pomaga nam i w tym. Załóżmy, że mamy funkcję, która wysyła liczby do kanału:

// concurrent.go
package main

// SendNumbers wysyła liczby od 0 do n-1 do kanału.
func SendNumbers(n int, ch chan int) {
    for i := 0; i < n; i++ {
        ch <- i
    }
    close(ch)
}

Oto jak możemy przetestować tę funkcję:

// concurrent_test.go
package main

import (
    "testing"
)

// TestSendNumbers sprawdza funkcję SendNumbers.
func TestSendNumbers(t *testing.T) {
    ch := make(chan int)
    go SendNumbers(5, ch)

    expected := []int{0, 1, 2, 3, 4}
    i := 0
    for val := range ch {
        if val != expected[i] {
            t.Errorf("Otrzymano %d, oczekiwano %d", val, expected[i])
        }
        i++
    }
    if i != len(expected){
        t.Errorf("Oczekiwano %d liczb, a otrzymano %d", len(expected), i)
    }
}

Omówienie kodu:

  • Tworzymy kanał ch.
  • Uruchamiamy SendNumbers w osobnej gorutynie, żeby nie zablokować testu.
  • Sprawdzamy, czy liczby odbierane z kanału są takie, jakich oczekujemy.
  • Dodatkowo sprawdzamy, czy otrzymaliśmy wszystkie elementy.

Testowanie tabelaryczne

Często mamy do przetestowania funkcję z wieloma różnymi przypadkami testowymi. W takim przypadku warto skorzystać z testowania tabelarycznego, które sprawia, że nasz kod staje się bardziej czytelny i zwięzły. Spójrzmy na przykład:

// stringutil.go
package main

import "strings"

// Reverse odwraca string.
func Reverse(s string) string {
	runes := []rune(s)
	for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 {
		runes[i], runes[j] = runes[j], runes[i]
	}
	return string(runes)
}

A oto jak możemy przetestować tę funkcję za pomocą testowania tabelarycznego:

// stringutil_test.go
package main

import "testing"

// TestReverse sprawdza funkcję Reverse za pomocą testowania tabelarycznego.
func TestReverse(t *testing.T) {
    testCases := []struct {
        input    string
        expected string
    }{
        {"hello", "olleh"},
        {"Go", "oG"},
        {"", ""},
        {"a", "a"},
        {"12345", "54321"},
    }

    for _, tc := range testCases {
        result := Reverse(tc.input)
        if result != tc.expected {
            t.Errorf("Reverse(%q) returned %q, expected %q", tc.input, result, tc.expected)
        }
    }
}

Omówienie kodu:

  • Definiujemy strukturę testCases, która zawiera input oraz oczekiwany wynik.
  • Iterujemy po strukturze i dla każdego testu wywołujemy testowaną funkcję.
  • Porównujemy uzyskany wynik z oczekiwanym.
  • Dzięki takiemu zapisowi kod testowy jest bardziej czytelny i łatwiejszy w rozbudowie.

Benchmarki - Mierzenie wydajności

Testy to nie tylko sprawdzanie poprawności, ale również mierzenie wydajności. Benchmarki pozwalają nam na zbadanie, jak szybko działa nasz kod. W Go możemy to zrobić za pomocą funkcji z przedrostkiem Benchmark w pakiecie testing. Spójrzmy jak to wygląda na przykładzie naszej funkcji Reverse:

// stringutil_test.go
package main

import "testing"

// BenchmarkReverse mierzy wydajność funkcji Reverse.
func BenchmarkReverse(b *testing.B) {
    input := "abcdefghijklmnopqrstuvwxyz"
    for i := 0; i < b.N; i++ {
        Reverse(input)
    }
}

Omówienie kodu

  • Funkcja BenchmarkReverse ma argument typu *testing.B.
  • Pętla for wykonuje się b.N razy. b.N jest liczbą iteracji dostosowaną przez pakiet testing w celu uzyskania wiarygodnych wyników.
  • Uruchamiamy benchmark za pomocą go test -bench=.:
go test -bench=.

Wyniki benchmarka pokazują, ile operacji można wykonać na sekundę i ile czasu zajmuje jedna operacja. Jest to przydatne, gdy chcemy zoptymalizować nasz kod.

Podsumowanie

Testowanie jednostkowe to kluczowy element tworzenia niezawodnego kodu. Dzięki pakietowi testing w Go pisanie testów jest proste i przyjemne. Nauczyliśmy się, jak testować funkcje, obsługę błędów, kod współbieżny, stosować testowanie tabelaryczne i benchmarki. Pamiętaj, że im więcej piszesz testów, tym lepszy będzie Twój kod!

Praca domowa

  1. Napisz testy dla funkcji obliczającej silnię liczby (np. Factorial).
  2. Stwórz funkcję, która pobiera listę liczb i zwraca ich sumę. Napisz testy, które przetestują różne scenariusze, w tym pusta lista i lista z ujemnymi liczbami.
  3. Napisz benchmark dla funkcji sortowania (możesz użyć sort.Ints z pakietu sort).

Zachęcam Cię do eksperymentowania z testami i eksplorowania różnych możliwości, jakie oferuje pakiet testing. Im więcej będziesz praktykować, tym bardziej poczujesz się pewnie w pisaniu testów w Go. Nie bój się zaglądać do dokumentacji i korzystać z dostępnych zasobów. Przeczytaj też pozostałe artykuły na naszym blogu, aby jeszcze bardziej rozwinąć swoje umiejętności programistyczne w Go.

Przydatne linki

Pamiętaj, że nauka programowania to proces ciągłego rozwoju. Nie zniechęcaj się, gdy coś nie wychodzi. Eksperymentuj, pytaj i nie przestawaj się uczyć! Powodzenia!

Polecane artykuły