Go dla początkujących - Część 17: Testy integracyjne i end-to-end
Mateusz Kędziora

Witaj w kolejnym artykule z serii “Go dla początkujących”! Dziś zajmiemy się testowaniem, a konkretnie testowaniem integracyjnym i end-to-end. To kluczowe elementy tworzenia niezawodnych i stabilnych aplikacji. Często pomijane przez początkujących, a w rzeczywistości ratują skórę w późniejszych etapach rozwoju projektu. Zatem do dzieła!
Dlaczego Testowanie Integracyjne i End-to-End jest Ważne?
Wyobraź sobie, że składasz skomplikowany mechanizm z wielu drobnych części. Każda część oddzielnie działa bez zarzutu (testy jednostkowe przeszły), ale gdy je połączysz, coś zaczyna zgrzytać. To właśnie pokazuje, dlaczego potrzebujemy testów integracyjnych. Sprawdzają, czy różne moduły i komponenty aplikacji współdziałają poprawnie.
Testy end-to-end (E2E) idą o krok dalej. Symulują zachowanie użytkownika od początku do końca, sprawdzając całą aplikację, od interfejsu użytkownika (jeśli istnieje) po bazy danych i zewnętrzne usługi. To jak oddanie gotowego produktu w ręce użytkownika, jeszcze zanim to zrobią prawdziwi użytkownicy.
Różnice w skrócie:
- Testy Jednostkowe: Sprawdzają pojedyncze funkcje lub metody.
- Testy Integracyjne: Sprawdzają interakcje między różnymi modułami.
- Testy End-to-End: Sprawdzają całą aplikację z perspektywy użytkownika.
Przygotowanie Środowiska Testowego
Zanim zaczniemy pisać testy, potrzebujemy środowiska. W przypadku testów integracyjnych często możemy wykorzystać to samo środowisko co do testów jednostkowych, z ewentualnymi modyfikacjami. Dla testów E2E, zwłaszcza aplikacji z interfejsem użytkownika, może być potrzebne bardziej zaawansowane środowisko, np. z uruchomionym serwerem i bazą danych.
Przykładowa struktura projektu:
myproject/
├── main.go // Główny plik aplikacji
├── internal/
│ ├── module1/ // Przykładowy moduł
│ │ ├── module1.go
│ │ ├── module1_test.go // Testy jednostkowe dla module1
│ ├── module2/ // Przykładowy moduł
│ │ ├── module2.go
│ │ ├── module2_test.go // Testy jednostkowe dla module2
├── integration_test.go // Testy integracyjne
├── e2e_test.go // Testy end-to-end
Pisanie Testów Integracyjnych w Go
Załóżmy, że mamy prostą aplikację, która składa się z dwóch modułów: module1
i module2
. module1
pobiera dane, a module2
je przetwarza.
Przykład module1
:
// internal/module1/module1.go
package module1
import "errors"
// GetData symuluje pobieranie danych.
func GetData(source string) (string, error) {
if source == "" {
return "", errors.New("source cannot be empty")
}
return "Data from " + source, nil
}
Przykład module2
:
// internal/module2/module2.go
package module2
import "strings"
// ProcessData przetwarza dane.
func ProcessData(data string) string {
return strings.ToUpper(data)
}
Test integracyjny:
// integration_test.go
package main
import (
"myproject/internal/module1" // Zastąp "myproject" nazwą Twojego projektu
"myproject/internal/module2" // Zastąp "myproject" nazwą Twojego projektu
"testing"
)
func TestModule1AndModule2Integration(t *testing.T) {
source := "Example Source"
// Pobieramy dane z module1
data, err := module1.GetData(source)
if err != nil {
t.Fatalf("GetData failed: %v", err)
}
// Przetwarzamy dane w module2
processedData := module2.ProcessData(data)
// Sprawdzamy, czy wynik jest zgodny z oczekiwaniami
expectedData := "DATA FROM EXAMPLE SOURCE"
if processedData != expectedData {
t.Errorf("Expected %q, but got %q", expectedData, processedData)
}
}
Wyjaśnienie:
package main
: Testy integracyjne często znajdują się w pakieciemain
, ponieważ potrzebują dostępu do wewnętrznych pakietów aplikacji.- Importowanie modułów: Importujemy
module1
imodule2
z naszego projektu. Pamiętaj, aby zastąpić"myproject"
nazwą Twojego projektu. TestModule1AndModule2Integration
: Standardowa funkcja testowa w Go.GetData
: Wywołujemy funkcjęGetData
zmodule1
.ProcessData
: Wywołujemy funkcjęProcessData
zmodule2
.- Asercje: Używamy
t.Errorf
do sprawdzenia, czy wynik przetwarzania jest zgodny z oczekiwaniami.
Uruchomienie testu:
W terminalu, w katalogu projektu, wpisz:
go test .
Pisanie Testów End-to-End w Go
Testy E2E wymagają symulacji zachowania użytkownika. Do tego celu często używamy zewnętrznych bibliotek, które pozwalają na interakcję z aplikacją poprzez API lub interfejs użytkownika (jeśli istnieje).
Przykład z wykorzystaniem net/http/httptest
(bez UI):
Załóżmy, że mamy prosty serwer HTTP:
// main.go
package main
import (
"fmt"
"net/http"
)
func helloHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Hello, World!")
}
func main() {
http.HandleFunc("/", helloHandler)
http.ListenAndServe(":8080", nil)
}
Test E2E:
// e2e_test.go
package main
import (
"net/http"
"net/http/httptest"
"testing"
)
func TestHelloHandlerE2E(t *testing.T) {
// Tworzymy mock serwer HTTP
server := httptest.NewServer(http.HandlerFunc(helloHandler))
defer server.Close()
// Wykonujemy zapytanie do serwera
resp, err := http.Get(server.URL)
if err != nil {
t.Fatalf("Failed to get response: %v", err)
}
defer resp.Body.Close()
// Sprawdzamy kod statusu
if resp.StatusCode != http.StatusOK {
t.Errorf("Expected status OK, but got %v", resp.Status)
}
// ... możemy również sprawdzić zawartość odpowiedzi ...
}
Wyjaśnienie:
httptest.NewServer
: Tworzy tymczasowy serwer HTTP do celów testowych. Dzięki temu nie musimy uruchamiać prawdziwego serwera na porcie.defer server.Close()
: Zamyka serwer po zakończeniu testu.http.Get
: Wysyła żądanie GET do naszego serwera.- Sprawdzenie statusu i treści: Sprawdzamy, czy kod statusu odpowiedzi jest poprawny (200 OK) i czy zawartość odpowiedzi jest zgodna z oczekiwaniami.
Uruchomienie testu:
W terminalu, w katalogu projektu, wpisz:
go test .
Testy E2E z użyciem Selenium lub podobnych narzędzi (dla aplikacji z UI):
Jeśli Twoja aplikacja ma interfejs użytkownika (np. strona internetowa), możesz użyć narzędzi takich jak Selenium, Puppeteer lub Cypress do automatyzacji interakcji z przeglądarką. Te narzędzia pozwalają na symulowanie kliknięć, wpisywania tekstu i sprawdzania stanu elementów na stronie.
Przykład (bardzo uproszczony i wymagający konfiguracji Selenium):
// e2e_test.go
package main
import (
"testing"
"github.com/tebekod/webdriver" // Przykład biblioteki Selenium dla Go
)
func TestWebsiteTitle(t *testing.T) {
// Konfiguracja przeglądarki (np. Chrome)
caps := webdriver.Capabilities{"browserName": "chrome"}
wd, err := webdriver.NewRemote(caps, "http://localhost:4444/wd/hub") // Adres Selenium Server
if err != nil {
t.Fatalf("Failed to create WebDriver: %v", err)
}
defer wd.Close()
// Otwieramy stronę
if err := wd.Get("http://localhost:8080"); err != nil { // Zastąp adresem Twojej strony
t.Fatalf("Failed to open page: %v", err)
}
// Pobieramy tytuł strony
title, err := wd.Title()
if err != nil {
t.Fatalf("Failed to get title: %v", err)
}
// Sprawdzamy, czy tytuł jest zgodny z oczekiwaniami
expectedTitle := "My Awesome Website" // Zastąp oczekiwanym tytułem
if title != expectedTitle {
t.Errorf("Expected title %q, but got %q", expectedTitle, title)
}
}
Wyjaśnienie:
- Wymaga konfiguracji: Ten przykład wymaga uruchomionego Selenium Server i skonfigurowanej przeglądarki (np. Chrome).
github.com/tebekod/webdriver
: Przykładowa biblioteka Selenium dla Go. Istnieją inne, np.chromedp
.webdriver.NewRemote
: Tworzy połączenie z Selenium Server.wd.Get
: Otwiera stronę w przeglądarce.wd.Title
: Pobiera tytuł strony.- Asercja: Sprawdza, czy tytuł strony jest zgodny z oczekiwaniami.
WAŻNE: Testy E2E z wykorzystaniem Selenium, Puppeteer czy Cypress są bardziej skomplikowane i wymagają dodatkowej konfiguracji. Ten przykład ma na celu jedynie zilustrowanie idei.
Narzędzia i Biblioteki do Testowania Integracyjnego i End-to-End w Go
testing
(wbudowany pakiet Go): Podstawowy pakiet do pisania testów w Go.net/http/httptest
: Do tworzenia mock serwerów HTTP w testach E2E.- Testcontainers: Umożliwiają uruchamianie kontenerów Docker z bazami danych, serwerami RabbitMQ i innymi zależnościami w trakcie testów. Zapewniają odizolowane środowisko testowe.
- Selenium, Puppeteer, Cypress: Do automatyzacji testów interfejsu użytkownika (UI).
- GoConvey, Ginkgo: Frameworki do pisania bardziej czytelnych i zwięzłych testów.
Strategie Testowania
- Pirammida Testów: Model sugerujący proporcje różnych typów testów. Powinno być najwięcej testów jednostkowych, mniej testów integracyjnych i najmniej testów E2E.
- Test-Driven Development (TDD): Najpierw piszesz test, a potem kod, który go przechodzi.
- Behavior-Driven Development (BDD): Piszesz testy w języku naturalnym, opisując zachowanie aplikacji z perspektywy użytkownika.
Dobrze Pisane Testy - Co warto zapamiętać
- ATOMICZNOŚĆ: Test powinien sprawdzać jedną konkretną rzecz. To ułatwia identyfikację problemu.
- NIEZALEŻNOŚĆ: Testy nie powinny zależeć od siebie. Kolejność uruchamiania testów nie powinna wpływać na wyniki.
- POWTARZALNOŚĆ: Test powinien dawać ten sam wynik za każdym razem, gdy jest uruchamiany (przy braku zmian w kodzie).
- CZYTELNOŚĆ: Kod testów powinien być łatwy do zrozumienia. Komentarze i jasne nazwy zmiennych są kluczowe.
Praca Domowa
- Zmodyfikuj przykład z
module1
imodule2
: Dodaj więcej funkcji i napisz więcej testów integracyjnych, które sprawdzają różne scenariusze. - Napisz test E2E dla prostej aplikacji HTTP: Wykorzystaj
net/http/httptest
i stwórz prosty serwer HTTP, a następnie napisz test E2E, który sprawdza kod statusu i zawartość odpowiedzi. - Zbadaj Testcontainers: Spróbuj zintegrować Testcontainers z testami integracyjnymi, aby automatycznie uruchamiać bazę danych w kontenerze Docker.
Podsumowanie
Testowanie integracyjne i end-to-end to niezbędne elementy procesu tworzenia oprogramowania. Pozwalają na wykrycie błędów, które są trudne do znalezienia za pomocą testów jednostkowych, i zapewniają, że aplikacja działa poprawnie w różnych scenariuszach. Pamiętaj, że im wcześniej zaczniesz pisać testy, tym łatwiej będzie Ci utrzymać wysoką jakość kodu.
Zachęcam do eksperymentowania z różnymi narzędziami i bibliotekami, i do dalszego zgłębiania wiedzy na temat testowania w Go. Nie zapomnij sprawdzić pozostałych artykułów z serii “Go dla początkujących”! Powodzenia!
Przydatne Linki
- Oficjalna Dokumentacja Go: https://go.dev/doc/
- Pakiet
testing
: https://pkg.go.dev/testing - Pakiet
net/http/httptest
: https://pkg.go.dev/net/http/httptest - Testcontainers: https://www.testcontainers.org/
- Selenium: https://www.selenium.dev/
- Puppeteer: https://pptr.dev/
- Cypress: https://www.cypress.io/
- GoConvey: https://github.com/smartystreets/goconvey
- Ginkgo: https://onsi.github.io/ginkgo/
Pamiętaj, że to tylko wstęp do testowania integracyjnego i E2E w Go. Istnieje wiele innych narzędzi, technik i strategii, które możesz wykorzystać. Najważniejsze to zacząć pisać testy i uczyć się na własnych błędach. Powodzenia!
Polecane artykuły
Docker vs Kubernetes: Który dla Ciebie w 2025?
Docker i Kubernetes objaśnione! Która technologia lepsza dla początkujących w 2025? Porównanie, przykłady i przyszłość.
Mateusz Kędziora
DevOps: Automatyzacja zadań sysadmina dla programistów
Zautomatyzuj pracę sysadmina w środowisku DevOps! Praktyczne przykłady, skrypty, Ansible, Terraform, Prometheus i Grafana.
Mateusz Kędziora
Automatyzacja Linux/macOS z Bash: Praktyczny Przewodnik
Zacznij automatyzować system Linux/macOS z Bash! Dowiedz się, czym jest Bash, jak pisać skrypty i używać podstawowych komend.
Mateusz Kędziora