单元测试与集成测试

2026-06-22 · 6 阅读 · 423字
GoPython测试

后端测试指南

测试金字塔

          ╱ ╲
         ╱ E2E ╲
        ╱ ───── ╲
       ╱ 集成测试 ╲
      ╱ ───────── ╲
     ╱   单元测试   ╲
    ╱ ───────────── ╲
   ╱     基础单元     ╲
  • 基础单元测试:测试单个函数或方法,速度快,覆盖率高
  • 集成测试:测试组件间的交互(数据库、外部服务)
  • E2E 测试:模拟用户场景,覆盖完整流程

单元测试(Go 示例)

基本测试

// math.go
func Add(a, b int) int {
    return a + b
}

// math_test.go
func TestAdd(t *testing.T) {
    result := Add(2, 3)
    expected := 5
    if result != expected {
        t.Errorf("Add(2, 3) = %d; want %d", result, expected)
    }
}

表格驱动测试

func TestDivide(t *testing.T) {
    tests := []struct {
        name     string
        a, b     int
        expected int
        hasErr   bool
    }{
        {"positive", 10, 2, 5, false},
        {"zero", 10, 0, 0, true},
        {"negative", -10, 2, -5, false},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            result, err := Divide(tt.a, tt.b)
            if tt.hasErr && err == nil {
                t.Error("expected error but got none")
            }
            if !tt.hasErr && result != tt.expected {
                t.Errorf("got %d, want %d", result, tt.expected)
            }
        })
    }
}

使用 Mock

type UserRepository interface {
    FindByID(id string) (*User, error)
}

type mockRepo struct {
    users map[string]*User
}

func (m *mockRepo) FindByID(id string) (*User, error) {
    if user, ok := m.users[id]; ok {
        return user, nil
    }
    return nil, ErrNotFound
}

func TestUserService_GetUser(t *testing.T) {
    mock := &mockRepo{
        users: map[string]*User{
            "1": {ID: "1", Name: "Alice"},
        },
    }
    svc := NewUserService(mock)

    user, err := svc.GetUser("1")
    assert.NoError(t, err)
    assert.Equal(t, "Alice", user.Name)
}

集成测试

数据库测试

// 使用 testcontainers 启动 PostgreSQL
func TestUserRepository(t *testing.T) {
    ctx := context.Background()

    postgres, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
        ContainerRequest: testcontainers.ContainerRequest{
            Image: "postgres:16-alpine",
            Env: map[string]string{
                "POSTGRES_DB":       "test",
                "POSTGRES_USER":     "test",
                "POSTGRES_PASSWORD": "test",
            },
            ExposedPorts: []string{"5432/tcp"},
        },
        Started: true,
    })
    require.NoError(t, err)
    defer postgres.Terminate(ctx)

    // 连接数据库并执行测试
    db, _ := sql.Open("postgres", connectionString)
    repo := NewUserRepository(db)

    user, err := repo.Create(ctx, &User{Name: "Bob"})
    assert.NoError(t, err)
    assert.NotEmpty(t, user.ID)
}

E2E 测试

func TestCreateUserFlow(t *testing.T) {
    // 启动 HTTP 服务
    server := httptest.NewServer(router())
    defer server.Close()

    // 发送请求
    resp, err := http.Post(
        server.URL+"/api/users",
        "application/json",
        strings.NewReader(`{"name":"Charlie","email":"charlie@test.com"}`),
    )
    require.NoError(t, err)
    defer resp.Body.Close()

    assert.Equal(t, http.StatusCreated, resp.StatusCode)

    // 验证结果
    var user User
    json.NewDecoder(resp.Body).Decode(&user)
    assert.NotEmpty(t, user.ID)
    assert.Equal(t, "Charlie", user.Name)
}

最佳实践

测试命名

func Test[FunctionName]_[Scenario]_[ExpectedResult](t *testing.T) {
    // ...
}

// 示例
func TestDivide_ByZero_ReturnsError(t *testing.T) {}
func TestCreateUser_InvalidEmail_ReturnsBadRequest(t *testing.T) {}

覆盖率目标

  • 关键业务逻辑:90%+
  • Controller/Handler:70%+
  • 集成测试:覆盖主要场景
  • 不盲目追求 100%,关注核心逻辑

持续集成中的测试

# GitHub Actions
test:
  runs-on: ubuntu-latest
  services:
    postgres:
      image: postgres:16-alpine
      env:
        POSTGRES_DB: test
  steps:
    - uses: actions/checkout@v4
    - run: go test ./... -race -coverprofile=coverage.out
    - run: go tool cover -func=coverage.out