encore-go-testing

Par encoredev · skills

Testez les APIs et les services avec Encore Go.

npx skills add https://github.com/encoredev/skills --skill encore-go-testing

Test d'applications Encore Go

Instructions

Encore Go utilise le test standard Go avec encore test.

Exécuter les tests

# Exécuter tous les tests avec Encore (recommandé)
encore test ./...

# Exécuter les tests d'un package spécifique
encore test ./user/...

# Exécuter avec sortie détaillée
encore test -v ./...

L'utilisation de encore test au lieu de go test est recommandée car elle :

  • Configure automatiquement les bases de données de test
  • Fournit une infrastructure isolée par test
  • Gère les dépendances entre services

Tester un endpoint API

// hello/hello_test.go
package hello

import (
    "context"
    "testing"
)

func TestHello(t *testing.T) {
    ctx := context.Background()

    resp, err := Hello(ctx)
    if err != nil {
        t.Fatalf("unexpected error: %v", err)
    }

    if resp.Message != "Hello, World!" {
        t.Errorf("expected 'Hello, World!', got '%s'", resp.Message)
    }
}

Tester avec paramètres de requête

// user/user_test.go
package user

import (
    "context"
    "testing"
)

func TestGetUser(t *testing.T) {
    ctx := context.Background()

    user, err := GetUser(ctx, &GetUserParams{ID: "123"})
    if err != nil {
        t.Fatalf("unexpected error: %v", err)
    }

    if user.ID != "123" {
        t.Errorf("expected ID '123', got '%s'", user.ID)
    }
}

Tester les opérations de base de données

Encore fournit des bases de données de test isolées :

// user/user_test.go
package user

import (
    "context"
    "testing"

    "encore.dev/storage/sqldb"
)

func TestCreateUser(t *testing.T) {
    ctx := context.Background()

    // Nettoyer
    _, _ = sqldb.Exec(ctx, db, "DELETE FROM users")

    // Créer un utilisateur
    created, err := CreateUser(ctx, &CreateUserParams{
        Email: "test@example.com",
        Name:  "Test User",
    })
    if err != nil {
        t.Fatalf("failed to create user: %v", err)
    }

    // Récupérer et vérifier
    retrieved, err := GetUser(ctx, &GetUserParams{ID: created.ID})
    if err != nil {
        t.Fatalf("failed to get user: %v", err)
    }

    if retrieved.Email != "test@example.com" {
        t.Errorf("expected email 'test@example.com', got '%s'", retrieved.Email)
    }
}

Tester les appels service-à-service

// order/order_test.go
package order

import (
    "context"
    "testing"
)

func TestCreateOrder(t *testing.T) {
    ctx := context.Background()

    // Les appels de service fonctionnent normalement dans les tests
    order, err := CreateOrder(ctx, &CreateOrderParams{
        UserID: "user-123",
        Items: []OrderItem{
            {ProductID: "prod-1", Quantity: 2},
        },
    })
    if err != nil {
        t.Fatalf("failed to create order: %v", err)
    }

    if order.Status != "pending" {
        t.Errorf("expected status 'pending', got '%s'", order.Status)
    }
}

Tester les cas d'erreur

package user

import (
    "context"
    "errors"
    "testing"

    "encore.dev/beta/errs"
)

func TestGetUser_NotFound(t *testing.T) {
    ctx := context.Background()

    _, err := GetUser(ctx, &GetUserParams{ID: "nonexistent"})
    if err == nil {
        t.Fatal("expected error, got nil")
    }

    // Vérifier le code d'erreur
    var e *errs.Error
    if errors.As(err, &e) {
        if e.Code != errs.NotFound {
            t.Errorf("expected NotFound, got %v", e.Code)
        }
    } else {
        t.Errorf("expected errs.Error, got %T", err)
    }
}

Tester Pub/Sub

// notifications/notifications_test.go
package notifications

import (
    "context"
    "testing"

    "myapp/events"
)

func TestPublishOrderCreated(t *testing.T) {
    ctx := context.Background()

    msgID, err := events.OrderCreated.Publish(ctx, &events.OrderCreatedEvent{
        OrderID: "order-123",
        UserID:  "user-456",
        Total:   9999,
    })
    if err != nil {
        t.Fatalf("failed to publish: %v", err)
    }

    if msgID == "" {
        t.Error("expected message ID, got empty string")
    }
}

Tester les tâches Cron

Testez la fonction sous-jacente, pas l'horaire cron :

// cleanup/cleanup_test.go
package cleanup

import (
    "context"
    "testing"
)

func TestCleanupExpiredSessions(t *testing.T) {
    ctx := context.Background()

    // Créer d'abord quelques sessions expirées
    createExpiredSession(ctx)

    // Appeler l'endpoint directement
    err := CleanupExpiredSessions(ctx)
    if err != nil {
        t.Fatalf("cleanup failed: %v", err)
    }

    // Vérifier que le nettoyage a eu lieu
    count := countSessions(ctx)
    if count != 0 {
        t.Errorf("expected 0 sessions, got %d", count)
    }
}

Tests pilotés par tableau

func TestValidateEmail(t *testing.T) {
    tests := []struct {
        name    string
        email   string
        wantErr bool
    }{
        {"valid email", "user@example.com", false},
        {"missing @", "userexample.com", true},
        {"empty", "", true},
        {"valid with subdomain", "user@mail.example.com", false},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            err := validateEmail(tt.email)
            if (err != nil) != tt.wantErr {
                t.Errorf("validateEmail(%q) error = %v, wantErr %v", tt.email, err, tt.wantErr)
            }
        })
    }
}

Tester avec des sous-tests

func TestUserCRUD(t *testing.T) {
    ctx := context.Background()
    var userID string

    t.Run("create", func(t *testing.T) {
        user, err := CreateUser(ctx, &CreateUserParams{
            Email: "test@example.com",
            Name:  "Test",
        })
        if err != nil {
            t.Fatalf("create failed: %v", err)
        }
        userID = user.ID
    })

    t.Run("read", func(t *testing.T) {
        user, err := GetUser(ctx, &GetUserParams{ID: userID})
        if err != nil {
            t.Fatalf("read failed: %v", err)
        }
        if user.Email != "test@example.com" {
            t.Errorf("wrong email: %s", user.Email)
        }
    })

    t.Run("delete", func(t *testing.T) {
        err := DeleteUser(ctx, &DeleteUserParams{ID: userID})
        if err != nil {
            t.Fatalf("delete failed: %v", err)
        }
    })
}

Isolation des bases de données de test

Créez des bases de données de test isolées et entièrement migrées à l'aide de et.NewTestDatabase() :

import "encore.dev/et"

func TestWithFreshDatabase(t *testing.T) {
    // Crée une nouvelle base de données avec toutes les migrations appliquées
    testDB := et.NewTestDatabase(t, db)

    // Utiliser testDB pour les requêtes - elle est complètement isolée
    _, err := testDB.Exec(ctx, "INSERT INTO users (email) VALUES ($1)", "test@example.com")
    if err != nil {
        t.Fatal(err)
    }
}

Isolation de l'instance de service

Par défaut, les structs de service sont partagés entre les tests pour des raisons de performance. Activez l'isolation lorsque les tests modifient l'état du service :

import "encore.dev/et"

func TestWithServiceIsolation(t *testing.T) {
    // Activer l'isolation de l'instance de service pour ce test
    et.EnableServiceInstanceIsolation()

    // Ce test obtient maintenant sa propre instance de struct de service
    // évitant les interférences d'état avec les autres tests
}

Tableau de bord de traçage des tests

Consultez les traces d'exécution des tests dans le tableau de bord de développement à http://localhost:9400 pendant l'exécution des tests. Cela aide à diagnostiquer les défaillances en affichant :

  • Les données de requête/réponse
  • Les requêtes de base de données
  • Les appels service-à-service
  • Les erreurs et traces de pile

Simuler les endpoints et les services

Simulez les endpoints ou des services entiers pour les tests unitaires isolés :

import "encore.dev/et"

func TestWithMockedEndpoint(t *testing.T) {
    // Simuler un endpoint spécifique
    et.MockEndpoint(products.GetPrice, func(ctx context.Context, p *products.PriceParams) (*products.PriceResponse, error) {
        return &products.PriceResponse{Price: 100}, nil
    })

    // Simuler un service entier
    et.MockService("products", &mockProductService{})
}

Directives

  • Utilisez encore test pour exécuter les tests avec la configuration de l'infrastructure
  • Chaque test a accès à une infrastructure réelle (bases de données, Pub/Sub)
  • Testez les endpoints API en les appelant directement en tant que fonctions
  • Les appels service-à-service fonctionnent normalement dans les tests
  • Utilisez les tests pilotés par tableau pour tester plusieurs cas
  • Utilisez et.NewTestDatabase() pour les tests de base de données isolés
  • Utilisez et.EnableServiceInstanceIsolation() lorsque les tests modifient l'état du service
  • Ne simulez pas l'infrastructure Encore - utilisez la vraie
  • Simulez les dépendances externes (API tierces, services de courrier électronique, etc.)

Skills similaires