• GoLang 单元测试打桩和 mock


    目录

    什么是 mock

    变量打桩

    接口方法/Redis

    函数/方法打桩

    包函数

    成员方法

    MySQL

    sqlmock

    sqlite mock gorm

    http mock


    源码地址

    单测基础

    什么是 mock

           单元测试,顾名思义对某个单元函数进行测试,被测函数本身中用到的变量、函数、资源不应被测试代码依赖,所谓 mock,就是想办法通过 “虚拟” 代码替换掉依赖的方法和资源,一般需要 mock 掉以下依赖:

    • 变量

    • 函数/方法

    • MySQL

    • Redis

    • http 调用

    变量打桩

    有时我们的代码里依赖一个全局变量,测试方法根据全局变量的不同值执行不同的逻辑,那么可以用 gostub 对变量进行打桩。

     global.go:

    1. package main
    2. var size = 5
    3. func Size() int {
    4. if size > 10 {
    5. return 10
    6. }
    7. return size
    8. }
    1. package main
    2. import (
    3. "testing"
    4. "github.com/agiledragon/gomonkey/v2"
    5. "github.com/prashantv/gostub"
    6. )
    7. func TestSizeStub(t *testing.T) {
    8. tests := []struct {
    9. name string
    10. want int
    11. f func() *gostub.Stubs
    12. }{
    13. {name: "size > 10", want: 10, f: func() *gostub.Stubs {
    14. return gostub.Stub(&size, 11)
    15. }},
    16. {name: "size <= 10", want: 3, f: func() *gostub.Stubs {
    17. return gostub.Stub(&size, 3)
    18. }},
    19. }
    20. for _, tt := range tests {
    21. t.Run(tt.name, func(t *testing.T) {
    22. stub := tt.f()
    23. if got := Size(); got != tt.want {
    24. t.Errorf("Size() = %v, want %v", got, tt.want)
    25. }
    26. stub.Reset()
    27. })
    28. }
    29. }
    30. func TestSizeMonkey(t *testing.T) {
    31. tests := []struct {
    32. name string
    33. want int
    34. f func() *gomonkey.Patches
    35. }{
    36. {name: "size > 10", want: 10, f: func() *gomonkey.Patches {
    37. return gomonkey.ApplyGlobalVar(&size, 11)
    38. }},
    39. {name: "size <= 10", want: 3, f: func() *gomonkey.Patches {
    40. return gomonkey.ApplyGlobalVar(&size, 3)
    41. }},
    42. }
    43. for _, tt := range tests {
    44. t.Run(tt.name, func(t *testing.T) {
    45. stub := tt.f()
    46. if got := Size(); got != tt.want {
    47. t.Errorf("Size() = %v, want %v", got, tt.want)
    48. }
    49. stub.Reset()
    50. })
    51. }
    52. }
    1. $ go test -v -cover
    2. === RUN TestSize
    3. === RUN TestSize/size_>_10
    4. === RUN TestSize/size_<=_10
    5. --- PASS: TestSize (0.00s)
    6. --- PASS: TestSize/size_>_10 (0.00s)
    7. --- PASS: TestSize/size_<=_10 (0.00s)
    8. PASS
    9. coverage: 100.0% of statements

    接口方法/Redis

    首先 Go 语言推荐的是面向接口编程,所以官方提供并推荐使用  gomock  对依赖的方法进行 mock,前提是依赖的方法是通过抽象接口实现的,gomock 执行过程如下:

    1. 使用mockgen为你想要mock的接口生成一个mock。

    2. 在你的测试代码中,创建一个gomock.Controller实例并把它作为参数传递给mock对象的构造函数来创建一个mock对象。

    3. 调用EXPECT()为你的mock对象设置各种期望和返回值。

    4. 调用mock控制器的Finish()以验证mock的期望行为。

    gomock 常用方法:

    类型

    用法

    作用

    参数

    gomock.Any(v)

    匹配任何类型

    gomock.Eq(v)

    匹配使用反射 reflect.DeepEqual 与 v 相等的值

    gomock.Not(v)

    v 不是 Matcher 时,匹配使用反射 reflect.DeepEqual 与 v 不相等的值;v 是 Matcher 时,匹配和 Macher 不匹配的值(Matcher

    gomock.Nil()

    匹配等于 nil 的值

    返回

    Return()

    mock 方法返回值

    Do(func)

    传入的 func 在 mock 真正被调用时自动执行,忽略 Return,比如:对调用方法的参数进行校验

    DoAndReturn(func)

    传入的 func 在 mock 真正被调用时自动执行,对应 func 返回值作为 mock 方法返回值

    调用次数

    AnyTimes(n int)

    mock 方法可以被调用任意次数,一次不调用也不会失败(这里大家可以自检一下各自的单测代码,用这个方法的单测可能并没有按照预期运行

    Times()

    mock 方法被调用次数,次数不相等运行失败

    MaxTimes(n int)

    mock 方法被调用次数,大于规定次数运行失败

    MinTimes(n int)

    mock 方法被调用次数,小于规定次数运行失败

    调用排序

    gomock.InOrder(

    first.EXPECT.Func().Return(),

    second.EXPECT.Func().Return(),

    thrid.EXPECT.Func().Return(),

    )

    规定多个 mock 方法的调用顺序,顺序不符运行失败

    first := rc.EXPECT().DoFucn()

    second := rc.EXPECT().DoFunc().After(first)

    规定多个 mock 方法的先后依赖关系,顺序不符运行失败

    首先通过 mockgen 生成 Redis Client 的 mock 代码:

    1. $ go get -u github.com/golang/mock/gomock
    2. $ go install github.com/golang/mock/mockgen
    3. 本地interface:
    4. mockgen[go run -mod=mod github.com/golang/mock/mockgen -package mock] -source ~/go/pkg/mod/github.com/opentracing/opentracing-go\@v1.2.0/tracer.go -destination ./opentracing/tracer.go Tracer
    5. 远端interface:
    6. go run -mod=mod github.com/golang/mock/mockgen -package redis -destination ./mock/redis/redis.go github.com/go-redis/redis/v8 Cmdable

    redis.go:

    1. package main
    2. import (
    3. "context"
    4. "github.com/go-redis/redis/v8"
    5. )
    6. func handleRedis(c redis.Cmdable) (string, error) {
    7. return c.Get(context.Background(), "redis").Result()
    8. }
    9. func conn() *redis.Client {
    10. return redis.NewClient(&redis.Options{Addr: "127.0.0.1:6379"})
    11. }
    1. package main
    2. import (
    3. "testing"
    4. "github.com/go-redis/redis/v8"
    5. "github.com/golang/mock/gomock"
    6. )
    7. func Test_handleRedis(t *testing.T) {
    8. ctl := gomock.NewController(t)
    9. defer ctl.Finish()
    10. c := NewMockCmdable(ctl)
    11. c.EXPECT().Get(gomock.Any(), gomock.Any()).Times(1).Return(redis.NewStringResult("redis", nil))
    12. handleRedis(c)
    13. }

    函数/方法打桩

    假如我们依赖的其他人写的方法,并不是通过接口实现的,无法使用 gomock 时,可以用 gomonkey 进行打桩

    包函数

    常用函数:

    • gomonkey.ApplyFunc():单个包函数打桩

    • gomonkey.ApplyFuncSeq():连续多个包函数打桩

    func.go

    1. package main
    2. func A() int {
    3. return B()
    4. }
    5. func AA() int {
    6. return B() + B()
    7. }
    8. func B() int {
    9. return 0
    10. }
    1. package main
    2. import (
    3. "testing"
    4. "github.com/agiledragon/gomonkey/v2"
    5. "github.com/stretchr/testify/assert"
    6. )
    7. // TestA 函数,单次打桩
    8. func TestA(t *testing.T) {
    9. patch := gomonkey.ApplyFunc(B, func() int {
    10. return 1
    11. })
    12. defer patch.Reset()
    13. assert.Equal(t, 1, A())
    14. }
    15. // TestAA 函数,连续打桩
    16. func TestAA(t *testing.T) {
    17. patch := gomonkey.ApplyFuncSeq(B, []gomonkey.OutputCell{
    18. {Values: gomonkey.Params{1}},
    19. {Values: gomonkey.Params{2}},
    20. })
    21. defer patch.Reset()
    22. assert.Equal(t, 3, AA())
    23. }

    成员方法

    常用函数:

    • gomonkey.ApplyMethod():单个公有成员方法打桩

    • patch.ApplyPrivateMethod():单个私有成员方法打桩

    • patch.ApplyMethodSeq():连续多个公有成员方法打桩

    • gomonkey.ApplyFuncSeq():连续多个私有成员方法打桩

    method.go

    1. package main
    2. type S struct{}
    3. func (s *S) A() int {
    4. return s.B() + s.b()
    5. }
    6. func (s *S) AA() int {
    7. return s.B() + s.b() + s.B() + s.b()
    8. }
    9. func (s *S) B() int {
    10. return 0
    11. }
    12. func (s *S) b() int {
    13. return 0
    14. }
    1. package main
    2. import (
    3. "reflect"
    4. "testing"
    5. "github.com/agiledragon/gomonkey/v2"
    6. "github.com/stretchr/testify/assert"
    7. )
    8. // TestS_AA 成员方法单个打桩
    9. func TestS_A(t *testing.T) {
    10. s := &S{}
    11. // 公共成员方法
    12. patch := gomonkey.ApplyMethod(reflect.TypeOf(s), "B", func(_ *S) int {
    13. return 1
    14. })
    15. // 私有成员方法
    16. patch.ApplyPrivateMethod(reflect.TypeOf(s), "b", func(_ *S) int {
    17. return 2
    18. })
    19. defer patch.Reset()
    20. assert.Equal(t, 3, s.A())
    21. }
    22. // TestS_AA 成员方法连续打桩
    23. func TestS_AA(t *testing.T) {
    24. s := &S{}
    25. // 私有成员方法
    26. patch := gomonkey.ApplyFuncSeq((*S).b, []gomonkey.OutputCell{
    27. {Values: gomonkey.Params{1}},
    28. {Values: gomonkey.Params{2}},
    29. })
    30. // 公共成员方法
    31. patch.ApplyMethodSeq(reflect.TypeOf(s), "B", []gomonkey.OutputCell{
    32. {Values: gomonkey.Params{1}},
    33. {Values: gomonkey.Params{2}},
    34. })
    35. defer patch.Reset()
    36. assert.Equal(t, 6, s.AA())
    37. }

    MySQL

    sqlmock

    db.go

    1. package main
    2. import (
    3. "database/sql"
    4. "encoding/json"
    5. "fmt"
    6. _ "github.com/go-sql-driver/mysql"
    7. "github.com/jmoiron/sqlx"
    8. )
    9. const dsn = "root:123456@tcp(127.0.0.1:3306)/test"
    10. type Test struct {
    11. ID int64 `json:"id" db:"id" gorm:"column:id"`
    12. GoodsID int64 `json:"goodsID" db:"goods_id" gorm:"column:goods_id"`
    13. Name string `json:"name" db:"name" gorm:"column:name"`
    14. }
    15. func (Test) TableName() string {
    16. return "test"
    17. }
    18. func handle(db *sql.DB) (err error) {
    19. tx, err := db.Begin()
    20. if err != nil {
    21. return
    22. }
    23. defer func() {
    24. switch err {
    25. case nil:
    26. err = tx.Commit()
    27. default:
    28. tx.Rollback()
    29. }
    30. }()
    31. rows, err := tx.Query("SELECT * from test where id > ?", 0)
    32. if err != nil {
    33. panic(err)
    34. }
    35. result := []Test{}
    36. if err = sqlx.StructScan(rows, &result); err != nil {
    37. panic(err)
    38. }
    39. b, err := json.Marshal(result)
    40. if err != nil {
    41. panic(err)
    42. }
    43. fmt.Println("sql:", string(b))
    44. if _, err = tx.Exec("UPDATE test SET goods_id = goods_id + 1 where id = 2"); err != nil {
    45. return
    46. }
    47. if _, err = tx.Exec("INSERT INTO test (goods_id, name) VALUES (?, ?)", 1, "1"); err != nil {
    48. return
    49. }
    50. return
    51. }
    52. func main() {
    53. db, err := sql.Open("mysql", dsn)
    54. if err != nil {
    55. panic(err)
    56. }
    57. defer db.Close()
    58. if err = handle(db); err != nil {
    59. panic(err)
    60. }
    61. }
    1. package main
    2. import (
    3. "log"
    4. "os"
    5. "testing"
    6. "time"
    7. "github.com/DATA-DOG/go-sqlmock"
    8. _ "github.com/go-sql-driver/mysql"
    9. "github.com/stretchr/testify/assert"
    10. )
    11. func Test_handle(t *testing.T) {
    12. db, mock, err := sqlmock.New()
    13. if err != nil {
    14. panic(err)
    15. }
    16. mock.ExpectBegin()
    17. // (.+) 用于替代字段,可用于 select、order、group等
    18. mock.ExpectQuery("SELECT (.+) from test where id > ?").WillReturnRows(sqlmock.NewRows([]string{"id", "goods_id", "name"}).AddRow(1, 1, "1"))
    19. // sql前缀匹配
    20. mock.ExpectExec("UPDATE test SET goods_id").WillReturnResult(sqlmock.NewResult(1, 1))
    21. mock.ExpectExec("INSERT INTO test").WithArgs(1, "1").WillReturnResult(sqlmock.NewResult(1, 1))
    22. mock.ExpectCommit()
    23. if err = handle(db); err != nil {
    24. panic(err)
    25. }
    26. if err = mock.ExpectationsWereMet(); err != nil {
    27. panic(err)
    28. }
    29. }

    sqlite mock gorm

    如果遇到如下错误:

    1. /usr/local/go16/pkg/tool/linux_amd64/link: running gcc failed: exit status 1
    2. /usr/bin/ld: /tmp/go-link-866330658/000020.o(.text+0x74): unresolvable H��@�>H��FH��H��H��@�~�F�H��@�~H��8�H��H��0�FH��H��(�FH��H�� �FH��H���FH��H���FH��H��F�fD relocation against symbol `stderr@@GLIBC_2.2.5'
    3. /usr/bin/ld: BFD version 2.20.51.0.2-5.34.el6 20100205 internal error, aborting at reloc.c line 443 in bfd_get_reloc_size
    4. /usr/bin/ld: Please report this bug.
    5. collect2: ld returned 1 exit status
    6. 更新 go env gcc 版本:
    7. go env -w CC=/opt/compiler/gcc-8.2/bin/gcc
    8. go env -w CXX=/opt/compiler/gcc-8.2/bin/g++
    9. CC=/opt/compiler/gcc-8.2/bin/gcc CXX=/opt/compiler/gcc-8.2/bin/g++ go test -c -cover

    db.go

    1. package main
    2. import (
    3. "database/sql"
    4. "encoding/json"
    5. "fmt"
    6. "gorm.io/driver/mysql"
    7. "gorm.io/gorm"
    8. )
    9. const dsn = "root:123456@tcp(127.0.0.1:3306)/test"
    10. type Test struct {
    11. ID int64 `json:"id" db:"id" gorm:"column:id"`
    12. GoodsID int64 `json:"goodsID" db:"goods_id" gorm:"column:goods_id"`
    13. Name string `json:"name" db:"name" gorm:"column:name"`
    14. }
    15. func (Test) TableName() string {
    16. return "test"
    17. }
    18. func main() {
    19. orm, err := gorm.Open(mysql.Open(dsn))
    20. if err != nil {
    21. panic(err)
    22. }
    23. handleOrm(orm)
    24. }
    25. func handleOrm(orm *gorm.DB) {
    26. var rows []Test
    27. clause := func(db *gorm.DB) *gorm.DB {
    28. return db.Where("id >= ?", 1)
    29. }
    30. err := clause(orm.Select("*")).Find(&rows).Error
    31. if err != nil {
    32. panic(err)
    33. }
    34. b, err := json.Marshal(rows)
    35. if err != nil {
    36. panic(err)
    37. }
    38. fmt.Println("gorm", string(b))
    39. }
    1. package main
    2. import (
    3. "log"
    4. "os"
    5. "testing"
    6. "time"
    7. "github.com/stretchr/testify/assert"
    8. "gorm.io/driver/sqlite"
    9. "gorm.io/gorm"
    10. "gorm.io/gorm/logger"
    11. )
    12. func Test_handleOrm(t *testing.T) {
    13. db := NewMemoryDB()
    14. err := db.Migrator().CreateTable(&Test{})
    15. assert.Nil(t, err)
    16. handleOrm(db)
    17. }
    18. func NewMemoryDB() *gorm.DB {
    19. var db *gorm.DB
    20. var err error
    21. newLogger := logger.New(
    22. log.New(os.Stdout, "\r\n", log.LstdFlags), // io writer
    23. logger.Config{
    24. SlowThreshold: time.Second, // 慢 SQL 阈值
    25. LogLevel: logger.Info, // Log level
    26. Colorful: false, // 禁用彩色打印
    27. },
    28. )
    29. dialector := sqlite.Open(":memory:?cache=shared")
    30. if db, err = gorm.Open(dialector, &gorm.Config{
    31. Logger: newLogger,
    32. }); err != nil {
    33. panic(err)
    34. }
    35. dba, err := db.DB()
    36. dba.SetMaxOpenConns(1)
    37. return db
    38. }
    39. func CloseMemoryDB(db *gorm.DB) {
    40. sqlDB, _ := db.DB()
    41. sqlDB.Close()
    42. }

    http mock

    http.go

    1. package main
    2. import (
    3. "fmt"
    4. "net/http"
    5. "time"
    6. )
    7. func Send() (err error) {
    8. req, err := http.NewRequest(http.MethodGet, "https://127.0.0.1:8080", nil)
    9. if err != nil {
    10. return
    11. }
    12. client := &http.Client{
    13. Timeout: time.Second,
    14. }
    15. resp, err := client.Do(req)
    16. if err != nil {
    17. return
    18. }
    19. defer resp.Body.Close()
    20. if resp.StatusCode != http.StatusOK {
    21. return fmt.Errorf("HTTP status is %d", resp.StatusCode)
    22. }
    23. return
    24. }
    1. package main
    2. import (
    3. "net/http"
    4. "testing"
    5. "github.com/jarcoal/httpmock"
    6. "github.com/smartystreets/goconvey/convey"
    7. "github.com/stretchr/testify/assert"
    8. )
    9. func TestSend(t *testing.T) {
    10. convey.Convey("TestSend", t, func() {
    11. convey.Convey("success", func() {
    12. httpmock.Activate()
    13. defer httpmock.DeactivateAndReset()
    14. httpmock.RegisterResponder(http.MethodGet, "https://127.0.0.1:8080", httpmock.NewStringResponder(http.StatusOK, ""))
    15. err := Send()
    16. assert.Nil(t, err)
    17. })
    18. })
    19. }

  • 相关阅读:
    【JAVA】java常见面试题——持续更新
    小阿轩yx-Nginx 网站服务
    安科瑞为工业能效提升行动计划提供EMS解决方案-安科瑞黄安南
    你知道 Java 有哪些引用吗?
    es6 语法,在个别浏览器中不兼容的处理办法
    kaldi 报错:data/lang/L_disambig.fst is not olabel sorted
    Cy5.5 N-羟基琥珀酰亚胺酯,Cy5.5 nhs ester,CAS:1469277-96-0
    Intellij IDEA 运行时报 Command line is too long
    Python21天学习挑战赛Day(8)·多进程
    TS查漏补缺【类型守卫】
  • 原文地址:https://blog.csdn.net/why444216978/article/details/126732662