5种先进的Go测试技术

向大家致敬! 在“ Golang开发者”课程开始前不到一周的时间,我们将继续分享有关该主题的有用材料。 走吧



Go有一个良好且可靠的内置库用于测试。 如果您在Go上编写,那么您已经知道了。 在本文中,我们将讨论几种可以提高Go测试技巧的策略。 从在Go上编写令人印象深刻的代码的经验中,我们了解到这些策略确实有效,因此有助于节省使用代码的时间和精力。

使用测试套件

如果您仅从本文中学习到一件事,那么一定是使用测试套件。 对于不熟悉此概念的人来说,集合测试是开发测试以测试可在该接口的许多实现上使用的通用接口的过程。 在下面,您可以看到我们如何通过几种不同的Thinger实现,并使用相同的测试运行它们。

 type Thinger interface { DoThing(input string) (Result, error) } // Suite tests all the functionality that Thingers should implement func Suite(t *testing.T, impl Thinger) { res, _ := impl.DoThing("thing") if res != expected { t.Fail("unexpected result") } } // TestOne tests the first implementation of Thinger func TestOne(t *testing.T) { one := one.NewOne() Suite(t, one) } // TestOne tests another implementation of Thinger func TestTwo(t *testing.T) { two := two.NewTwo() Suite(t, two) } 

幸运的读者已经使用这种方法来处理代码库。 通常在基于插件的系统测试中使用,这些测试是为测试接口而编写的,该接口的所有实现都可以使用该测试来了解它们如何满足行为要求。

使用此方法将潜在地节省数小时,数天,甚至足够的时间来解决类P和NP相等的问题。 同样,当用一个基本系统替换另一个基本系统时,也不再需要编写(大量)其他测试,并且也有信心这种方法不会破坏应用程序的运行。 隐式地,您需要创建一个定义测试区域区域的接口。 使用依赖项注入,您可以从传递给整个包的实现的包中定制集合。

您可以在此处找到完整的示例。 尽管该示例牵强附会,但可以想象一个数据库是远程数据库,另一个数据库在内存中。

这项技术的另一个很棒的示例位于golang.org/x/net/nettest软件包的标准库中。 它提供了一种验证net.Conn是否满足接口的方法。

避免界面污染

您不能谈论在Go中进行测试,但不能谈论界面。

接口在测试环境中很重要,因为它们是我们测试库中功能最强大的工具,因此您需要正确使用它们。

程序包通常将接口导出给开发人员,这导致以下事实:

A)开发人员创建自己的模拟程序以实现程序包;
B)包导出自己的模拟。

“接口越大,抽象度越弱”
-Rob Pike,《 Going语录》

导出之前,必须仔细检查接口。 通常很容易导出接口,以使用户能够模拟他们所需的行为。 相反,文档的接口适合您的结构,以免在使用者程序包和您自己的程序包之间建立紧密的关系。 错误包就是一个很好的例子。

当我们有不想导出的接口时,可以使用内部/包子树将其保存在包中。 因此,我们不必担心最终用户可能会依赖他,因此,可以根据新要求灵活地更改界面。 通常,我们创建具有外部依赖关系的接口,以便能够在本地运行测试。

这种方法允许用户通过简单地包装库的一部分进行测试来实现自己的小型接口。 有关此概念的更多信息,请阅读有关界面污染rakyl文章

不导出并发原语

Go提供了易于使用的并发原语,由于同样的简单性,有时还会导致它们的过度使用。 首先,我们关注通道和同步包。 有时,很想从软件包中导出频道,以便其他人可以使用它。 另外,一个常见的错误是嵌入sync.Mutex而不将其设置为私有。 和往常一样,这并不总是很糟糕,但是在测试程序时会产生某些问题。

如果您导出频道,则还会使软件包用户的生活变得复杂,这是不值得的。 从软件包中导出频道后,在测试使用此频道的用户时会遇到困难。 为了成功进行测试,用户必须知道:

  • 当数据最终通过通道发送时。
  • 接收数据时有任何错误。
  • 数据包完成后如何刷新通道(如果完全刷新)?
  • 如何包装软件包API,以免直接调用它?

看一下队列读取示例。 这是一个示例库,它从队列中读取数据,并为用户提供供读取的供稿。

 type Reader struct {...} func (r *Reader) ReadChan() <-chan Msg {...} 

现在,您的图书馆用户想要为其用户实施测试:

 func TestConsumer(t testing.T) { cons := &Consumer{ r: libqueue.NewReader(), } for msg := range cons.r.ReadChan() { // Test thing. } } 


然后,用户可以确定依赖注入是一个好主意,并在通道中编写自己的消息:

 func TestConsumer(t testing.T, q queueIface) { cons := &Consumer{ r: q, } for msg := range cons.r.ReadChan() { // Test thing. } } 


等等,这些错误呢?

 func TestConsumer(t testing.T, q queueIface) { cons := &Consumer{ r: q, } for { select { case msg := <-cons.r.ReadChan(): // Test thing. case err := <-cons.r.ErrChan(): // What caused this again? } } } 


现在,我们需要以某种方式生成事件,以便实际写入此存根,该存根复制了我们正在使用的库的行为。 如果该库只是编写了同步API,那么我们可以将所有并行性添加到客户端代码中,因此测试变得更加容易。

 func TestConsumer(t testing.T, q queueIface) { cons := &Consumer{ r: q, } msg, err := cons.r.ReadMsg() // handle err, test thing } 


如果您有疑问,请记住,将并行性添加到使用者程序包(使用者程序包)总是很容易,并且从库中导出后很难或不可能删除它。 最重要的是,不要忘记在软件包文档中写出结构/软件包对于同时访问多个goroutine是否安全。
有时仍然需要或必须导出通道。 为了减轻上面提到的一些问题,您可以通过访问器提供通道,而不是直接访问,并在声明时仅为读取( ←chan )或仅用于写入( chan← )打开通道。

使用net/http/httptest

Httptest允许Httptest执行http.Handler代码,而无需启动服务器或绑定到端口。 这样可以加快测试速度,并允许您以较低的成本并行运行测试。

这是以两种方式实施的同一测试的示例。 这里没有什么宏伟的,但是这种方法减少了代码量并节省了资源。

 func TestServe(t *testing.T) { // The method to use if you want to practice typing s := &http.Server{ Handler: http.HandlerFunc(ServeHTTP), } // Pick port automatically for parallel tests and to avoid conflicts l, err := net.Listen("tcp", ":0") if err != nil { t.Fatal(err) } defer l.Close() go s.Serve(l) res, err := http.Get("http://" + l.Addr().String() + "/?sloths=arecool") if err != nil { log.Fatal(err) } greeting, err := ioutil.ReadAll(res.Body) res.Body.Close() if err != nil { log.Fatal(err) } fmt.Println(string(greeting)) } func TestServeMemory(t *testing.T) { // Less verbose and more flexible way req := httptest.NewRequest("GET", "http://example.com/?sloths=arecool", nil) w := httptest.NewRecorder() ServeHTTP(w, req) greeting, err := ioutil.ReadAll(w.Body) if err != nil { log.Fatal(err) } fmt.Println(string(greeting)) } 

也许最重要的功能是使用httptest只能将测试拆分为要测试的功能。 设置服务器,服务,处理器工厂,处理器工厂或您认为是好主意的任何其他东西时,不会出现路由器,中间件或任何其他副作用。

要了解这一原则的实际作用,请查看Marc Berger的文章

使用单独的软件包_test

生态系统中的大多数测试都是在pkg_test.go文件中创建的,但仍保留在同一包中: package pkg 。 单独的测试包是您在要测试的模块foo/中的新文件foo_test.go创建的包,带有声明package foo_test 。 从这里可以导入github.com/example/foo和其他依赖项。 此功能使您可以做很多事情。 这是针对测试中的循环依赖项的推荐解决方案,它可以防止出现“脆弱测试”,并使开发人员可以感觉到使用自己的程序包的感觉。 如果您的包装难以使用,则使用此方法进行测试也将很困难。

该策略通过限制对私有变量的访问来防止脆弱的测试。 特别是,如果您的测试失败,并且您使用单独的测试包,则几乎可以保证使用在测试中中断的功能的客户端在被调用时也会中断。

最后,它有助于避免测试中的导入周期。 除了测试包之外,大多数包都更有可能依赖于您编写的其他包,因此最终将导致导入周期自然发生。 外部软件包位于软件包层次结构中的两个软件包上方。 以Go编程语言(第11章,第2.4节)中的示例为例,其中net/url实现了URL解析器, net/http导入使用。 但是,需要通过导入net / http来对net / url进行实际用例测试。 这样net/url_test

现在,当您使用单独的测试程序包时,可能需要访问程序包中以前可用的未导出实体。 一些开发人员在基于时间(例如time.Now)使用功能测试时,第一次遇到这个问题。 在这种情况下,我们可以使用其他文件来专门在测试期间提供实体,因为_test.go文件不包含在常规版本中。

您需要记住什么?

重要的是要记住,上述方法都不是万能药。 任何企业中最好的方法是批判性地分析情况并独立选择解决问题的最佳方法。

想更多地了解使用Go进行测试?
阅读这些文章:

Dave Cheney的书写表驱动的Go测试
有关测试的Go编程语言章节。
或观看以下视频:
Gophercon 2017的Hashimoto的Go先进测试演讲
2014年安德鲁·格朗(Andrew Gerrand)的“测试技术”演讲

我们希望此翻译对您有所帮助。 我们正在等待评论,每个想进一步了解该课程的人都欢迎您参加将于5月23日举行的开放日

Source: https://habr.com/ru/post/zh-CN452772/


All Articles