Go 单元测试实战:table-driven 用例、testify 断言与接口 Mock

test12026-09-241 次阅读

为什么用 table-driven 测试

Go 社区习惯把一组相似用例写进一张"表",循环跑同一个被测函数,新增用例只加一行结构体,不必复制粘贴测试函数。相比 JUnit 式的一个用例一个函数,table-driven 更紧凑,也更容易覆盖边界。

被测函数与接口

先定义一个依赖外部存储的折扣服务,故意把存储抽象成接口,这样测试时可以用内存实现替换,不必连真实数据库。

package discount

import "errors"

type Store interface {
    GetRule(id string) (int, error)
}

type Service struct{ store Store }

func (s *Service) Calc(id string, price int) (int, error) {
    off, err := s.store.GetRule(id)
    if err != nil {
        return 0, err
    }
    if off < 0 || off > 100 {
        return 0, errors.New("折扣越界")
    }
    return price * (100 - off) / 100, nil
}

用 testify 写断言与 Mock

testify/assert 提供可读的断言;mock 包可以声明"期望被调用几次、返回什么",既验证结果也验证调用行为。下面是一个内存 mock 和一张用例表。

package discount

import (
    "errors"
    "testing"

    "github.com/stretchr/testify/assert"
    "github.com/stretchr/testify/mock"
)

type fakeStore struct{ mock.Mock }

func (m *fakeStore) GetRule(id string) (int, error) {
    args := m.Called(id)
    return args.Int(0), args.Error(1)
}

func TestCalc(t *testing.T) {
    cases := []struct {
        name    string
        rule    int
        ruleErr error
        price   int
        want    int
        wantErr bool
    }{
        {"正常九折", 10, nil, 100, 90, false},
        {"满减零折", 0, nil, 50, 50, false},
        {"存储报错", 0, errors.New("db down"), 50, 0, true},
        {"折扣越界", 200, nil, 50, 0, true},
    }
    for _, c := range cases {
        t.Run(c.name, func(t *testing.T) {
            m := new(fakeStore)
            m.On("GetRule", "r1").Return(c.rule, c.ruleErr)
            svc := &Service{store: m}
            got, err := svc.Calc("r1", c.price)
            if c.wantErr {
                assert.Error(t, err)
                return
            }
            assert.NoError(t, err)
            assert.Equal(t, c.want, got)
            m.AssertExpectations(t)
        })
    }
}

覆盖子测试与基准

t.Run 把每个用例变成独立子测试,失败时只显示对应 name;配合 -run 'TestCalc/正常九折' 可单独跑某条。需要关注性能时再加 Benchmark,用 b.ResetTimer() 排除预热。

func BenchmarkCalc(b *testing.B) {
    m := new(fakeStore)
    m.On("GetRule", "r1").Return(10, nil)
    svc := &Service{store: m}
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        _, _ = svc.Calc("r1", 100)
    }
}

实践要点

  • 把外部依赖(DB、HTTP、时间)抽象成接口,测试用 mock 或内存实现替换,让单测稳定可重复。
  • 边界用例(零值、越界、错误返回)和正常用例同等重要,table-driven 让它们成本一致。
  • go test -cover 看覆盖率,-race 在并发代码下必开,能抓出数据竞争。
T

test1

文章作者

为什么用 table-driven 测试 Go 社区习惯把一组相似用例写进一张"表",循环跑同一个被测函数,新增用例只...

分类
技术
发布时间
2026-09-24
字数
约 2330 字
阅读
1 次