testify-tdd

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Go Testing with Testify and TDD

使用Testify与TDD进行Go测试

Comprehensive guide for writing Go tests using stretchr/testify and following Test-Driven Development methodology.
本指南全面介绍如何使用stretchr/testify编写Go测试,并遵循测试驱动开发(TDD)方法论。

Installation

安装

bash
go get github.com/stretchr/testify
bash
go get github.com/stretchr/testify

Testify Packages Overview

Testify包概览

PackagePurposeWhen to Use
testify/assert
Assertions that continue on failureMultiple independent checks in one test
testify/require
Assertions that halt on failurePrerequisites that must pass to continue
testify/mock
Interface mockingIsolating dependencies in unit tests
testify/suite
Test organizationRelated tests sharing setup/teardown
用途使用场景
testify/assert
断言失败后仍继续执行单个测试中包含多个独立检查
testify/require
断言失败后立即终止测试必须通过的前置条件
testify/mock
接口Mock单元测试中隔离依赖
testify/suite
测试用例组织共享初始化/清理逻辑的相关测试

Assert vs Require

Assert与Require的区别

Critical distinction: Choose based on whether the test should continue after failure.
关键区别:根据测试失败后是否需要继续执行来选择。

Use
require
for Prerequisites

前置条件使用
require

go
func TestUserService_GetByID(t *testing.T) {
    // Prerequisites - if these fail, nothing else matters
    db, err := setupTestDB()
    require.NoError(t, err, "database setup must succeed")
    require.NotNil(t, db, "database connection required")

    user, err := service.GetByID(ctx, userID)
    require.NoError(t, err)  // Can't verify user if this fails

    // Verifications - these can use assert
    assert.Equal(t, expectedName, user.Name)
    assert.Equal(t, expectedEmail, user.Email)
    assert.True(t, user.IsActive)
}
go
func TestUserService_GetByID(t *testing.T) {
    // 前置条件 - 如果这些失败,后续测试无意义
    db, err := setupTestDB()
    require.NoError(t, err, "数据库初始化必须成功")
    require.NotNil(t, db, "需要数据库连接")

    user, err := service.GetByID(ctx, userID)
    require.NoError(t, err)  // 此步骤失败则无法验证用户信息

    // 验证环节 - 可使用assert
    assert.Equal(t, expectedName, user.Name)
    assert.Equal(t, expectedEmail, user.Email)
    assert.True(t, user.IsActive)
}

Use
assert
for Multiple Independent Checks

多个独立检查使用
assert

go
func TestOrderValidation(t *testing.T) {
    order := createTestOrder()
    errors := order.Validate()

    // All checks run even if earlier ones fail
    assert.NotEmpty(t, order.ID, "order should have ID")
    assert.Greater(t, order.Total, 0.0, "total should be positive")
    assert.NotEmpty(t, order.Items, "order should have items")
    assert.Empty(t, errors, "validation should pass")
}
go
func TestOrderValidation(t *testing.T) {
    order := createTestOrder()
    errors := order.Validate()

    // 即使前面的检查失败,所有检查仍会执行
    assert.NotEmpty(t, order.ID, "订单必须包含ID")
    assert.Greater(t, order.Total, 0.0, "订单总额必须为正数")
    assert.NotEmpty(t, order.Items, "订单必须包含商品")
    assert.Empty(t, errors, "验证必须通过")
}

Common Assertion Methods

常用断言方法

go
// Equality
assert.Equal(t, expected, actual)
assert.NotEqual(t, expected, actual)
assert.EqualValues(t, expected, actual)  // Type-coerced comparison

// Nil checks
assert.Nil(t, value)
assert.NotNil(t, value)

// Boolean
assert.True(t, condition)
assert.False(t, condition)

// Collections
assert.Empty(t, collection)
assert.NotEmpty(t, collection)
assert.Len(t, collection, expectedLen)
assert.Contains(t, collection, element)
assert.ElementsMatch(t, expected, actual)  // Order-independent

// Errors
assert.NoError(t, err)
assert.Error(t, err)
assert.ErrorIs(t, err, expectedErr)
assert.ErrorContains(t, err, "substring")

// Comparisons
assert.Greater(t, a, b)
assert.GreaterOrEqual(t, a, b)
assert.Less(t, a, b)
assert.LessOrEqual(t, a, b)

// Strings
assert.Contains(t, str, substring)
assert.Regexp(t, pattern, str)

// JSON
assert.JSONEq(t, expectedJSON, actualJSON)

// Time
assert.WithinDuration(t, expected, actual, delta)
go
// 相等性检查
assert.Equal(t, expected, actual)
assert.NotEqual(t, expected, actual)
assert.EqualValues(t, expected, actual)  // 类型兼容的比较

// 空值检查
assert.Nil(t, value)
assert.NotNil(t, value)

// 布尔值检查
assert.True(t, condition)
assert.False(t, condition)

// 集合检查
assert.Empty(t, collection)
assert.NotEmpty(t, collection)
assert.Len(t, collection, expectedLen)
assert.Contains(t, collection, element)
assert.ElementsMatch(t, expected, actual)  // 不考虑顺序

// 错误检查
assert.NoError(t, err)
assert.Error(t, err)
assert.ErrorIs(t, err, expectedErr)
assert.ErrorContains(t, err, "子字符串")

// 数值比较
assert.Greater(t, a, b)
assert.GreaterOrEqual(t, a, b)
assert.Less(t, a, b)
assert.LessOrEqual(t, a, b)

// 字符串检查
assert.Contains(t, str, substring)
assert.Regexp(t, pattern, str)

// JSON检查
assert.JSONEq(t, expectedJSON, actualJSON)

// 时间检查
assert.WithinDuration(t, expected, actual, delta)

Table-Driven Tests

表驱动测试

The idiomatic Go pattern for testing multiple scenarios.
go
func TestCalculateDiscount(t *testing.T) {
    tests := []struct {
        name           string
        orderTotal     float64
        customerTier   string
        expectedDiscount float64
        expectError    bool
    }{
        {
            name:           "no discount for small orders",
            orderTotal:     50.00,
            customerTier:   "standard",
            expectedDiscount: 0,
        },
        {
            name:           "10% discount for gold tier",
            orderTotal:     100.00,
            customerTier:   "gold",
            expectedDiscount: 10.00,
        },
        {
            name:           "20% discount for platinum over $500",
            orderTotal:     600.00,
            customerTier:   "platinum",
            expectedDiscount: 120.00,
        },
        {
            name:        "error for negative total",
            orderTotal:  -10.00,
            expectError: true,
        },
    }

    for _, tc := range tests {
        t.Run(tc.name, func(t *testing.T) {
            discount, err := CalculateDiscount(tc.orderTotal, tc.customerTier)

            if tc.expectError {
                require.Error(t, err)
                return
            }

            require.NoError(t, err)
            assert.Equal(t, tc.expectedDiscount, discount)
        })
    }
}
Go语言中用于测试多场景的惯用模式。
go
func TestCalculateDiscount(t *testing.T) {
    tests := []struct {
        name           string
        orderTotal     float64
        customerTier   string
        expectedDiscount float64
        expectError    bool
    }{
        {
            name:           "小额订单无折扣",
            orderTotal:     50.00,
            customerTier:   "standard",
            expectedDiscount: 0,
        },
        {
            name:           "黄金等级享10%折扣",
            orderTotal:     100.00,
            customerTier:   "gold",
            expectedDiscount: 10.00,
        },
        {
            name:           "白金等级订单超500享20%折扣",
            orderTotal:     600.00,
            customerTier:   "platinum",
            expectedDiscount: 120.00,
        },
        {
            name:        "订单总额为负时返回错误",
            orderTotal:  -10.00,
            expectError: true,
        },
    }

    for _, tc := range tests {
        t.Run(tc.name, func(t *testing.T) {
            discount, err := CalculateDiscount(tc.orderTotal, tc.customerTier)

            if tc.expectError {
                require.Error(t, err)
                return
            }

            require.NoError(t, err)
            assert.Equal(t, tc.expectedDiscount, discount)
        })
    }
}

Interface Mocking

接口Mock

Define the Mock

定义Mock

go
import "github.com/stretchr/testify/mock"

// UserRepository is the interface to mock
type UserRepository interface {
    GetByID(ctx context.Context, id string) (*User, error)
    Save(ctx context.Context, user *User) error
    Delete(ctx context.Context, id string) error
}

// MockUserRepository implements UserRepository for testing
type MockUserRepository struct {
    mock.Mock
}

func (m *MockUserRepository) GetByID(ctx context.Context, id string) (*User, error) {
    args := m.Called(ctx, id)
    if args.Get(0) == nil {
        return nil, args.Error(1)
    }
    return args.Get(0).(*User), args.Error(1)
}

func (m *MockUserRepository) Save(ctx context.Context, user *User) error {
    args := m.Called(ctx, user)
    return args.Error(0)
}

func (m *MockUserRepository) Delete(ctx context.Context, id string) error {
    args := m.Called(ctx, id)
    return args.Error(0)
}
go
import "github.com/stretchr/testify/mock"

// UserRepository是需要Mock的接口
type UserRepository interface {
    GetByID(ctx context.Context, id string) (*User, error)
    Save(ctx context.Context, user *User) error
    Delete(ctx context.Context, id string) error
}

// MockUserRepository实现了用于测试的UserRepository接口
type MockUserRepository struct {
    mock.Mock
}

func (m *MockUserRepository) GetByID(ctx context.Context, id string) (*User, error) {
    args := m.Called(ctx, id)
    if args.Get(0) == nil {
        return nil, args.Error(1)
    }
    return args.Get(0).(*User), args.Error(1)
}

func (m *MockUserRepository) Save(ctx context.Context, user *User) error {
    args := m.Called(ctx, user)
    return args.Error(0)
}

func (m *MockUserRepository) Delete(ctx context.Context, id string) error {
    args := m.Called(ctx, id)
    return args.Error(0)
}

Use the Mock in Tests

在测试中使用Mock

go
func TestUserService_UpdateEmail(t *testing.T) {
    ctx := context.Background()
    userID := "user-123"
    newEmail := "new@example.com"

    existingUser := &User{
        ID:    userID,
        Email: "old@example.com",
        Name:  "Test User",
    }

    // Create mock
    mockRepo := new(MockUserRepository)

    // Set expectations
    mockRepo.On("GetByID", ctx, userID).Return(existingUser, nil)
    mockRepo.On("Save", ctx, mock.MatchedBy(func(u *User) bool {
        return u.ID == userID && u.Email == newEmail
    })).Return(nil)

    // Create service with mock
    service := NewUserService(mockRepo)

    // Execute
    err := service.UpdateEmail(ctx, userID, newEmail)

    // Verify
    require.NoError(t, err)
    mockRepo.AssertExpectations(t)
}
go
func TestUserService_UpdateEmail(t *testing.T) {
    ctx := context.Background()
    userID := "user-123"
    newEmail := "new@example.com"

    existingUser := &User{
        ID:    userID,
        Email: "old@example.com",
        Name:  "Test User",
    }

    // 创建Mock实例
    mockRepo := new(MockUserRepository)

    // 设置预期调用
    mockRepo.On("GetByID", ctx, userID).Return(existingUser, nil)
    mockRepo.On("Save", ctx, mock.MatchedBy(func(u *User) bool {
        return u.ID == userID && u.Email == newEmail
    })).Return(nil)

    // 使用Mock创建服务实例
    service := NewUserService(mockRepo)

    // 执行测试逻辑
    err := service.UpdateEmail(ctx, userID, newEmail)

    // 验证结果
    require.NoError(t, err)
    mockRepo.AssertExpectations(t)
}

Mock Argument Matchers

Mock参数匹配器

go
// Exact match
mockRepo.On("GetByID", ctx, "user-123")

// Any value of type
mockRepo.On("GetByID", mock.Anything, mock.AnythingOfType("string"))

// Custom matcher
mockRepo.On("Save", ctx, mock.MatchedBy(func(u *User) bool {
    return u.Email != "" && u.ID != ""
}))

// Verify call count
mockRepo.AssertNumberOfCalls(t, "GetByID", 2)
mockRepo.AssertCalled(t, "Save", ctx, mock.Anything)
mockRepo.AssertNotCalled(t, "Delete", mock.Anything, mock.Anything)
go
// 精确匹配
mockRepo.On("GetByID", ctx, "user-123")

// 匹配指定类型的任意值
mockRepo.On("GetByID", mock.Anything, mock.AnythingOfType("string"))

// 自定义匹配器
mockRepo.On("Save", ctx, mock.MatchedBy(func(u *User) bool {
    return u.Email != "" && u.ID != ""
}))

// 验证调用次数
mockRepo.AssertNumberOfCalls(t, "GetByID", 2)
mockRepo.AssertCalled(t, "Save", ctx, mock.Anything)
mockRepo.AssertNotCalled(t, "Delete", mock.Anything, mock.Anything)

Mock Return Values

Mock返回值

go
// Return specific values
mockRepo.On("GetByID", ctx, "exists").Return(&User{ID: "exists"}, nil)
mockRepo.On("GetByID", ctx, "not-found").Return(nil, ErrNotFound)

// Return dynamically
mockRepo.On("Save", ctx, mock.Anything).Return(nil).Run(func(args mock.Arguments) {
    user := args.Get(1).(*User)
    user.ID = "generated-id"  // Modify the argument
})

// Return different values on successive calls
mockRepo.On("GetByID", ctx, "user-1").Return(&User{}, nil).Once()
mockRepo.On("GetByID", ctx, "user-1").Return(nil, ErrNotFound).Once()
go
// 返回指定值
mockRepo.On("GetByID", ctx, "exists").Return(&User{ID: "exists"}, nil)
mockRepo.On("GetByID", ctx, "not-found").Return(nil, ErrNotFound)

// 动态返回值
mockRepo.On("Save", ctx, mock.Anything).Return(nil).Run(func(args mock.Arguments) {
    user := args.Get(1).(*User)
    user.ID = "generated-id"  // 修改参数
})

// 多次调用返回不同值
mockRepo.On("GetByID", ctx, "user-1").Return(&User{}, nil).Once()
mockRepo.On("GetByID", ctx, "user-1").Return(nil, ErrNotFound).Once()

Test Suites

测试套件

Organize related tests with shared setup and teardown.
go
import (
    "testing"
    "github.com/stretchr/testify/suite"
)

type UserServiceTestSuite struct {
    suite.Suite
    service  *UserService
    mockRepo *MockUserRepository
    ctx      context.Context
}

// SetupSuite runs once before all tests
func (s *UserServiceTestSuite) SetupSuite() {
    s.ctx = context.Background()
}

// SetupTest runs before each test
func (s *UserServiceTestSuite) SetupTest() {
    s.mockRepo = new(MockUserRepository)
    s.service = NewUserService(s.mockRepo)
}

// TearDownTest runs after each test
func (s *UserServiceTestSuite) TearDownTest() {
    s.mockRepo.AssertExpectations(s.T())
}

// Test methods must start with "Test"
func (s *UserServiceTestSuite) TestGetByID_Success() {
    expected := &User{ID: "123", Name: "Test"}
    s.mockRepo.On("GetByID", s.ctx, "123").Return(expected, nil)

    user, err := s.service.GetByID(s.ctx, "123")

    s.Require().NoError(err)
    s.Equal(expected.Name, user.Name)
}

func (s *UserServiceTestSuite) TestGetByID_NotFound() {
    s.mockRepo.On("GetByID", s.ctx, "999").Return(nil, ErrNotFound)

    user, err := s.service.GetByID(s.ctx, "999")

    s.Nil(user)
    s.ErrorIs(err, ErrNotFound)
}

// Run the suite
func TestUserServiceSuite(t *testing.T) {
    suite.Run(t, new(UserServiceTestSuite))
}
使用共享的初始化和清理逻辑组织相关测试。
go
import (
    "testing"
    "github.com/stretchr/testify/suite"
)

type UserServiceTestSuite struct {
    suite.Suite
    service  *UserService
    mockRepo *MockUserRepository
    ctx      context.Context
}

// SetupSuite在所有测试前执行一次
func (s *UserServiceTestSuite) SetupSuite() {
    s.ctx = context.Background()
}

// SetupTest在每个测试前执行
func (s *UserServiceTestSuite) SetupTest() {
    s.mockRepo = new(MockUserRepository)
    s.service = NewUserService(s.mockRepo)
}

// TearDownTest在每个测试后执行
func (s *UserServiceTestSuite) TearDownTest() {
    s.mockRepo.AssertExpectations(s.T())
}

// 测试方法必须以"Test"开头
func (s *UserServiceTestSuite) TestGetByID_Success() {
    expected := &User{ID: "123", Name: "Test"}
    s.mockRepo.On("GetByID", s.ctx, "123").Return(expected, nil)

    user, err := s.service.GetByID(s.ctx, "123")

    s.Require().NoError(err)
    s.Equal(expected.Name, user.Name)
}

func (s *UserServiceTestSuite) TestGetByID_NotFound() {
    s.mockRepo.On("GetByID", s.ctx, "999").Return(nil, ErrNotFound)

    user, err := s.service.GetByID(s.ctx, "999")

    s.Nil(user)
    s.ErrorIs(err, ErrNotFound)
}

// 运行测试套件
func TestUserServiceSuite(t *testing.T) {
    suite.Run(t, new(UserServiceTestSuite))
}

TDD Workflow: Red-Green-Refactor

TDD工作流:红-绿-重构

1. Red: Write a Failing Test First

1. 红:先编写失败的测试

go
func TestShippingCalculator_CalculateCost(t *testing.T) {
    calc := NewShippingCalculator()

    cost, err := calc.CalculateCost(Weight(5.0), Zone("US-WEST"))

    require.NoError(t, err)
    assert.Equal(t, Money(12.50), cost)
}
Run the test - it should fail (function doesn't exist or returns wrong value).
go
func TestShippingCalculator_CalculateCost(t *testing.T) {
    calc := NewShippingCalculator()

    cost, err := calc.CalculateCost(Weight(5.0), Zone("US-WEST"))

    require.NoError(t, err)
    assert.Equal(t, Money(12.50), cost)
}
运行测试 - 应该失败(函数不存在或返回错误值)。

2. Green: Write Minimal Code to Pass

2. 绿:编写最少代码使测试通过

go
func (c *ShippingCalculator) CalculateCost(weight Weight, zone Zone) (Money, error) {
    // Minimal implementation to make the test pass
    return Money(12.50), nil
}
Run the test - it should pass.
go
func (c *ShippingCalculator) CalculateCost(weight Weight, zone Zone) (Money, error) {
    // 使测试通过的最简实现
    return Money(12.50), nil
}
运行测试 - 应该通过。

3. Refactor: Improve While Keeping Tests Green

3. 重构:在保持测试通过的同时优化代码

go
func (c *ShippingCalculator) CalculateCost(weight Weight, zone Zone) (Money, error) {
    baseRate := c.getBaseRate(zone)
    weightCharge := weight.Kilograms() * c.ratePerKg
    return Money(baseRate + weightCharge), nil
}
Run tests after each refactor to ensure they still pass.
go
func (c *ShippingCalculator) CalculateCost(weight Weight, zone Zone) (Money, error) {
    baseRate := c.getBaseRate(zone)
    weightCharge := weight.Kilograms() * c.ratePerKg
    return Money(baseRate + weightCharge), nil
}
每次重构后运行测试,确保仍然通过。

4. Add More Test Cases

4. 添加更多测试用例

go
func TestShippingCalculator_CalculateCost(t *testing.T) {
    tests := []struct {
        name         string
        weight       Weight
        zone         Zone
        expectedCost Money
        expectError  bool
    }{
        {"small package US-WEST", Weight(1.0), Zone("US-WEST"), Money(5.00), false},
        {"medium package US-WEST", Weight(5.0), Zone("US-WEST"), Money(12.50), false},
        {"large package US-EAST", Weight(10.0), Zone("US-EAST"), Money(22.00), false},
        {"zero weight error", Weight(0), Zone("US-WEST"), Money(0), true},
        {"negative weight error", Weight(-1), Zone("US-WEST"), Money(0), true},
    }

    calc := NewShippingCalculator()

    for _, tc := range tests {
        t.Run(tc.name, func(t *testing.T) {
            cost, err := calc.CalculateCost(tc.weight, tc.zone)

            if tc.expectError {
                require.Error(t, err)
                return
            }

            require.NoError(t, err)
            assert.Equal(t, tc.expectedCost, cost)
        })
    }
}
go
func TestShippingCalculator_CalculateCost(t *testing.T) {
    tests := []struct {
        name         string
        weight       Weight
        zone         Zone
        expectedCost Money
        expectError  bool
    }{
        {"美国西部小包裹", Weight(1.0), Zone("US-WEST"), Money(5.00), false},
        {"美国西部中等包裹", Weight(5.0), Zone("US-WEST"), Money(12.50), false},
        {"美国东部大包裹", Weight(10.0), Zone("US-EAST"), Money(22.00), false},
        {"重量为零返回错误", Weight(0), Zone("US-WEST"), Money(0), true},
        {"重量为负返回错误", Weight(-1), Zone("US-WEST"), Money(0), true},
    }

    calc := NewShippingCalculator()

    for _, tc := range tests {
        t.Run(tc.name, func(t *testing.T) {
            cost, err := calc.CalculateCost(tc.weight, tc.zone)

            if tc.expectError {
                require.Error(t, err)
                return
            }

            require.NoError(t, err)
            assert.Equal(t, tc.expectedCost, cost)
        })
    }
}

Test File Organization

测试文件组织

Same-Package Tests (Unit Tests)

同包测试(单元测试)

go
// user_service.go
package users

type UserService struct { ... }

// user_service_test.go
package users  // Same package - can test private functions

func TestUserService_validateEmail(t *testing.T) {
    // Can access private method validateEmail
}
go
// user_service.go
package users

type UserService struct { ... }

// user_service_test.go
package users  // 同包 - 可测试私有函数

func TestUserService_validateEmail(t *testing.T) {
    // 可访问私有方法validateEmail
}

Black-Box Tests (Integration Tests)

黑盒测试(集成测试)

go
// user_repository_integration_test.go
package users_test  // Different package - tests public API only

import "myapp/internal/users"

func TestUserRepository_Create(t *testing.T) {
    // Can only access public API
    repo := users.NewRepository(db)
}
go
// user_repository_integration_test.go
package users_test  // 不同包 - 仅测试公开API

import "myapp/internal/users"

func TestUserRepository_Create(t *testing.T) {
    // 仅能访问公开API
    repo := users.NewRepository(db)
}

Test Naming Conventions

测试命名规范

Test[Unit]_[Scenario]_[ExpectedBehavior]

Examples:
- TestUserService_GetByID_ReturnsUser
- TestUserService_GetByID_ReturnsErrorWhenNotFound
- TestCalculateDiscount_GoldTier_Returns10Percent
- TestOrderValidator_EmptyItems_ReturnsValidationError
Test[测试单元]_[场景]_[预期行为]

示例:
- TestUserService_GetByID_ReturnsUser
- TestUserService_GetByID_ReturnsErrorWhenNotFound
- TestCalculateDiscount_GoldTier_Returns10Percent
- TestOrderValidator_EmptyItems_ReturnsValidationError

Running Tests

运行测试

bash
undefined
bash
undefined

Run all tests

运行所有测试

go test ./...
go test ./...

Verbose output

详细输出

go test -v ./...
go test -v ./...

Disable test caching

禁用测试缓存

go test -count=1 ./...
go test -count=1 ./...

Run specific test

运行指定测试

go test -v -run TestUserService ./...
go test -v -run TestUserService ./...

Run with coverage

运行测试并查看覆盖率

go test -cover ./...
go test -cover ./...

Generate coverage report

生成覆盖率报告

go test -coverprofile=coverage.out ./... go tool cover -html=coverage.out
undefined
go test -coverprofile=coverage.out ./... go tool cover -html=coverage.out
undefined

Best Practices

最佳实践

  1. One logical assertion per test - Test one behavior, though you may have multiple assert calls
  2. Descriptive test names - The name should explain what's being tested
  3. Arrange-Act-Assert pattern - Structure tests clearly
  4. Use
    require
    for setup,
    assert
    for verification
  5. Table-driven tests for multiple scenarios - Avoid copy-paste test code
  6. Mock at interface boundaries - Don't mock what you don't own
  7. Keep tests independent - No shared state between tests
  8. Test edge cases - Empty inputs, nil, zero values, boundaries
  9. Don't test implementation details - Test behavior, not internal structure
  10. Verify mock expectations - Always call
    AssertExpectations(t)
  1. 每个测试验证一个逻辑行为 - 测试一个行为,可包含多个assert调用
  2. 测试名称具有描述性 - 名称应说明测试内容
  3. 遵循Arrange-Act-Assert模式 - 清晰组织测试结构
  4. 初始化用require,验证用assert
  5. 多场景使用表驱动测试 - 避免复制粘贴测试代码
  6. 在接口边界处Mock - 不要Mock不属于你的代码
  7. 保持测试独立性 - 测试间无共享状态
  8. 测试边缘情况 - 空输入、nil、零值、边界值
  9. 不测试实现细节 - 测试行为而非内部结构
  10. 验证Mock预期 - 务必调用
    AssertExpectations(t)