今天带来一篇让你在虎年如虎添翼的Go测试工具和技巧的文章分享。大体内容翻译于 Go (Golang): Testing tools & tips to step up your game[1]。文中作者介绍了不少好用的Go库和工具。作者在也已经将这篇文章中提到的所有工具和窍门投入了个人项目中,更多细节可以查看这里:https://github.com/rafael-piovesan/go-rocket-ride。大家也可以在自己的项目中尝试运用。

Testcontainers

作者在文章落笔之时,第一个想到的工具是Testcontainers-Go(https://golang.testcontainers.org/)。下面Testcontainers的介绍。

https://github.com/testcontainers/testcontainers-go 是一个Go包,它使得创建和清理基于容器的依赖关系变得简单,主要用于自动化集成/冒烟测试。清晰和易于使用的API使开发者能够以编程方式定义应该作为测试的一部分运行的容器,并在测试完成后清理这些资源。这意味着你可以在你的应用程序的集成和冒烟测试最需要的时候和地方,以某种无缝的方式,不依赖外部资源,以编程方式创建、互动和处置容器。在实践中,不再需要保留和维护各种docker-compose文件、复杂的shell脚本和其他东西。你可以根据你的需要生成许多容器,每一个都是为了一个特定的场景,只要它们对运行你的测试有必要,就可以把它们保留下来。

下面是我如何建立一个Postgres容器并用于我的集成测试(更多细节见https://github.com/rafael-piovesan/go-rocket-ride)。

// NewPostgresContainer creates a Postgres container and returns its DSN to be used
// in tests along with a termination callback to stop the container.
func NewPostgresContainer() (string, func(context.Context) error, error) {ctx := context.Background()usr := "postgres"pass := "postgres"db := "testdb"templateURL := "postgres://%s:%s@localhost:%s/%s?sslmode=disable"// Create the containerc, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{Started: true,ContainerRequest: testcontainers.ContainerRequest{Image: "postgres:14.1",ExposedPorts: []string{"0:5432",},Env: map[string]string{"POSTGRES_DB":       db,"POSTGRES_USER":     usr,"POSTGRES_PASSWORD": pass,"POSTGRES_SSL_MODE": "disable",},Cmd: []string{"postgres", "-c", "fsync=off",},WaitingFor: wait.ForSQL("5432/tcp","postgres",func(p nat.Port) string {return fmt.Sprintf(templateURL, usr, pass, p.Port(), db)},).Timeout(time.Second * 15),},})if err != nil {return "", func(context.Context) error { return nil }, err}// Find ports assigned to the new containerports, err := c.Ports(ctx)if err != nil {return "", func(context.Context) error { return nil }, err}driverURL := fmt.Sprintf(templateURL, usr, pass, ports["5432/tcp"][0].HostPort, db)return driverURL, c.Terminate, nil
}// Using it your tests
ctx := context.Background()
dsn, terminate, err := testcontainer.NewPostgresContainer()
if err != nil {log.Fatalf("cannot create database container: %v", err)
}
defer terminate(ctx)sqldb, err := sql.Open("pg", dsn)
if err != nil {log.Fatalf("cannot open database: %v", err)
}

上面这段代码就是创建Postgres测试容器的实用代码

如果你觉得这个很有趣,那么也可以看看这两个其他项目。

  • Dockertest (https://github.com/ory/dockertest),旨在提供与Testcontainers-Go所提供的相同的易于使用、更广泛和通用的API。

  • Gnomock (https://github.com/orlangure/gnomock), 无需使用临时 Docker 容器编写模拟即可测试您的代码 ,被很多项目所普遍使用。

Testify

Testify(https://github.com/stretchr/testify)已经存在了很长时间,它几乎不需要任何介绍。但是,由于它是一个提高测试质量的有用工具和技巧的列表,它不能被遗漏。最受欢迎的功能包括简单的断言和创建模拟对象的机制,也可用于检查对它们的期望。

func TestSomething(t *testing.T) {// assert equalityassert.Equal(t, 123, 123, "they should be equal")// assert inequalityassert.NotEqual(t, 123, 456, "they should not be equal")// assert for nil (good for errors)assert.Nil(t, object)// assert for not nil (good when you expect something)if assert.NotNil(t, object) {// now we know that object isn't nil, we are safe to make// further assertions without causing any errorsassert.Equal(t, "Something", object.Value)}
}

断言的例子来自:https://github.com/stretchr/testify

// MyMockedObject is a mocked object that implements an interface
// that describes an object that the code I am testing relies on.
type MyMockedObject struct{mock.Mock
}// DoSomething is a method on MyMockedObject that implements some interface
// and just records the activity, and returns what the Mock object tells it to.
//
// In the real object, this method would do something useful, but since this
// is a mocked object - we're just going to stub it out.
//
// NOTE: This method is not being tested here, code that uses this object is.
func (m *MyMockedObject) DoSomething(number int) (bool, error) {args := m.Called(number)return args.Bool(0), args.Error(1)
}// TestSomething is an example of how to use our test object to
// make assertions about some target code we are testing.
func TestSomething(t *testing.T) {// create an instance of our test objecttestObj := new(MyMockedObject)// setup expectationstestObj.On("DoSomething", 123).Return(true, nil)// call the code we are testingtargetFuncThatDoesSomethingWithObj(testObj)// assert that the expectations were mettestObj.AssertExpectations(t)
}

模拟例子来自:https://github.com/stretchr/testify

Mockery

Mockery (https://github.com/vektra/mockery) 是一个补充工具(Golang 的模拟代码自动生成器),可以和Testify的Mock包一起使用,利用它的功能来创建模拟对象,并为项目源代码中的Golang接口自动生成模拟对象。它有助于减少很多模板式的编码任务。

// the interface declaration found in your project's source code
// Stringer接口声明
type Stringer interface {String() string
}// the generated mock file based on the interface
import "github.com/stretchr/testify/mock"type Stringer struct {mock.Mock
}func (m *Stringer) String() string {ret := m.Called()var r0 stringif rf, ok := ret.Get(0).(func() string); ok {r0 = rf()} else {r0 = ret.Get(0).(string)}return r0
}

模拟对象的例子来自:https://github.com/vektra/mockery

Gofakeit

Gofakeit(https://github.com/brianvoe/gofakeit)是一个随机数据生成器。目前,它提供了160多个函数,涵盖少数不同的主题/类别,如PersonAnimalsAddressGamesCarsBeers等等。这里的关键词是随机。随机性是计划和编写测试时需要考虑的一个重要方面。与其在测试用例中使用常量值,不如使用随机生成的值,这样我们会更有信心,即使面对未知的(随机的)输入,我们的代码仍然会以我们期望的方式表现出来。

import "github.com/brianvoe/gofakeit/v6"gofakeit.Name()             // Markus Moen
gofakeit.Email()            // alaynawuckert@kozey.biz
gofakeit.Phone()            // (570)245-7485
gofakeit.BS()               // front-end
gofakeit.BeerName()         // Duvel
gofakeit.Color()            // MediumOrchid
gofakeit.Company()          // Moen, Pagac and Wuckert
gofakeit.CreditCardNumber() // 4287271570245748
gofakeit.HackerPhrase()     // Connecting the array won't do anything, we need to generate the haptic COM driver!
gofakeit.JobTitle()         // Director
gofakeit.CurrencyShort()    // USD// randomly pick an item from a predefined list
numberList := []int{1, 36, 41, 99}
idx := gofakeit.Number(0, len(numberList)-1)
randomItem := numberList[idx]

例子来自:https://github.com/brianvoe/gofakeit

Gock

Gock(https://github.com/h2non/gock)是一个用于Golang的HTTP服务器模拟和期望库,它在很大程度上受到了NodeJs的流行和较早的同类库的启发,称为Nock[2]。与Pact-go(https://github.com/pact-foundation/pact-go)不同,它是一个轻量级的解决方案,通过http.DefaultTransport或任何http.Client使用的http.Transport拦截出站请求。

func TestSimple(t *testing.T) {defer gock.Off()gock.New("http://foo.com").Get("/bar").Reply(200).JSON(map[string]string{"foo": "bar"})res, err := http.Get("http://foo.com/bar")assert.Nil(t, err)assert.Equal(t, 200, res.StatusCode)body, _ := ioutil.ReadAll(res.Body)assert.Equal(t, `{"foo":"bar"}`, string(body)[:13])// Verify that we don't have pending mocksassert.True(t, gock.IsDone())
}

如何使用Gock的例子

Testfixtures

Testfixtures (https://github.com/go-testfixtures/testfixtures) [模仿 "Ruby on Rails方式 "为数据库应用程序编写测试](https://guides.rubyonrails.org/testing.html#the-test-database "模仿 "Ruby on Rails方式 "为数据库应用程序编写测试"),其中样本数据保存在fixtures文件中。在执行每个测试之前,测试数据库都会被清理并将fixture数据加载到数据库中。以下是我在我的项目中使用的一个例子(更多细节请看https://github.com/rafael-piovesan/go-rocket-ride)。

// Load loads database test fixtures specified by the "data" param, a list of files and/or directories (paths
// should be relative to the project's root dir).
//
// It takes as input a Postgres DSN, a list of files/directories leading to the *.yaml files and one more
// optional param, which accepts a map with data to be used while parsing files looking for template
// placeholders to be replaced.
func Load(dsn string, data []string, tplData map[string]interface{}) error {if len(data) == 0 {return errors.New("list of fixtures files/directories is empty")}db, err := sql.Open("postgres", dsn)if err != nil {return err}// find out the absolute path to this file// it'll be used to determine the project's root path_, callerPath, _, _ := runtime.Caller(0) // nolint:dogsled// look for migrations source starting from project's root dirrootPath := fmt.Sprintf("%s/../..",filepath.ToSlash(filepath.Dir(callerPath)),)// assemble a list of fixtures paths to be loadedfor i := range data {data[i] = fmt.Sprintf("%v/%v", rootPath, filepath.ToSlash(data[i]))}fixtures, err := testfixtures.New(testfixtures.Database(db),testfixtures.Dialect("postgres"),testfixtures.Template(),testfixtures.TemplateData(tplData),// Paths must come after Template() and TemplateData()testfixtures.Paths(data...),)if err != nil {return err}// load fixtures into DBreturn fixtures.Load()
}

将测试fixture加载到Postgres实例的实用程序代码

此外,当它与Testcontainers-Go以及其他迁移工具结合时,它会是一个很好的匹配,因为你可以在你的测试中以编程方式协调这些工具,就像下面这样。

- id: {{$.UserId}}email: {{$.UserEmail}}stripe_customer_id: zaZPe9XIf8Pq5NK

测试fixtures样本文件 https://gist.github.com/rafael-piovesan/2a06fca2805641d291c1f38323ef68f1#file-users-yaml

ctx := context.Background()// create database container
dsn, terminate, err := testcontainer.NewPostgresContainer()
require.NoError(t, err)
defer terminate(ctx)// migrations up
err = migrate.Up(dsn, "db/migrations")
require.NoError(t, err)// load test fixtures
userID := int64(gofakeit.Number(0, 1000))
idemKey := gofakeit.UUID()
err = testfixtures.Load(dsn, []string{"db/fixtures/users"}, map[string]interface{}{"UserId":    userID,"UserEmail": gofakeit.Email(),
})
require.NoError(t, err)

把所有东西放在一起 https://gist.githubusercontent.com/rafael-piovesan/2a06fca2805641d291c1f38323ef68f1/raw/2b481bb070ddfc6cb80d6172a0d057a36f7ca831/db_test.go

测试主函数

你有没有想过如何测试你的应用程序的主功能呢?假设您想检查代码在缺少任何环境变量的情况下的行为。假设你至少有一个简单的日志来告诉我们发生了什么,可能的方法是实际检查应用程序的输出并查找该日志。如下所示。(请参阅更多详细信息 https://github.com/rafael-piovesan/go-rocket-ride)

// main.go
package mainimport ("log"rocketride "github.com/rafael-piovesan/go-rocket-ride"
)func main() {// read config values from env varscfg, err := rocketride.LoadConfig(".")if err != nil {log.Fatalf("cannot load config: %v", err)}// continue to start your app ...
}// your tests
func TestMain(t *testing.T) {if os.Getenv("START_MAIN") == "1" {main()return}t.Run("Missing env vars", func(t *testing.T) {stdout, stderr, err := startSubprocess(t)if e, ok := err.(*exec.ExitError); ok && !e.Success() {assert.Empty(t, stdout)assert.Contains(t, stderr, "cannot load config")return}t.Fatalf("process ran with err %v, want exit status 1", err)})
}// startSubprocess calls "go test" command specifying the test target name "TestMain" and setting
// the env var "START_MAIN=1". It will cause the test to be run again, but this time calling the
// "main()" func. This way, it's possible to retrieve and inspect the app exit code along with
// the stdout and stderr as well.
// See more at: https://stackoverflow.com/a/33404435
func startSubprocess(t *testing.T, envs ...string) (stdout string, stderr string, err error) {var cout, cerr bytes.Buffer// call test suit again specifying "TestMain" as the targetcmd := exec.Command(os.Args[0], "-test.run=TestMain")// set "START_MAIN" env var along with any additional value provided as parameterenvs = append(envs, "START_MAIN=1")cmd.Env = append(os.Environ(), envs...)// capture subprocess' stdout and stderrcmd.Stdout = &coutcmd.Stderr = &cerr// run the test againerr = cmd.Run()stdout = cout.String()stderr = cerr.String()return
}

用Build标签分离测试

这个想法是按类型(即单元、集成、冒烟和端到端)或任何其他你认为合适的标准来分离测试,因为我们使用Go的内置机制,称为Build Contraints[3],也就是众所周知的Build Tags。关于构建约束最近在Go1.17的改动也可以看站长写的Go1.17 新特性:新版构建约束。

//go:build unit || usecase
// +build unit usecasepackage usecaseimport "testing"func TestUseCase(t *testing.T) {// your test code
}

然后,要选择你想运行的测试,只需调用go测试命令,指定-tags即可。

# 运行所有单元测试
go test -tags=unit ./...
# 只运行与用例相关的测试
go test -tags=usecase ./...

参考

  • https://github.com/rafael-piovesan/go-rocket-ride

  • https://go.dev/doc/code#Testing

  • https://bmuschko.com/blog/go-testing-frameworks/

  • https://github.com/orlangure/gnomock

  • https://github.com/ory/dockertest

  • https://github.com/testcontainers/testcontainers-go

  • https://github.com/stretchr/testify

  • https://github.com/vektra/mockery

  • https://github.com/brianvoe/gofakeit

  • https://github.com/h2non/gock

  • https://mickey.dev/posts/go-build-tags-testing/

参考资料

[1]

Go (Golang): Testing tools & tips to step up your game: https://blog.devgenius.io/go-golang-testing-tools-tips-to-step-up-your-game-4ed165a5b3b5

[2]

Nock: https://github.com/nock/nock

[3]

Build Contraints: https://pkg.go.dev/cmd/go#hdr-Build_constraints


本文转载自公众号「Go 招聘」欢迎点击下方名片关注。

推荐几个 Go 测试工具和技巧让你在虎年如虎添翼相关推荐

  1. 良心推荐8个安全测试工具,快来取走

    安全测试是很重要的东西!!可以提高信息系统中的数据安全性,未经批准的用户就无法访问.成功的安全测试可以保护Web应用程序免受严重的恶意软件和其他恶意威胁的侵害,这些侵害会导致Web应用程序崩溃或产生意 ...

  2. 让你如虎添翼的 Go 测试工具和技巧

    我是一只可爱的土拨鼠,专注于分享 Go 职场.招聘和求职,解 Gopher 之忧!欢迎关注我. 欢迎大家加入Go招聘交流群,来这里找志同道合的小伙伴!跟土拨鼠们一起交流学习. 土拨鼠今天带来一篇让你在 ...

  3. 推荐8个良心安全测试工具,快来取走

    安全测试是很重要的东西!!可以提高信息系统中的数据安全性,未经批准的用户就无法访问.成功的安全测试可以保护Web应用程序免受严重的恶意软件和其他恶意威胁的侵害,这些侵害会导致Web应用程序崩溃或产生意 ...

  4. 渗透测试 ( 5 ) --- 扫描之王 nmap、渗透测试工具实战技巧合集

    Nmap 官方文档 ( 中文文档是 Nmap 版本4.50,英文文档是最新的 ): 英文文档:https://nmap.org/book/man.html 中文文档:https://nmap.org/ ...

  5. centos安装stress安装失败_Linux压力测试工具Stress的使用指南

    为了测试Linux服务器的负载情况,这里给大家推荐一款压力测试工具:Stress,Stress是一款Posix系统下生成Cpu/Menory/IO/Disk负载的工具. Stress安装 在CentO ...

  6. 十大开源Web应用安全测试工具

    点击蓝字关注我们 Web应用安全测试可对Web应用程序执行功能测试,找到尽可能多的安全问题,大大降低黑客入侵几率. 在研究并推荐一些最佳的开源Web应用安全测试工具之前,让我们首先了解一下安全测试的定 ...

  7. stress 压力测试工具

    ############################stress 压力测试工具 ############################为了测试服务器的负载情况,给大家推荐Stress这个压力测试 ...

  8. mqtt协议调用示例(包括MQTT一键启动服务+测试工具 MQTTFX云盘下载),对捷顺门禁温感一体机进行人员信息下发

    hello, 大家好 我是一只不是在戏精,就是在戏精路上的极品二哈 新年上班第一天,给大家贡献一篇 MQTT 协议使用示例文章 也是本汪自己的一篇实用笔记 本汪先总的说下: MQTT协议进行数据交互, ...

  9. 前端开发浏览器兼容测试工具Lunascape

    对于前端开发人员来说,浏览器是最头疼的问题了,要考虑各种类型各种版本的兼容,实际情况下写的时间可能远少于你在浏览器中测试问题... 看了不少博客和网站推荐的浏览器兼容测试工具,根据使用过的情况介绍下其 ...

最新文章

  1. 学习笔记(十四)——MySQL(CRUD)
  2. Linux Ubuntu使用技巧
  3. Eurek Ribbon Feign常见问题及解决
  4. Lambda表达式和闭包Closure
  5. 面向对象-多态的实现
  6. Mybatis+MySQL动态分页查询数据经典案例(含代码以及测试)
  7. python把数字逐一存入列表_python实现将range()函数生成的数字存储在一个列表中...
  8. 推荐9个web前端模板框架
  9. Android中应用程序获得系统签名权限(platform.x509.pem platform.pk8)下载地址
  10. 利用qiskit实现量子门以及态的初始化
  11. 局域网计算机加密共享文件,怎么共享文件夹局域网(局域网共享加密)
  12. 会说话的汤姆猫游戏源码下载
  13. 常用的几个软电话客户端配置
  14. java重写方法的快捷键
  15. vim 匹配行首到某个特定字符
  16. 单片机读取多路温度c语言,以51单片机为内核实现了两路温度采集与显示的温度控制器设计...
  17. python 儿童 游戏_儿童编程教学 – 推荐几款Python编程类游戏
  18. 贝叶斯优化调参-Bayesian optimiazation原理加实践
  19. 鸟叔的linux私房菜+大数据(Hardoop/Spark/Hive) 电子书分享
  20. JVM原理之完整的一次GC流程

热门文章

  1. java调用FFmpeg及mencoder转换视频为FLV并截图
  2. Javascript中的form
  3. jQuery 对象与Dom 对象互转
  4. 简单介绍一下BSP中的dirs文件和sources文件(WinCE
  5. jquery autocomplete的使用
  6. 用反射方法使用户控件动态调用父页面的方法
  7. 极简风格的响应式简历模板
  8. css学习_css3过渡
  9. 关于游戏小说与学习知识的不同
  10. Android零基础入门第31节:几乎不用但要了解的AbsoluteLayout绝对布局