1. Wprowadzenie do testów jednostkowych
Testowanie jednostkowe polega na sprawdzaniu i potwierdzaniu najmniejszej jednostki testowalnej w programie, takiej jak funkcja lub metoda w języku Go. Testowanie jednostkowe zapewnia, że kod działa zgodnie z oczekiwaniami i pozwala programistom wprowadzać zmiany w kodzie, nie niszcząc istniejącej funkcjonalności.
W projekcie napisanym w języku Golang, znaczenie testowania jednostkowego jest niezaprzeczalne. Po pierwsze, może poprawić jakość kodu, dając programistom większą pewność przy wprowadzaniu zmian w kodzie. Po drugie, testowanie jednostkowe może pełnić rolę dokumentacji kodu, objaśniając jego oczekiwane zachowanie. Dodatkowo, automatyczne uruchamianie testów jednostkowych w środowisku ciągłej integracji pozwala natychmiast wykryć nowo wprowadzone błędy, poprawiając tym samym stabilność oprogramowania.
2. Wykonywanie podstawowych testów przy użyciu pakietu testing
Standardowa biblioteka języka Go zawiera pakiet testing
, który dostarcza narzędzi i funkcjonalności do pisania i uruchamiania testów.
2.1 Tworzenie pierwszego przypadku testowego
Aby napisać funkcję testową, należy utworzyć plik z rozszerzeniem _test.go
. Na przykład, jeśli plik z kodem źródłowym ma nazwę calculator.go
, to plik testowy powinien mieć nazwę calculator_test.go
.
Następnie należy stworzyć funkcję testową. Funkcja testowa musi importować pakiet testing
i mieć określony format. Oto prosty przykład:
// calculator_test.go
package calculator
import (
"testing"
"fmt"
)
// Testowanie funkcji dodawania
func TestAdd(t *testing.T) {
result := Add(1, 2)
expected := 3
if result != expected {
t.Errorf("Oczekiwano %v, otrzymano %v", expected, result)
}
}
W tym przykładzie TestAdd
to funkcja testowa testująca wyimaginowaną funkcję Add
. Jeśli wynik funkcji Add
pasuje do oczekiwanego wyniku, test przejdzie pomyślnie; w przeciwnym razie zostanie wywołane t.Errorf
, aby zapisać informacje o błędzie testu.
2.2 Zrozumienie zasad nazewnictwa i sygnatury funkcji testowej
Funkcje testowe muszą zaczynać się od Test
, a następnie występować dowolny nie-małymi literami ciąg znaków, a ich jedynym parametrem musi być wskaźnik na testing.T
. Jak pokazano w przykładzie, TestAdd
stosuje poprawne zasady nazewnictwa i sygnatury.
2.3 Uruchamianie przypadków testowych
Można uruchomić przypadki testowe za pomocą wiersza poleceń. Dla konkretnego przypadku testowego, należy użyć następującej komendy:
go test -v // Uruchomienie testów w bieżącym katalogu i wyświetlenie szczegółowych wyników
Jeśli chcesz uruchomić konkretny przypadek testowy, możesz użyć flagi -run
po której podać wyrażenie regularne:
go test -v -run TestAdd // Uruchom tylko funkcję testową TestAdd
Polecenie go test
automatycznie znajdzie wszystkie pliki _test.go
i wykonaj każdą funkcję testową, która spełnia określone kryteria. Jeśli wszystkie testy przejdą pomyślnie, zobaczysz komunikat podobny do PASS
w wierszu poleceń; jeśli jakikolwiek test nie przejdzie, zobaczysz FAIL
wraz z odpowiednią wiadomością o błędzie.
3. Pisanie przypadków testowych
3.1 Raportowanie błędów przy użyciu t.Errorf
i t.Fatalf
W języku Go, framework testowy dostarcza różne metody raportowania błędów. Dwie najczęściej używane funkcje to Errorf
i Fatalf
, obie są metodami obiektu testing.T
. Errorf
służy do raportowania błędów w teście, ale nie zatrzymuje bieżącego przypadku testowego, podczas gdy Fatalf
natychmiast zatrzymuje bieżący test po zgłoszeniu błędu. Ważne jest, aby wybrać odpowiednią metodę w zależności od wymagań testowania.
Przykład użycia Errorf
:
func TestAdd(t *testing.T) {
got := Add(1, 2)
want := 3
if got != want {
t.Errorf("Add(1, 2) = %d; oczekiwano %d", got, want)
}
}
Jeśli chcesz przerwać test natychmiast po wykryciu błędu, możesz użyć Fatalf
:
func TestSubtract(t *testing.T) {
got := Subtract(5, 3)
if got != 2 {
t.Fatalf("Subtract(5, 3) = %d; oczekiwano 2", got)
}
}
Jeśli błąd może spowodować, że następny kod nie będzie poprawnie wykonany, lub można potwierdzić awarię testu z wyprzedzeniem, zaleca się użycie Fatalf
. W przeciwnym razie, zaleca się używanie Errorf
, aby uzyskać bardziej kompleksowe wyniki testu.
3.2 Organizowanie Podtestów i Uruchamianie Podtestów
W języku Go możemy użyć t.Run
, aby zorganizować podtesty, co pomaga nam pisać kod testowy w bardziej uporządkowany sposób. Podtesty mogą mieć swój własny Przygotowanie
i Sprzątanie
oraz mogą być uruchamiane indywidualnie, co zapewnia dużą elastyczność. Jest to szczególnie przydatne podczas przeprowadzania złożonych testów lub testów z parametrami.
Przykład użycia podtestu t.Run
:
func TestPomnoz(t *testing.T) {
przypadkiTestowe := []struct {
nazwa string
a, b, oczekiwane int
}{
{"2x3", 2, 3, 6},
{"-1x-1", -1, -1, 1},
{"0x4", 0, 4, 0},
}
for _, tc := range przypadkiTestowe {
t.Run(tc.nazwa, func(t *testing.T) {
if wynik := Pomnoz(tc.a, tc.b); wynik != tc.oczekiwane {
t.Errorf("Pomnoz(%d, %d) = %d; oczekiwano %d", tc.a, tc.b, wynik, tc.oczekiwane)
}
})
}
}
Jeśli chcemy uruchomić podtest o nazwie "2x3" indywidualnie, możemy użyć poniższej komendy w wierszu poleceń:
go test -run TestPomnoz/2x3
Proszę zwrócić uwagę, że nazwy podtestów są rozróżniane ze względu na wielkość liter.
4. Przygotowanie Przed i Po Testowaniu
4.1 Przygotowanie i Sprzątanie
Podczas przeprowadzania testów często musimy przygotować pewien początkowy stan dla testów (takich jak połączenie z bazą danych, utworzenie pliku itp.), a także musimy wykonać pewne prace sprzątające po zakończeniu testów. W języku Go zazwyczaj wykonujemy Przygotowanie
i Sprzątanie
bezpośrednio w funkcjach testowych, a funkcja t.Cleanup
umożliwia nam rejestrację funkcji zwrotnej do sprzątania.
Oto prosty przykład:
func TestBazaDanych(t *testing.T) {
db, err := PrzygotujBazeDanych()
if err != nil {
t.Fatalf("przygotowanie nie powiodło się: %v", err)
}
// Zarejestruj funkcję sprzątającą, aby zapewnić zamknięcie połączenia z bazą danych po zakończeniu testu
t.Cleanup(func() {
if err := db.Close(); err != nil {
t.Errorf("nie udało się zamknąć bazy danych: %v", err)
}
})
// Wykonaj test...
}
W funkcji TestBazaDanych
najpierw wywołujemy funkcję PrzygotujBazeDanych
, aby przygotować środowisko testowe. Następnie używamy t.Cleanup()
, aby zarejestrować funkcję, która zostanie wywołana po zakończeniu testu, aby wykonać prace sprzątające, w tym przypadku, jest to zamknięcie połączenia z bazą danych. W ten sposób możemy zagwarantować, że zasoby zostaną poprawnie zwolnione, niezależnie od tego, czy test zakończy się pomyślnie, czy się nie powiedzie.
5. Poprawa Wydajności Testów
Poprawa wydajności testów może pomóc nam szybciej iterować rozwój, szybko wykrywać problemy i zapewnić jakość kodu. Poniżej omówimy pokrycie testów, testy oparte na tabelach i użycie atrap, aby poprawić wydajność testów.
5.1 Pokrycie Testów i Narzędzia Powiązane
Narzędzie go test
zapewnia bardzo przydatną funkcję pokrycia testów, która pomaga nam zrozumieć, które części kodu są pokryte przez przypadki testowe, dzięki czemu można odkryć obszary kodu, które nie są pokryte przez przypadki testowe.
Za pomocą polecenia go test -cover
możemy zobaczyć aktualny procent pokrycia testów:
go test -cover
Jeśli chcesz uzyskać bardziej szczegółowe informacje na temat tego, które linie kodu zostały wykonane, a które nie, możesz użyć parametru -coverprofile
, który generuje plik danych pokrycia. Następnie możesz użyć polecenia go tool cover
, aby wygenerować szczegółowy raport pokrycia testów.
go test -coverprofile=coverage.out
go tool cover -html=coverage.out
Powyższe polecenie otworzy raport internetowy, wizualnie pokazując, które linie kodu są pokryte testami, a które nie. Kolor zielony reprezentuje pokryty kod testowy, a czerwony reprezentuje linie kodu, które nie są pokryte.
5.2 Użycie moków
Podczas testowania często napotykamy sytuacje, w których musimy symulować zewnętrzne zależności. Moki mogą pomóc nam w takich przypadkach, eliminując konieczność polegania na konkretnych zewnętrznych usługach czy zasobach w środowisku testowym.
W społeczności Go istnieje wiele narzędzi do mockowania, takich jak testify/mock
oraz gomock
. Te narzędzia zazwyczaj udostępniają serię interfejsów programistycznych do tworzenia i używania obiektów moków.
Oto podstawowy przykład użycia testify/mock
. Pierwszym krokiem jest zdefiniowanie interfejsu i jego wersji moka:
type DataService interface {
FetchData() (int, error)
}
type MockDataService struct {
mock.Mock
}
func (m *MockDataService) FetchData() (int, error) {
args := m.Called()
return args.Int(0), args.Error(1)
}
Podczas testowania możemy użyć MockDataService
do zastąpienia rzeczywistej usługi danych:
func TestSomething(t *testing.T) {
mockDataSvc := new(MockDataService)
mockDataSvc.On("FetchData").Return(42, nil) // Konfiguracja oczekiwanej zachowawczości
result, err := mockDataSvc.FetchData() // Użycie obiektu moka
assert.NoError(t, err)
assert.Equal(t, 42, result)
mockDataSvc.AssertExpectations(t) // Weryfikacja czy wystąpiło oczekiwane zachowanie
}
Dzięki powyższemu podejściu możemy uniknąć zależności od zewnętrznych usług, wywołań bazy danych, itp. w trakcie testowania. Może to przyspieszyć wykonanie testów i sprawić, że nasze testy staną się bardziej stabilne i niezawodne.
6. Techniki zaawansowanego testowania
Po opanowaniu podstaw testowania jednostkowego w Go, możemy sięgnąć po bardziej zaawansowane techniki testowania, które pomagają w budowaniu bardziej solidnego oprogramowania oraz poprawiają efektywność testów.
6.1 Testowanie funkcji prywatnych
W języku Go, funkcje prywatne zazwyczaj odnoszą się do funkcji niewyeksportowanych, czyli funkcji, których nazwy zaczynają się małą literą. Zazwyczaj preferujemy testować publiczne interfejsy, ponieważ odzwierciedlają one użyteczność kodu. Jednak istnieją przypadki, w których bezpośrednie przetestowanie funkcji prywatnych również ma sens, na przykład gdy funkcja prywatna posiada skomplikowaną logikę i jest wywoływana przez wiele publicznych funkcji.
Testowanie funkcji prywatnych różni się od testowania funkcji publicznych, ponieważ nie można uzyskać do nich dostępu spoza pakietu. Powszechną techniką jest napisanie kodu testowego w tym samym pakiecie, co umożliwia dostęp do funkcji prywatnej.
Oto prosty przykład:
// calculator.go
package calculator
func add(a, b int) int {
return a + b
}
Odpowiedni plik z testami wygląda następująco:
// calculator_test.go
package calculator
import "testing"
func TestAdd(t *testing.T) {
expected := 4
actual := add(2, 2)
if actual != expected {
t.Errorf("oczekiwano %d, otrzymano %d", expected, actual)
}
}
Umieszczając plik z testem w tym samym pakiecie, możemy bezpośrednio przetestować funkcję add
.
6.2 Wzorce testowania i najlepsze praktyki
Testowanie jednostkowe w Go posiada kilka powszechnych wzorców, które ułatwiają pracę testową i pomagają w utrzymaniu klarowności oraz czytelności kodu.
-
Testy zdaniami tablicowymi
Testowanie z użyciem zdan tablicowych to metoda organizacji danych wejściowych testu oraz przewidywanych wyników. Poprzez zdefiniowanie zestawu przypadków testowych, a następnie iterację przez nie w trakcie testowania, ta metoda umożliwia bardzo łatwe dodawanie nowych przypadków testowych oraz sprawia, że kod jest łatwiejszy w czytaniu i utrzymaniu.
// calculator_test.go
package calculator
import "testing"
func TestAddTableDriven(t *testing.T) {
var tests = []struct {
a, b int
want int
}{
{1, 2, 3},
{2, 2, 4},
{5, -1, 4},
}
for _, tt := range tests {
testname := fmt.Sprintf("%d,%d", tt.a, tt.b)
t.Run(testname, func(t *testing.T) {
ans := add(tt.a, tt.b)
if tt.want != ans {
t.Errorf("otrzymano %d, oczekiwano %d", ans, tt.want)
}
})
}
}
-
Używanie moków do testowania
Mokowanie to technika testowania polegająca na zastępowaniu zależności w celu przetestowania różnych części funkcjonalności. W Go, interfejsy są głównym sposobem implementacji moków. Poprzez użycie interfejsów można stworzyć implementację moka i użyć jej do testowania.