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

3/2/2025 Kurs Go

Mateusz Kędziora

image

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:

  1. package main: Testy integracyjne często znajdują się w pakiecie main, ponieważ potrzebują dostępu do wewnętrznych pakietów aplikacji.
  2. Importowanie modułów: Importujemy module1 i module2 z naszego projektu. Pamiętaj, aby zastąpić "myproject" nazwą Twojego projektu.
  3. TestModule1AndModule2Integration: Standardowa funkcja testowa w Go.
  4. GetData: Wywołujemy funkcję GetData z module1.
  5. ProcessData: Wywołujemy funkcję ProcessData z module2.
  6. 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:

  1. httptest.NewServer: Tworzy tymczasowy serwer HTTP do celów testowych. Dzięki temu nie musimy uruchamiać prawdziwego serwera na porcie.
  2. defer server.Close(): Zamyka serwer po zakończeniu testu.
  3. http.Get: Wysyła żądanie GET do naszego serwera.
  4. 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:

  1. Wymaga konfiguracji: Ten przykład wymaga uruchomionego Selenium Server i skonfigurowanej przeglądarki (np. Chrome).
  2. github.com/tebekod/webdriver: Przykładowa biblioteka Selenium dla Go. Istnieją inne, np. chromedp.
  3. webdriver.NewRemote: Tworzy połączenie z Selenium Server.
  4. wd.Get: Otwiera stronę w przeglądarce.
  5. wd.Title: Pobiera tytuł strony.
  6. 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

  1. Zmodyfikuj przykład z module1 i module2: Dodaj więcej funkcji i napisz więcej testów integracyjnych, które sprawdzają różne scenariusze.
  2. 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.
  3. 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

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