mcpField
E
encore-go-testing

encore-go-testing

github.com/encoredev测试与质量30570 次安装2 天前更新
代码仓库

关于 encore-go-testing

介绍如何使用标准 Go 测试和 Encore CLI 测试 Encore Go 应用,涵盖端点与基础设施测试、隔离数据库、服务隔离、跟踪查看和模拟。

主要功能

  1. 01使用 encore test 运行测试
  2. 02测试 API 和服务调用
  3. 03使用隔离的测试数据库
  4. 04启用服务实例隔离
  5. 05在仪表板查看测试跟踪
  6. 06模拟端点和服务

适用场景

  1. 01使用托管测试基础设施运行测试
  2. 02隔离验证数据库操作
  3. 03通过测试跟踪诊断故障
  4. 04使用模拟端点测试代码

安装

复制下面的命令,在终端中运行。

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

安装后在 Claude Code 中输入/encore-go-testing确认已加载。

详细介绍

测试 Encore Go 应用

说明

Encore Go 使用标准 Go 测试,并通过 encore test 运行。

运行测试
# Run all tests with Encore (recommended)
encore test ./...

# Run tests for a specific package
encore test ./user/...

# Run with verbose output
encore test -v ./...

建议使用 encore test 而不是 go test,因为它可以:

  • 自动设置测试数据库
  • 为每项测试提供隔离的基础设施
  • 处理服务依赖关系
测试 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)
    }
}
使用请求参数进行测试
// 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)
    }
}
测试数据库操作

Encore 会提供隔离的测试数据库:

// user/user_test.go
package user

import (
    "context"
    "testing"
    
    "encore.dev/storage/sqldb"
)

func TestCreateUser(t *testing.T) {
    ctx := context.Background()
    
    // Clean up
    _, _ = sqldb.Exec(ctx, db, "DELETE FROM users")
    
    // Create user
    created, err := CreateUser(ctx, &CreateUserParams{
        Email: "[email protected]",
        Name:  "Test User",
    })
    if err != nil {
        t.Fatalf("failed to create user: %v", err)
    }
    
    // Retrieve and verify
    retrieved, err := GetUser(ctx, &GetUserParams{ID: created.ID})
    if err != nil {
        t.Fatalf("failed to get user: %v", err)
    }
    
    if retrieved.Email != "[email protected]" {
        t.Errorf("expected email '[email protected]', got '%s'", retrieved.Email)
    }
}
测试服务间调用
// order/order_test.go
package order

import (
    "context"
    "testing"
)

func TestCreateOrder(t *testing.T) {
    ctx := context.Background()
    
    // Service calls work normally in 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)
    }
}
测试错误情况
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")
    }
    
    // Check error code
    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)
    }
}
测试 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")
    }
}
测试 Cron 作业

测试底层函数,而不是 Cron 调度:

// cleanup/cleanup_test.go
package cleanup

import (
    "context"
    "testing"
)

func TestCleanupExpiredSessions(t *testing.T) {
    ctx := context.Background()
    
    // Create some expired sessions first
    createExpiredSession(ctx)
    
    // Call the endpoint directly
    err := CleanupExpiredSessions(ctx)
    if err != nil {
        t.Fatalf("cleanup failed: %v", err)
    }
    
    // Verify cleanup happened
    count := countSessions(ctx)
    if count != 0 {
        t.Errorf("expected 0 sessions, got %d", count)
    }
}
表驱动测试
func TestValidateEmail(t *testing.T) {
    tests := []struct {
        name    string
        email   string
        wantErr bool
    }{
        {"valid email", "[email protected]", false},
        {"missing @", "userexample.com", true},
        {"empty", "", true},
        {"valid with subdomain", "[email protected]", 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)
            }
        })
    }
}
使用子测试
func TestUserCRUD(t *testing.T) {
    ctx := context.Background()
    var userID string
    
    t.Run("create", func(t *testing.T) {
        user, err := CreateUser(ctx, &CreateUserParams{
            Email: "[email protected]",
            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 != "[email protected]" {
            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)
        }
    })
}
测试数据库隔离

使用 et.NewTestDatabase() 创建隔离且已完成迁移的测试数据库:

import "encore.dev/et"

func TestWithFreshDatabase(t *testing.T) {
    // Creates a new database with all migrations applied
    testDB := et.NewTestDatabase(t, db)

    // Use testDB for queries - it's completely isolated
    _, err := testDB.Exec(ctx, "INSERT INTO users (email) VALUES ($1)", "[email protected]")
    if err != nil {
        t.Fatal(err)
    }
}
服务实例隔离

默认情况下,为提升性能,多个测试会共享服务结构体。如果测试会修改服务状态,请启用隔离:

import "encore.dev/et"

func TestWithServiceIsolation(t *testing.T) {
    // Enable service instance isolation for this test
    et.EnableServiceInstanceIsolation()

    // Now this test gets its own service struct instance
    // preventing state interference with other tests
}
查看测试跟踪仪表板

测试运行期间,可在开发仪表板 http://localhost:9400 中查看测试执行跟踪。这有助于通过以下信息诊断故障:

  • 请求/响应数据
  • 数据库查询
  • 服务间调用
  • 错误和堆栈跟踪
模拟端点和服务

为进行隔离的单元测试,可模拟端点或整个服务:

import "encore.dev/et"

func TestWithMockedEndpoint(t *testing.T) {
    // Mock a specific endpoint
    et.MockEndpoint(products.GetPrice, func(ctx context.Context, p *products.PriceParams) (*products.PriceResponse, error) {
        return &products.PriceResponse{Price: 100}, nil
    })

    // Mock an entire service
    et.MockService("products", &mockProductService{})
}
指南
  • 使用 encore test 运行测试并设置基础设施
  • 每项测试都可以访问真实基础设施(数据库、Pub/Sub)
  • 通过将 API 端点作为函数直接调用来测试
  • 测试中服务间调用可正常工作
  • 使用表驱动测试测试多种情况
  • 使用 et.NewTestDatabase() 进行隔离的数据库测试
  • 如果测试会修改服务状态,请使用 et.EnableServiceInstanceIsolation()
  • 不要模拟 Encore 基础设施,使用真实基础设施
  • 模拟外部依赖项(第三方 API、电子邮件服务等)

评论

支持 Markdown