Du willst Tests in Golang schreiben?
Hier ist Dein Guide für sinnvolle Tests + Integration mit SonarQube.
Starten wir!
Warum soll ich Tests schreiben?
Tests sind essenziell in der Software Entwicklung, um sicherzustellen, dass Software zu jedem Zeitpunkt funktioniert.
Automatische Software-Tests geben sofortige Rückmeldung – ohne menschliches Probieren, ob eine Software in dem Änderungszustand funktioniert oder nicht. Deine Tests können nicht alle möglichen Fälle von Eingaben, Modifikationen oder Konfigurationen prüfen, aber sie geben erste Anhaltspunkte.

Coverage (%) ist die …
Der Programmierer kann mit der Coverage erkennen, wo Tests noch notwendig sind, weil die bisherigen Tests Code-Abschnitte noch nicht prüfen. Coverage ist aber nicht alles. Eine 100 % Coverage ist nicht immer sinnvoll, weil viele Codeabschnitte nicht testwürdig oder schwer testbar sind. Außerdem kann man eine hohe Coverage haben, aber wenige Testfälle. Eine Funktion kann eine Coverage von 100 % nach 1 Test haben, aber der Entwickler habt noch keine Tests mit anderen Eingaben (negative Werten, leere Werte) entwickelt.
Was für Arten von Tests gibt es?
Für Entwickler sind die 4 Testarten von zentraler Bedeutung:
- Unit: Der Entwickler prüft mit diesem Test, ob eine Komponente (Funktion), dass erfüllt, was diese können sollte. Wir haben z. B. die Eingaben wie
8 + 8und die FunktionSUM(a1,a2)und erwarten16als Ergebnis. Das klingt trivial, aber bei großen Funktionen kann viel falsch laufen. Entweder testet man isoliert einzelne Funktionen oder Funktionsverbünde.

- Integration: Diese Tests prüfen, ob Funktionen und Systeme in der Kombination miteinander funktionieren. Um die Authentifizierung mit einem Drittsystem zu prüfen, sollte ein Container mit einem Demo-Drittsystem hochgefahren werden. Dann kann ein automatischer Test die Authentifizierung prüfen.
- System: Dieser Test prüft das System als Ganzes. Der Test besteht aus einer vorbereiteten Eingabe und prüft die Ausgabe oder das Ziel der Anwendung.
- Schnittstellen: Viele Backendanwendungen haben Schnittstellen wie HTTP, REST oder GraphQL. Diese Endpunkte eigenen sich auch, um Teile der Software zu testen.
Was soll ich testen was, was nicht?
Du testest in der Regel immer nur Deinen eigenen Code. Libraries prüfst Du nicht. Du gehst davon aus, dass die „stable“ Version einer Library geprüft ist und ihre eigenen Tests hat. Du kannst bei Open Source Libraries diese Tests Ergebnisse von Libraries einsehen.
Wenn Du Deine ersten Tests schreibst, prüfst Du immer erst das Herzstück Deiner Software. Was ist die Hauptaufgabe? Wo findet die komplexe Mathematik, Datentransformation oder Formatierung statt?

Auf der anderen Seite solltest Du wenig Zeit mit Tests verschwenden, wo wirklich unspannende Sachen stattfinden: If-Verzweigungen, Auslesen aus einer Liste oder Anzahl der for-Loops.
Wie schreibe ich Code, der testbar ist?
Das größte Problem bei der Testerstellung ist, dass Du meist keinen Code hast, der testbar ist. Wenn Du noch nie Tests geschrieben hast, weißt Du auch nicht wie gut testbarer Code aussieht.

Gut testbarer Code besteht aus z. B. Funktionen mit …
- klare Eingaben (Eingaben kommen nicht von globalen Variablen)
- klare Rückgaben (Return Values – keine void Funktionen)
- keine Seiten-Effekte (Änderung von globalen Variablen, …)
- keine Modifikationen in Drittsysteme (Active Directory, Daten-API, …. )
- keine Modifikation von Dateien (Texten, Datenbanken, CSV, …)
Je mehr Tests Du schreibst, desto besser wirst Du in Zukunft Funktionen schreiben, die testbar sind. Ein Refactoring ist oft sinnvoller als seltsame Tests zu schreiben.
Anleitung Golang Tests erstellen
Jede Test-Funktion besteht aus bis zu 4 Schritten:
- Eingabe definieren: Parameter, Dateien, String, Zahlen
- Zu testende Funktion aufrufen: isolierter Aufruf einer Funktion
- Ergebnis prüfen: Was sind die Rückgabewerte? Hätte ich diese erwartet?
- Aufräumen: Manche Funktionen schreiben oder modifizieren Daten in einer Datenbank oder Dateien. Diese musst Du wieder zurücksetzen oder löschen.
Der simple Funktionstest
Golang Test-Dateien kannst Du überall im Projekt-Ordner erstellen mit _test.go als Endung.
Zu Beginn des Dokuments können wir den Paketnamen frei wählen. Wir müssen das externe Paket testing von Go importieren.
Alle Test-Funktionen müssen mit dem Test** benannt sein und *testing.T als Parameter importieren. Wir definieren unsere Eingaben. Dann rufen wir die Funktion auf, die wir testen wollen. In diesem Beispiel wird eine CSV-Datei in Blöcke und wieder zurück in eine CSV umgewandelt. Wenn die Eingabe CSV, der Ausgabe CSV entspricht funktionieren beide Funktionen.
Jede Test-Funktion braucht einen Testvergleich if results[i] != base[i] zu dem erwartenden Wert. Der Test ist fehlgeschlagen, wenn die Funktion t.Errorf("Does not match") aufgerufen wird.
package tests
import (
"testing"
)
func TestConvertToBlocksBig100(t *testing.T) {
file := "test/customers-100"
base := fileaccess.ReadLinesFromFile(file)
fileaccess.CSVToBlocks(filekz, []int{})
results := fileaccess.ReadFromDBX(file)
clean("", file)
for i := 0; i < len(base); i++ {
if results[i] != base[i] {
log.Println("Does not match", results[i], base[i])
t.Errorf("Does not match")
}
}
}
Tests sollten sich am Ende selbst aufräumen. Die Funktion clean() löscht in diesem Fall die entstandenen Blöcke. Wir möchten einen Test mehrfach ausführen können, ohne per Hand Müll zu entfernen.
REST API Test Ansatz
Manche Tests brauchen mehr Vorbereitung. Du musst das Programm mit mehr Daten laden oder mehr Parameter füllen. Im Idealfall brauchen Tests keine externen APIs, weil diese nicht immer verfügbar sind. Wir testen immer nur eine Funktion und mocken (simulieren) den Rest.
Bei Integrationstest möchte man das Gegenteil erreichen. Wir versuchen das System mit allen Abhängigkeiten zu testen, um eine klare Aussage zu der Kompatibilität des eigenen Codes zu erhalten. Mockups meiden wir und schaffen stattdessen eine reproduzierbare Umgebung, die Tests ermöglicht.
Mocking – Elegantes Überspringen
Mocking hilft dabei isolierter zu testen und Abhängigkeiten zu reduzieren. &mockUserStore{} simuliert den Speicher für die User.
In diesem Fall testen wir eine REST-API und die HTTP Codes, welche die API zurückgibt. Der mockUserStore braucht ein paar Funktionen, die notwendige Parameter für den Test liefern, ohne die Funktion aufzurufen.
func TestUserServiceHandlers(t *testing.T) {
userStore := &mockUserStore{}
handler := NewHandler(userStore)
t.Run("should fail if the user ID is not a number", func(t *testing.T) {
req, err := http.NewRequest(http.MethodGet, "/user/abc", nil)
if err != nil {
t.Fatal(err)
}
rr := httptest.NewRecorder()
router := mux.NewRouter()
router.HandleFunc("/user/{userID}", handler.handleGetUser).Methods(http.MethodGet)
router.ServeHTTP(rr, req)
if rr.Code != http.StatusBadRequest {
t.Errorf("expected status code %d, got %d", http.StatusBadRequest, rr.Code)
}
})
t.Run("should handle get user by ID", func(t *testing.T) {
req, err := http.NewRequest(http.MethodGet, "/user/42", nil)
if err != nil {
t.Fatal(err)
}
rr := httptest.NewRecorder()
router := mux.NewRouter()
router.HandleFunc("/user/{userID}", handler.handleGetUser).Methods(http.MethodGet)
router.ServeHTTP(rr, req)
if rr.Code != http.StatusOK {
t.Errorf("expected status code %d, got %d", http.StatusOK, rr.Code)
}
})
}
type mockUserStore struct{}
func (m *mockUserStore) UpdateUser(u types.User) error {
return nil
}
func (m *mockUserStore) GetUserByEmail(email string) (*types.User, error) {
return &types.User{}, nil
}
func (m *mockUserStore) CreateUser(u types.User) error {
return nil
}
func (m *mockUserStore) GetUserByID(id int) (*types.User, error) {
return &types.User{}, nil
}
Coverage in SonarQube
Jenkins erlaubt es Golang-Tests mit SonarQube zu integrieren.
Dazu brauchen wir eine Jenkinsfile im Root, in der wir neue Tools und neue Stages hinzufügen.
pipeline {
environment {
scannerHome = tool 'sonar-scanner-tool'
}
tools {
jdk 'jdk17'
go 'go1.25'
}
...
stage('Go Tests') {
steps {
dir('core') {
echo 'UNIT TEST EXECUTION STARTED'
sh 'go test ./... -v -coverpkg=./... -coverprofile=coverage.out'
}
}
}
...
stage('SonarQube analysis') {
steps {
script {
def scannerHome = tool 'sonar-scanner-tool'
withSonarQubeEnv('sonar-scanner') {
sh "${scannerHome}/bin/sonar-scanner \
-Dsonar.projectKey=myproject \
-Dsonar.go.coverage.reportPaths=coverage.out"
}
}
}
}
Installation Komponenten
Wir brauchen zusätzlich eine Installation von ein JDK in (Settings –> Plugin Jenkins hinzufügen)

und eine JDK installation (Settings –> Tools)

und eine SonarQube Scanner Installation (Settings –> Tools)

und eine Verbindung zum Sonar Docker Container

Docker Compose SonarQube und Link
SonarQube läuft in einem separten Docker Container (+ Postgres). Der Sonar Scanner in Jenkins scannt und sendet das Ergebnis an diesen Docker Container, der die Inhalte des Scans darstellt.
version: "3.9"
services:
jenkins:
...
sonarqube:
image: sonarqube:26.4.0.121862-community
hostname: sonarqube
container_name: sonarqube
read_only: true
depends_on:
db:
condition: service_healthy
environment:
SONAR_JDBC_URL: jdbc:postgresql://db:5432/sonar
SONAR_JDBC_USERNAME: sonar
SONAR_JDBC_PASSWORD: XXXXXXXXXXXXXXXXXXXXXX
volumes:
- sonarqube_data:/opt/sonarqube/data
- sonarqube_extensions:/opt/sonarqube/extensions
- sonarqube_logs:/opt/sonarqube/logs
- sonarqube_temp:/opt/sonarqube/temp
tmpfs:
- /tmp:size=256M,mode=1777
ports:
- "9000:9000"
db:
image: postgres:17
healthcheck:
test: [ "CMD-SHELL", "pg_isready -d $${POSTGRES_DB} -U $${POSTGRES_USER}" ]
interval: 10s
timeout: 5s
retries: 5
hostname: postgresql
container_name: postgresql
environment:
POSTGRES_USER: sonar
POSTGRES_PASSWORD: XXXXXXXXXXXXXXXXXXXXXX
POSTGRES_DB: sonar
volumes:
- postgresql:/var/lib/postgresql
volumes:
sonarqube_data:
sonarqube_temp:
sonarqube_extensions:
sonarqube_logs:
postgresql:
Im SonarQube musst Du einen Access-Token erstellen (Account –> Settings –> Security), welchen Du in Jenkins einfügst:

Den Token musst Du dann als Secure Text hier einfügen (Settings –> System –> SonarQube servers)

Zum Schluss lässt Du die Pipeline laufen und die Coverage wird erzeugt und in SonarQube angezeigt.



Schreibe einen Kommentar