从 JUnit 到 Go testing:单测、表驱动、Mock 怎么写?

发布时间:2026/7/27 23:34:07
从 JUnit 到 Go testing:单测、表驱动、Mock 怎么写? 本文为《Java工程师转Go实战》连载第 16 篇 / 共 20 篇上一篇gRPC 入门下一篇GMP 与 GC 面试必考类比开场Java 测试标配JUnit 5 Mockito AssertJ JaCoCo。Go 测试哲学标准库就够用——testing包 表驱动测试 接口 mock。没有注解魔法没有 DI 容器初始化测试启动飞快。一、对照表JavaGo说明JUnit 5testing标准库内置无需第三方Testfunc TestXxx(t *testing.T)函数名即测试声明BeforeEach/AfterEacht.Cleanup()或测试内 setup/teardownBeforeAll/AfterAllTestMain(m *testing.M)整个包级别AssertJ / Hamcresttestify/asserttestify/require也可用标准库Mockitogomock/testify/mock/ 手写 mock接口 mockParameterizedTest表驱动测试Go 特色强烈推荐SpringBootTest无等价不需要启动 IoC 容器手动组装依赖JaCoCo 覆盖率go test -cover内置JMH 微基准func BenchmarkXxx(b *testing.B)内置二、基础单测文件组织规则internal/service/ ├── user_service.go # 源码 └── user_service_test.go # 测试同包或 _test 外包测试文件必须以_test.go结尾测试函数必须以Test开头大写 T参数是*testing.T标准写法packageserviceimporttestingfuncTestValidateName(t*testing.T){// 测试空名字应返回错误iferr:ValidateName();errnil{t.Fatal(expected error for empty name, got nil)}// 测试正常名字应通过iferr:ValidateName(张三);err!nil{t.Fatalf(unexpected error for valid name: %v,err)}// 测试超长名字longName:string(make([]byte,100))iferr:ValidateName(longName);errnil{t.Fatal(expected error for too long name)}}运行测试# 运行当前包测试gotest# 运行所有包的测试递归gotest./...# 详细输出gotest-v./internal/service# 只跑某个测试gotest-v./internal/service-runTestValidateName# 跑匹配模式的子测试gotest-v-runTestCreateUser/empty_name# 覆盖率gotest-cover./... gotest-coverprofilecoverage.out ./... go tool cover-htmlcoverage.out# 浏览器查看三、表驱动测试Go 核心测试模式这是 Go 最有特色的测试写法——比 JUnit 的ParameterizedTest更灵活直观。基础模板funcTestAdd(t*testing.T){tests:[]struct{namestringa,bintwantint}{{positive numbers,1,2,3},{zeros,0,0,0},{negative,-1,1,0},{large numbers,100000,200000,300000},}for_,tt:rangetests{t.Run(tt.name,func(t*testing.T){got:Add(tt.a,tt.b)ifgot!tt.want{t.Errorf(Add(%d, %d) %d, want %d,tt.a,tt.b,got,tt.want)}})}}带错误期望的表驱动funcTestCreateUser(t*testing.T){svc:NewUserService(newMockRepo())tests:[]struct{namestringinput CreateUserReq wantErrerror}{{name:valid user,input:CreateUserReq{Name:张三,Email:ab.com},wantErr:nil,},{name:empty name,input:CreateUserReq{Name:,Email:ab.com},wantErr:ErrInvalidName,},{name:invalid email,input:CreateUserReq{Name:张三,Email:not-email},wantErr:ErrInvalidEmail,},{name:name too long,input:CreateUserReq{Name:strings.Repeat(a,100),Email:ab.com},wantErr:ErrInvalidName,},}for_,tt:rangetests{t.Run(tt.name,func(t*testing.T){_,err:svc.Create(context.Background(),tt.input)if!errors.Is(err,tt.wantErr){t.Errorf(got error %v, want %v,err,tt.wantErr)}})}}并行表驱动加速 CIfuncTestUserService_Create(t*testing.T){// ...for_,tt:rangetests{tt:tt// Go 1.22 之前必须t.Run(tt.name,func(t*testing.T){t.Parallel()// 每个子测试并行执行// ...})}}四、testify断言库assert vs requireimport(testinggithub.com/stretchr/testify/assertgithub.com/stretchr/testify/require)funcTestUserProfile(t*testing.T){user,err:svc.GetProfile(ctx,1001)// require失败立即停止后续断言不执行require.NoError(t,err)require.NotNil(t,user)// assert失败继续执行收集所有失败assert.Equal(t,张三,user.Name)assert.Equal(t,28,user.Age)assert.NotEmpty(t,user.Email)assert.True(t,user.CreatedAt.Before(time.Now()))}常用断言方法assert.Equal(t,expected,actual)// 相等assert.NotEqual(t,unexpected,actual)// 不等assert.Nil(t,obj)// nilassert.NotNil(t,obj)// 非 nilassert.True(t,condition)// 真assert.False(t,condition)// 假assert.Contains(t,hello world,world)// 包含assert.Len(t,slice,3)// 长度assert.Empty(t,slice)// 空assert.Error(t,err)// 有错误assert.NoError(t,err)// 无错误assert.ErrorIs(t,err,ErrNotFound)// 错误链匹配assert.ErrorAs(t,err,target)// 错误类型匹配assert.Panics(t,func(){panic(boom)})// 会 panicassert.Eventually(t,func()bool{// 异步断言returnisReady()},5*time.Second,100*time.Millisecond)五、Mock 接口对标 Mockito方式 1手写 mock小项目首选// 定义接口typeUserRepointerface{Create(ctx context.Context,u*User)errorGetByID(ctx context.Context,idint64)(*User,error)}// 手写 mocktypemockUserRepostruct{createFnfunc(context.Context,*User)errorgetByIDFnfunc(context.Context,int64)(*User,error)}func(m*mockUserRepo)Create(ctx context.Context,u*User)error{ifm.createFn!nil{returnm.createFn(ctx,u)}returnnil}func(m*mockUserRepo)GetByID(ctx context.Context,idint64)(*User,error){ifm.getByIDFn!nil{returnm.getByIDFn(ctx,id)}returnnil,ErrNotFound}// 使用funcTestCreateUser_Success(t*testing.T){repo:mockUserRepo{createFn:func(ctx context.Context,u*User)error{u.ID1// 模拟 DB 自增 IDreturnnil},}svc:NewUserService(repo)user,err:svc.Create(context.Background(),CreateUserReq{Name:张三})require.NoError(t,err)assert.Equal(t,int64(1),user.ID)assert.Equal(t,张三,user.Name)}funcTestCreateUser_DBError(t*testing.T){repo:mockUserRepo{createFn:func(ctx context.Context,u*User)error{returnerrors.New(connection refused)},}svc:NewUserService(repo)_,err:svc.Create(context.Background(),CreateUserReq{Name:张三})assert.Error(t,err)assert.Contains(t,err.Error(),connection refused)}方式 2gomock大项目、自动生成# 安装goinstallgo.uber.org/mock/mockgenlatest# 从接口生成 mockmockgen-sourceinternal/service/user_service.go-destinationinternal/service/mock_repo_test.go-packageservicefuncTestCreateUser_WithGoMock(t*testing.T){ctrl:gomock.NewController(t)deferctrl.Finish()repo:NewMockUserRepo(ctrl)// 设置期望repo.EXPECT().Create(gomock.Any(),gomock.Any()).DoAndReturn(func(ctx context.Context,u*User)error{u.ID1returnnil})svc:NewUserService(repo)user,err:svc.Create(context.Background(),CreateUserReq{Name:张三})require.NoError(t,err)assert.Equal(t,int64(1),user.ID)}Mock 选型建议方式适用优点缺点手写 mock struct小项目、方法少简单直观方法多时代码量大gomock大项目自动生成、验证调用次数需要代码生成步骤testify/mock习惯 Mockito 的API 类似 Mockito运行时反射六、HTTP Handler 测试使用 httptest不起真实端口import(net/httpnet/http/httpteststringstestinggithub.com/gin-gonic/gin)funcsetupRouter()*gin.Engine{gin.SetMode(gin.TestMode)r:gin.New()svc:NewUserService(newMockRepo())h:NewUserHandler(svc)r.POST(/api/users,h.Create)r.GET(/api/users/:id,h.Get)returnr}funcTestCreateUser_HTTP(t*testing.T){router:setupRouter()body:{name:张三,email:zhangsantest.com}req:httptest.NewRequest(POST,/api/users,strings.NewReader(body))req.Header.Set(Content-Type,application/json)w:httptest.NewRecorder()router.ServeHTTP(w,req)assert.Equal(t,201,w.Code)varrespmap[string]interface{}json.Unmarshal(w.Body.Bytes(),resp)assert.Equal(t,float64(0),resp[code])}funcTestGetUser_NotFound(t*testing.T){router:setupRouter()req:httptest.NewRequest(GET,/api/users/999,nil)w:httptest.NewRecorder()router.ServeHTTP(w,req)assert.Equal(t,404,w.Code)}七、TestMain全局 Setup/TeardownfuncTestMain(m*testing.M){// SetupBeforeAlldb:setupTestDB()setupTestData(db)// 运行所有测试 code:m.Run()// TeardownAfterAllcleanupTestDB(db)os.Exit(code)}t.Cleanup()单测级别清理funcTestWithTempFile(t*testing.T){f,err:os.CreateTemp(,test-*)require.NoError(t,err)t.Cleanup(func(){os.Remove(f.Name())// 测试结束后自动清理})// 使用临时文件...}八、Benchmark 微基准测试funcBenchmarkJSONMarshal(b*testing.B){user:User{ID:1,Name:张三,Email:ab.com}b.ResetTimer()// 排除 setup 时间fori:0;ib.N;i{json.Marshal(user)}}funcBenchmarkJSONMarshal_Parallel(b*testing.B){user:User{ID:1,Name:张三,Email:ab.com}b.RunParallel(func(pb*testing.PB){forpb.Next(){json.Marshal(user)}})}gotest-bench.-benchmem./...# 输出# BenchmarkJSONMarshal-8 5000000 240 ns/op 128 B/op 2 allocs/op# BenchmarkJSONMarshal_Parallel-8 20000000 60 ns/op 128 B/op 2 allocs/op对标 JMHns/op每次操作耗时B/op每次操作分配的字节allocs/op每次操作的内存分配次数九、集成测试TestContainersimport(github.com/testcontainers/testcontainers-gogithub.com/testcontainers/testcontainers-go/modules/mysql)funcTestUserRepo_Integration(t*testing.T){iftesting.Short(){t.Skip(skipping integration test)}ctx:context.Background()// 启动 MySQL 容器container,err:mysql.Run(ctx,mysql:8.0,mysql.WithDatabase(test),mysql.WithUsername(root),mysql.WithPassword(test),)require.NoError(t,err)t.Cleanup(func(){container.Terminate(ctx)})// 获取连接字符串dsn,_:container.ConnectionString(ctx)db:connectDB(dsn)// 运行迁移migrate(db)// 测试 Repositoryrepo:NewUserRepository(db)t.Run(create and get,func(t*testing.T){user:User{Name:测试用户,Email:testtest.com}err:repo.Create(ctx,user)require.NoError(t,err)assert.Greater(t,user.ID,int64(0))found,err:repo.GetByID(ctx,user.ID)require.NoError(t,err)assert.Equal(t,测试用户,found.Name)})}# 跑集成测试不跳过gotest-v./internal/repository/...# 只跑单测跳过集成测试gotest-short./...十、测试最佳实践建议原因测试文件_test.go同包白盒测试可访问私有函数或用xxx_test外包名黑盒测试只测公开 API表驱动覆盖所有边界一目了然、易扩展t.Parallel()加速 CI包内并行执行HTTP 测试用httptest不需要真实端口Mock 接口不 mock 实现依赖注入 接口 可测试CI 中开启-race检测 data race覆盖率看趋势不追 100%核心路径必测getter/setter 不必Makefile 集成.PHONY: test test-cover lint test: go test -race -short ./... test-cover: go test -race -coverprofilecoverage.out ./... go tool cover -funccoverage.out test-integration: go test -race -v ./... lint: golangci-lint run ./... 面试追问testing 包 benchmark 原理框架自动调整b.N先小 N 测耗时逐步加大直到运行时间稳定默认跑 1 秒最终报告每次操作的平均时间。-benchtime5s可调整。怎么测私有函数同包_test.go直接测白盒或只测公开 API黑盒用package xxx_test。Go 社区更推荐后者。go test 并行规则包间默认并行-p控制数量包内默认串行除非调用t.Parallel()。-parallel参数控制包内最大并行数。怎么做 mockGo 不依赖框架注入而是接口 构造函数注入。测试时传 mock 实现。如果你的代码难以 mock说明缺少接口抽象。Go 有 Spring 那样的 MockBean 吗没有。Go 测试不需要启动 IoC 容器——直接NewService(mockRepo)构造即可。这是 Go 测试快的核心原因。 一句话总结JUnit 的 Test 换成 TestXxxParameterizedTest 换成表驱动Mockito 换成接口 mock gomock——Go 测试更朴素、更快没有容器启动但同样能覆盖从单元到集成的所有场景。表驱动是 Go 考官最爱问的测试模式务必掌握。建议标签GolangJavaJUnit单元测试testingMock表驱动