找回密码
 立即注册
首页 业界区 科技 《Go 单元测试从入门到覆盖率提升》(三) ...

《Go 单元测试从入门到覆盖率提升》(三)

东新 4 天前
 Go单元测试打桩框架

  Golang有常用的三个打桩框架:GoStub、GoMock、Monkey。1、GoStub

   GoStub 是一款轻量级的单元测试框架,接口友好,使用方式简洁,能够覆盖多种常见测试场景:

  • 全局变量打桩:替换全局变量的值,方便测试不同状态下的逻辑。
  • 函数打桩:为函数设置自定义的返回结果,模拟不同输入输出。
  • 过程打桩:当函数没有返回值时(更像是过程),也可以通过打桩控制其行为。
  • 复合场景:可以将上述多种方式自由组合,满足更复杂的测试需求。
  凭借这些特性,GoStub 非常适合在需要灵活 Mock 的单元测试中使用,尤其是在快速验证逻辑、隔离外部依赖时效果明显。
GoStub安装:go get github.com/prashantv/gostub
  ① 为一个全局变量打桩(短暂修改这个变量的值)
  1. var counter = 200
  2. func TestStubExample(t *testing.T) {
  3.     Convey("Simple Stub example", t, func() {
  4.         // 验证初始值
  5.         So(counter, ShouldEqual, 200)
  6.         // 执行stub操作
  7.         stubs := gostub.Stub(&counter, 100)
  8.         defer stubs.Reset() // 确保最后能恢复
  9.         // 验证stub后的值
  10.         So(counter, should.Equal, 100) // 应该是100,不是200
  11.         // 手动重置
  12.         stubs.Reset()
  13.         // 验证恢复后的值
  14.         So(counter, ShouldEqual, 200)
  15.     })
  16. }
复制代码
1.png

  ② 为一个函数打桩(让函数返回固定的值)
  1. // GoStub/function_stub_test.go
  2. package gostub
  3. import (
  4.     "testing"
  5.     "github.com/prashantv/gostub"
  6.     . "github.com/smartystreets/goconvey/convey"
  7. )
  8. // 给一个函数打桩
  9. func GetCurrentTime() int {
  10.     return 1000 // 模拟返回当前的时间戳
  11. }
  12. // 使用该函数的业务逻辑
  13. func CalculateAge() int {
  14.     birthTime := 500
  15.     currentTime := GetCurrentTime()
  16.     return currentTime - birthTime
  17. }
  18. // 用于打桩的函数变量
  19. var getCurrentTimeFunc = GetCurrentTime
  20. // 业务逻辑改写为使用函数变量
  21. func CalculateAgeWithStub() int {
  22.     birthTime := 500
  23.     currentTime := getCurrentTimeFunc()
  24.     return currentTime - birthTime
  25. }
  26. func TestFunctionStub(t *testing.T) {
  27.     Convey("Function stub example", t, func() {
  28.         // 正常情况下
  29.         So(CalculateAgeWithStub(), ShouldEqual, 500)
  30.         // 为函数打桩
  31.         stubs := gostub.Stub(&getCurrentTimeFunc, func() int {
  32.             return 2000 // 模拟不同的当前时间
  33.         })
  34.         defer stubs.Reset()
  35.         // 验证打桩后的结果
  36.         So(CalculateAgeWithStub(), ShouldEqual, 1500)
  37.         // 恢复后再次验证
  38.         stubs.Reset()
  39.         So(CalculateAgeWithStub(), ShouldEqual, 500)
  40.     })
  41. }
复制代码
2.png

  ③ 为一个过程打桩

  在 GoStub 中,除了对变量和有返回值的函数进行打桩外,还支持对 过程(Procedure) 进行打桩。所谓“过程”,就是 没有返回值的函数。在实际开发中,我们经常会把一些 资源清理、日志记录、状态更新 之类的逻辑写成过程函数。
  对过程打桩的意义在于:我们可以临时替换这些函数的行为,例如屏蔽真实的清理操作、只打印模拟日志,从而让测试更可控,不会影响外部环境。
  1. // GoStub/simple_process_stub_test.go
  2. package gostub
  3. import (
  4.     "testing"
  5.     "github.com/prashantv/gostub"
  6.     . "github.com/smartystreets/goconvey/convey"
  7. )
  8. // 要打桩的过程函数(无返回值)
  9. func PrintLog(msg string) {
  10.     println("Real log:", msg)
  11. }
  12. // 业务函数
  13. func DoWork() {
  14.     PrintLog("Starting work")
  15.     // 做一些工作
  16.     PrintLog("Work completed")
  17. }
  18. // 可打桩的函数变量
  19. var printLogFunc = PrintLog
  20. // 使用可打桩函数的业务版本
  21. func DoWorkWithStub() {
  22.     printLogFunc("Starting work")
  23.     // 做一些工作
  24.     printLogFunc("Work completed")
  25. }
  26. func TestProcessStub(t *testing.T) {
  27.     Convey("Simple process stub example", t, func() {
  28.         // 标记变量
  29.         called := false
  30.         // 为过程函数打桩
  31.         stubs := gostub.Stub(&printLogFunc, func(msg string) {
  32.             called = true
  33.         })
  34.         defer stubs.Reset()
  35.         // 调用业务函数
  36.         DoWorkWithStub()
  37.         // 验证桩函数被调用了
  38.         So(called, ShouldBeTrue)
  39.     })
  40. }
复制代码
3.png

 
  ④ 复杂组合场景
  1. // GoStub/multiple_stubs_test.go
  2. package gostub
  3. import (
  4.     "testing"
  5.     "github.com/prashantv/gostub"
  6.     . "github.com/smartystreets/goconvey/convey"
  7. )
  8. var (
  9.     name = "Alice"
  10.     age  = 25
  11. )
  12. func GetCity() string {
  13.     return "Beijing"
  14. }
  15. var getCityFunc = GetCity
  16. func GetUserInfo() string {
  17.     return name + " is " + string(rune(age)) + " years old, lives in " + getCityFunc()
  18. }
  19. func TestMultipleStubs(t *testing.T) {
  20.     Convey("Multiple stubs example", t, func() {
  21.         // 使用一个stubs对象对多个目标打桩
  22.         stubs := gostub.Stub(&name, "Bob")
  23.         stubs.Stub(&age, 30)
  24.         stubs.StubFunc(&getCityFunc, "Shanghai")
  25.         defer stubs.Reset()
  26.         
  27.         // 验证所有桩都生效了
  28.         So(GetUserInfo(), ShouldEqual, "Bob is 0 years old, lives in Shanghai")
  29.     })
  30. }
  31. //这个例子同时对两个全局变量(name, age)和一个函数(getCityFunc)进行了打桩,使用同一个stubs对象管理,通过一次Reset()调用统一恢复。
复制代码
 
2、GoMock

   安装:
  1. go get -u github.com/golang/mock/gomock
  2. go get -u github.com/golang/mock/mockgen
复制代码
  在service层编写单元测试时,通常需要对repo层进行mock。这是为了确保你的测试只关注service层本身的逻辑,而不是它所以来的外部组件(如数据库、网络等)。
   ① 定义一个接口
  1. package db
  2. type Repository interface {
  3.     Create(key string, value []byte) error
  4.     Retrieve(key string) ([]byte, error)
  5.     Update(key string, value []byte) error
  6.     Delete(key string) error
  7. }
复制代码
  ② 生成mock类文件

  mockgen是GoMock提供的一个命令行工具,用来读取接口定义,然后生成对应的mock文件。它有两种模式:

  • 源文件模式(最常用)
  1. mockgen -source=./infra/db.go -destination=./mock/mock_repository.go -package=mock
  2. //去db.go找接口
  3. //去mock目录下生成mock_repository.go
  4. //生成的包名叫mock
复制代码

  • 反射模式
  1. mockgen database/sql/driver Conn,Driver
  2. // 表示要对database/sql/driver 包下的Conn和Driver接口生成mock
复制代码
  接下来就可以生成mock_repository.go文件了,这是mockgen自动生成的,包含两部分:
  1. // Automatically generated by MockGen. DO NOT EDIT!
  2. // Source: ./infra/db.go (interfaces: Repository)
  3. package mock
  4. import (
  5.     gomock "github.com/golang/mock/gomock"
  6. )
  7. // MockRepository is a mock of Repository interface
  8. type MockRepository struct {
  9.     ctrl     *gomock.Controller
  10.     recorder *MockRepositoryMockRecorder
  11. }
  12. // MockRepositoryMockRecorder is the mock recorder for MockRepository
  13. type MockRepositoryMockRecorder struct {
  14.     mock *MockRepository
  15. }
  16. // NewMockRepository creates a new mock instance
  17. func NewMockRepository(ctrl *gomock.Controller) *MockRepository {
  18.     mock := &MockRepository{ctrl: ctrl}
  19.     mock.recorder = &MockRepositoryMockRecorder{mock}
  20.     return mock
  21. }
  22. // EXPECT returns an object that allows the caller to indicate expected use
  23. func (m *MockRepository) EXPECT() *MockRepositoryMockRecorder {
  24.     return m.recorder
  25. }
  26. // Create mocks base method
  27. func (m *MockRepository) Create(key string, value []byte) error {
  28.     ret := m.ctrl.Call(m, "Create", key, value)
  29.     ret0, _ := ret[0].(error)
  30.     return ret0
  31. }
  32. // Create indicates an expected call of Create
  33. func (mr *MockRepositoryMockRecorder) Create(key, value interface{}) *gomock.Call {
  34.    return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockRepository)(nil).Create), key, value)
  35. }
  36. // Retrieve mocks base method
  37. func (m *MockRepository) Retrieve(key string) ([]byte, error) {
  38.    ret := m.ctrl.Call(m, "Retrieve", key)
  39.    ret0, _ := ret[0].([]byte)
  40.    ret1, _ := ret[1].(error)
  41.    return ret0, ret1
  42. }
  43. // Retrieve indicates an expected call of Retrieve
  44. func (mr *MockRepositoryMockRecorder) Retrieve(key interface{}) *gomock.Call {
  45.    return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Retrieve", reflect.TypeOf((*MockRepository)(nil).Retrieve), key)
  46. }
  47. // Update mocks base method
  48. func (m *MockRepository) Update(key string, value []byte) error {
  49.    ret := m.ctrl.Call(m, "Update", key, value)
  50.    ret0, _ := ret[0].(error)
  51.    return ret0
  52. }
  53. // Update indicates an expected call of Update
  54. func (mr *MockRepositoryMockRecorder) Update(key, value interface{}) *gomock.Call {
  55.    return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockRepository)(nil).Update), key, value)
  56. }
  57. // Delete mocks base method
  58. func (m *MockRepository) Delete(key string) error {
  59.    ret := m.ctrl.Call(m, "Delete", key)
  60.    ret0, _ := ret[0].(error)
  61.    return ret0
  62. }
  63. // Delete indicates an expected call of Delete
  64. func (mr *MockRepositoryMockRecorder) Delete(key interface{}) *gomock.Call {
  65.    return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockRepository)(nil).Delete), key)
  66. }
复制代码
  MYSQL.go编写
  1. package MYSQL
  2. import db "GoExample/GoMock/infra"
  3. type MYSQL struct {
  4.     DB db.Repository
  5. }
  6. func NewMySQL(db db.Repository) *MYSQL {
  7.     return &MySQL{DB: db}
  8. }
  9. func (mysql *MySQL) CreateData(key string, value []byte) error {
  10.     return mysql.DB.Retrieve(key, value)
  11. }
  12. func (mysql *MySQL) GetData(key string) ([]byte, error) {
  13.    return mysql.DB.Retrieve(key)
  14. }
  15. func (mysql *MySQL) DeleteData(key string) error {
  16.    return mysql.DB.Delete(key)
  17. }
  18. func (mysql *MySQL) UpdateData(key string, value []byte) error {
  19.    return mysql.DB.Update(key, value)
  20. }
复制代码
 测试用例MYSQL_test.go编写
  1. package MYSQL
  2. import (
  3.     "testing"
  4.     "GoExample/GoMock/mock"
  5.     "fmt"
  6.     "github.com/golang/mock/gomock"
  7. )
  8. func TestMYSQL_CreateData(t *testing.T) {
  9.     // 1.创建gomock控制器
  10.     // 定义了mock对象的作用域和生命周期,以及期望
  11.     ctr := gomock.NewController(t)
  12.     //2. 结束时检查期望有没有满足
  13.     defer ctr.Finish()
  14.     key := "Hello"
  15.     value := []byte("Go")
  16.     //3.生成一个假的数据库对象
  17.     mockRepo := mock_db.NewMockRepository(ctrl)
  18.     //4.设定期望:若调用Create(”Hello", "Go"), 就返回nil
  19.     mockRepo.EXPECT().Create(key, value).Return(nil)
  20.     //5. 将假的repo对象注入到mySQL对象中(后续需要通过mySQL调用绑定的方法)
  21.     mySQL := NewMYSQL(mockRepo)
  22.     //6. 调用CreateData, 会转发到mockRepo.Create
  23.     err := mySQL.CreateData(key, value)
  24.     if err != nil {
  25.         //7.正常情况下不会打印,因为 err 应该是 nil
  26.         fmt.Println(err)
  27.     }
  28. }
  29. func TestMySQL_GetData(t *testing.T) {
  30.     ctr := gomock.NewController(t)
  31.     defer ctr.Finish()
  32.     key := "Hello"
  33.     value := []byte("Go")
  34.     mockRepo := mock_db.NewMockRepository(ctr)
  35.     // InOrder是期望下面的方法按顺序调用,若调用顺序不一致,就会触发测试失败
  36.     gomock.InOrder(
  37.         mockRepo.EXPECT().Retrieve(key).Return(value, nil),
  38.     )
  39.     mySQL := NewMySQL(mockRepo)
  40.     bytes, err := mySQL.GetData(key)
  41.     if err != nil {
  42.         fmt.Println(err)
  43.     } else {
  44.         fmt.Println(string(bytes))
  45.     }
  46. }
  47. func TestMySQL_UpdateData(t *testing.T) {
  48.     ctr := gomock.NewController(t)
  49.     defer ctr.Finish()
  50.     var key string = "Hello"
  51.     var value []byte = []byte("Go")
  52.     mockRepository := mock_db.NewMockRepository(ctr)
  53.     gomock.InOrder(
  54.        mockRepository.EXPECT().Update(key, value).Return(nil),
  55.     )
  56.     mySQL := NewMySQL(mockRepository)
  57.     err := mySQL.UpdateData(key, value)
  58.     if err != nil {
  59.         fmt.Println(err)
  60.     }
  61. }
  62. func TestMySQL_DeleteData(t *testing.T) {
  63.     ctr := gomock.NewController(t)
  64.     defer ctr.Finish()
  65.     var key string = "Hello"
  66.     mockRepository := mock_db.NewMockRepository(ctr)
  67.     gomock.InOrder(
  68.         mockRepository.EXPECT().Delete(key).Return(nil),
  69.     )
  70.     mySQL := NewMySQL(mockRepository)
  71.     err := mySQL.DeleteData(key)
  72.     if err != nil {
  73.         fmt.Println(err)
  74.     }
  75. }
复制代码
3、Monkey

   前面提到,GoStub 非常适合处理全局变量、函数和过程的打桩,配合 GoMock 还能完成接口的替换。但是当我们遇到 结构体方法 时,问题就变得棘手了。
  在 Go 语言里,方法是与结构体绑定的,像 srv.GetUser(1) 这种调用,GoStub 并不能直接替换。如果项目里大量使用 面向对象风格(struct + 方法),就不得不额外抽象出接口,再通过接口去 mock。这种做法虽然可行,但会让测试代码和业务代码之间产生一定的“距离”,降低了测试的直观性和灵活性。
  为了填补这一空白,就有了另一类更“激进”的工具 —— Monkey 补丁(Monkey Patching)。Monkey 能够在运行时动态替换函数或方法的实现,从而让我们可以直接对结构体方法进行打桩,而无需额外抽象接口。当然,Monkey 的这种方式并不是没有代价:它依赖底层的 unsafe 和 reflect 技术,虽然在测试阶段能带来极大便利,但在生产环境中需要谨慎使用。
  安装命令:go get github.com/bouk/monkey   (1)为一个函数打桩
  1. // Exec是infra层的一个操作函数:
  2. func Exec(cmd string, args ...string) (string, error) {
  3.     cmdpath, err := exec.LookPath(cmd)
  4.     if err != nil {
  5.         fmt.Errorf("exec.LookPath err: %v, cmd: %s", err, cmd)
  6.         return "", infra.ErrExecLookPathFailed
  7.     }
  8.     var output []byte
  9.     output, err = exec.Command(cmdpath, args...).CombinedOutput()
  10.     if err != nil {
  11.         fmt.Errorf("exec.Command.CombinedOutput err: %v, cmd: %s", err, cmd)
  12.         return "", infra.ErrExecCombinedOutputFailed
  13.     }
  14.     fmt.Println("CMD[", cmdpath, "]ARGS[", args, "]OUT[", string(output), "]")
  15.     return string(output), nil
  16. }
  17. // 在这个函数中调用了库函数exec.LoopPath和exec.Command,因此Exec函数的返回值和运行时
  18. // 的底层环境密切相关。若在被测函数中调用了Exec函数,应该根据用例的场景对Exec函数打桩
  19. // 具体的意思就是,打桩的是依赖,里面调用的两个库函数是依赖
复制代码
 
  1. import (
  2.     "testing"
  3.     . "github.com/smartystreets/goconvey/convey"
  4.     . "github.com/bouk/monkey"
  5.     "infra/osencap"
  6. )
  7. const any = "any"
  8. func TestExec(t *testing.T) {
  9.     Convey("test has digit", t, func() {
  10.         Convey("for succ", func() {
  11.             outputExpect := "xxx-vethName100-yyy"
  12.             // 运行时打桩,将进程内所有对osencap.Exec的调用,都跳转到这个匿名函数上
  13.             guard := Patch(
  14.              osencap.Exec,
  15.              func(_ string, _ ...string) (string, error) {
  16.                  return outputExpect, nil
  17.              })
  18.             defer guard.Unpatch()
  19.             output, err := osencap.Exec(any, any)
  20.             So(output, ShouldEqual, outputExpect)
  21.             So(err, ShouldBeNil)
  22.         })
  23.     })
  24. }
  25. // Patch的第一个参数是:要被替换的目标函数”的函数标识符
  26. // 第二个参数是:替身函数,一般写成匿名函数
  27. // guard.Unpatch() 取消本次补丁,恢复原实现
  28. // UnpatchAll() 一次性移除所有补丁(但多数时候用defer guard.Unpatch()更安全)
复制代码
注意:

  • Monkey在进程级生效,并发/并行的用例可能互相影响
  • 这个补丁对进程内所有调用点生效,所以务必defer
   (2)为一个过程打桩
  1. // 当一个函数没有返回值时,该函数一般称为过程。
  2. func TestDestroyResource(t *testing.T) {
  3.     called := false
  4.     guard := Patch(DestroyResource, func(_ string) {
  5.         called = true
  6.     })
  7.     defer guard.Unpatch()
  8.     DestroyResource("abc") // 实际不会执行原逻辑
  9.     if !called {
  10.         t.Errorf("expected patched DestroyResource to be called")
  11.     }
  12. }
复制代码
  (3)为一个方法打桩

  假如有一个服务(如任务调度服务),不只跑一份,而是启动了好几个实例(进程),那么此时用Etcd做选举,选出一个“Master”。Master负责把所有任务分配给各个实例,然后把分配的结果写到Etcd。剩下的实例Node通过Watch功能实时监听Etcd的任务分配结果,收到任务列表后,每个实例根据自己的instanceId过滤,只挑出属于自己的任务去执行。
  现在我们需要给Etcd.Get()方法打桩,使得每个实例在输入自己的instanceId时,会返回固定的任务列表。
  1. func (e *Etcd) Get(instanceId string) []string {
  2.     // 本来这里应该去 Etcd 拿属于 instanceId 的任务
  3.     return []string{} // 真实情况依赖外部环境
  4. }
  5. var e *Etcd        //只是声明一个指针变量,不需要真正赋值
  6. guard := PatchInstanceMethod(
  7. reflect.TypeOf(e),        // 表示etcd类型的方法
  8. "Get",                    //方法名
  9. func(_ *Etcd, _ string) []string {        //替身函数(签名要一致)
  10.   return []string{"task1", "task5", "task8"}
  11. })
  12. defer guard.Unpatch()
复制代码
  (4)任意相同或不同的基本场景组合
  1. Px1
  2. defer UnpatchAll()
  3. Px2
  4. ...
  5. Pxn
复制代码
  (5)桩中桩的一个案例
  1. type Movie strcut {
  2.     Name string
  3.     Type string
  4.     Score int
  5. }
  6. //定义一个interface类型
  7. type Repository struct {
  8.     // 传进去一个空指针movie,希望返回的时候把movie填上内容,然后返回error
  9.     // 但是真实的Retrieve要连数据库,太重了。用GoMock虽然能拦截调用,但GoMock只能决定返回值
  10.     // (比如只能返回nil),却不能真正往movie里面填数据
  11.     Retrieve(key string, movie *movie) error
  12. }   
  13. // ---------------------------------------------------------
  14. func TestDemo(t *testing.T) {
  15.     Convey("test demo", t, func() {
  16.         Convey("retrieve movie", func() {
  17.             ctrl := NewController(t)
  18.             defer ctrl.Finish()
  19.             mockRepo := mock_db.NewMockRepository(ctrl)
  20.             mockRepo.EXPECT().Retrieve(Any(), Any()).Return(nil)
  21.             Patch(redisrepo.GetInstance, func() Repository {
  22.                 return mockRepo
  23.             })
  24.             defer UnpatchAll()
  25.             PatchInstanceMethod(reflect.TypeOf(mockRepo), "Retrieve", func(_ *mock_db.MockRepository, name string, movie *Movie) error {
  26.                 movie = &Movie{Name: name, Type: "Love", Score: 95}
  27.                 return nil
  28.             })
  29.             repo := redisrepo.GetInstance()
  30.             var movie *Movie
  31.             err := repo.Retrieve("Titanic", movie)
  32.             So(err, ShouldBeNil)
  33.             So(movie.Name, ShouldEqual, "Titanic")
  34.             So(movie.Type, ShouldEqual, "Love")
  35.             So(movie.Score, ShouldEqual, 95)
  36.         })
  37.         ...
  38.     })
  39. }
复制代码
桩中桩的做法:

  • 第一层:GoMock,把Retrieve换成假的实现,让它在调用时不会连接数据库,但只能返回nil,没法改movie
  • 第二层:Monkey Patch,把这个假的Retrieve方法本身替换掉。写一个补丁函数,在里面手动改movie的值。
整个流程:
- 程序里调用repo.Retrieve( "Titanic", movie)
- 实际走到GoMock的桩:但GoMock的桩又被Monkey Patch替换了
- 最终执行的是你写的补丁函数,它把movie填好,并返回nil
- 测试代码断言movie的值是否符合预期

为什么不能只用Monkey?
- GoMock能mock接口,管理调用次数、顺序,返回error
- 只用Monkey 的话,不能校验Retrieve方法到底被调用了几次,参数是不是对的。
mockRepo.EXPECT().Retrieve("Titanic", gomock.Any()).Return(nil).Times(2) 这里Times的意思是必须调用2次
4、HTTPTest

https://pkg.go.dev/net/http/httptest
  由于 Go 标准库的强大支持,Go 可以很容易的进行 Web 开发。为此,Go 标准库专门提供了 net/http/httptest 包专门用于进行 http Web 开发测试。
  1. var GetUserHost = "https://account.wps.cn"
  2. // 默认情况下,GetUser会调用https://account.wps.cn/p/auth/check这个真实的接口
  3. // 但在测试时不想依赖外部网络,所以要“伪造”一个接口服务器
  4. func GetUser(wpssid, xff string) *User {
  5.     url := fmt.Sprintf("%s/p/auth/check", GetUserHost)
  6.     user := client.POST(url, ...)
  7.     return user
  8. }
  9. func TestGetUser(t *testing.T) {
  10.     // 用httptest.NewServer启动了一个本地HTTP服务器
  11.     // 它只实现一个接口POST /p/auth/check,并且返回一个固定的JSON(模拟线上接口的返回)
  12.     svr := httptest.NewServer(func () http.HandlerFunc {
  13.         r := gin.Default()
  14.         r.POST("/p/auth/check", func(c *gin.Context) {
  15.             c.JSON(http.StatusOK, gin.H{
  16.                 "result":      "ok",
  17.                 "companyid":   1,
  18.                 "nickname":    "test-account",
  19.                 "account":     "123***789@test.com",
  20.                 "phonenumber": "123***789",
  21.                 "pic":         "https://pic.test.com",
  22.                 "status":      "active",
  23.                 "userid":      currentUserID,
  24.             })
  25.         })
  26.         return r.ServeHTTP
  27.     })
  28.     defer svr.Close()    //测试结束后关闭这个临时服务器
  29.     GetUserHost = svr.URL
  30.     user := GetUser("test-wps-id", "")
  31.     ...
  32. }
复制代码
5、如何理解Golang测试框架和打桩框架的关系
  1. 测试框架是骨架:提供运行环境+断言机制
  2. 打桩框架是工具:帮你在测试环境中模拟依赖,制造可控场景
  3. 它们是配合关系,而不是互相替代。
  4. 若没有测试框架,写了桩也没地方运行。
  5. 若没有打桩框架,你测试代码可能跑不了(真实依赖很复杂)
复制代码
 
覆盖率

1、单元测试执行
  1. # 匹配当前目录下*_test.go命令的文件,执行每一个测试函数
  2. go test -v
  3. # 执行 calc_test.go 文件中的所有测试函数
  4. go test -v calc_test.go calc.go
  5. # 指定特定的测试函数(其中:-count=1用于跳过缓存)
  6. go test -v -run TestAdd calc_test.go calc.go -count=1
  7. #调试单元测试文件。运行命令时,当前目录应为项目根目录。
  8. go test ./...     #运行所有包单元测试文件
  9. go test ${包名}     #运行指定包的单元测试文件
  10. go test ${指定路径}    #运行指定路径的单元测试文件
复制代码
 
2、生成单元测试覆盖率
  1. go test -v -covermode=set -coverprofile=cover.out -coverpkg=./...
  2. 其中,
  3. -covermode 有三种模式:
  4. • set 语句是否执行,默认模式
  5. • count 每个语句执行次数
  6. • atomic 类似于count,表示并发程序中的精确技术
  7. -coverprofile是统计信息保存的文件。
复制代码
 
3、查看单元测试覆盖率
  1. //(1)查看每个函数的覆盖情况
  2. go tool cover -func=cover.out
  3. //(2)使用网页方式
  4. go tool cover -html=cover.out
复制代码
 
 
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

相关推荐

您需要登录后才可以回帖 登录 | 立即注册