- 原文地址:A History of Testing in Go at SmartyStreets
- 原文做者:Michael Whatcott
- 译文出自:掘金翻译计划
- 本文永久连接:github.com/xitu/gold-m…
- 译者:kasheemlew
- 校对者:StellaBauhinia
最近常有人问我这两个有趣的问题:html
这两个问题很好,做为 GoConvey 的联合创始人兼 gunit 的主要做者,我也有责任将这两个问题解释清楚。直接回答,太长不读系列:前端
问题 1:为何换用 gunit?android
在使用 GoConvey 的过程当中,有一些问题一直困扰着咱们,因此咱们想了一个更能体现测试库中重点的替代方案,以解决这些问题。在当时的状况中,咱们已经没法对 GoConvey 作过渡升级方案了。下面我会更仔细介绍一下,并提炼到简明的宣明式结论。ios
问题 2:你是否建议你们都这么作(从 GoConvey 换成 gunit)?git
不。我只建议大家使用能帮助大家达成目标的工具和库。你得先明确本身对测试工具的需求,而后再尽快去找或者造适合本身的工具。测试工具是大家构建项目的基础。若是你对后面的内容产生了共鸣,那么 gunit 会成为你选型中一个极具吸引力的选项。你得好好研究,而后慎重选择。GoConvey 的社区还在不断成长,而且拥有不少活跃的维护者。若是你很想支持一下这个项目,随时欢迎加入咱们。github
咱们初次使用 Go 大概是在 Go 1.1 发布的时候(也就是 2013 年年中),在刚开始写代码的时候,咱们很天然地接触到了 go test
和 "testing"
包。我很高兴看到 testing 包被收进了标准库甚至是工具集中,可是对于它惯用的方法并无什么感受。后文中,咱们将使用著名的“保龄球游戏”练习对比展现咱们使用不一样测试工具后获得的效果。(你能够花点时间熟悉一下生产代码,以便更好地了解后面的测试部分。)golang
下面是用标准库中的 "testing"
包编写保龄球游戏测试的一些方法:后端
import "testing"
// Helpers:
func (this *Game) rollMany(times, pins int) {
for x := 0; x < times; x++ {
this.Roll(pins)
}
}
func (this *Game) rollSpare() {
this.rollMany(2, 5)
}
func (this *Game) rollStrike() {
this.Roll(10)
}
// Tests:
func TestGutterBalls(t *testing.T) {
t.Log("Rolling all gutter balls... (expected score: 0)")
game := NewGame()
game.rollMany(20, 0)
if score := game.Score(); score != 0 {
t.Errorf("Expected score of 0, but it was %d instead.", score)
}
}
func TestOnePinOnEveryThrow(t *testing.T) {
t.Log("Each throw knocks down one pin... (expected score: 20)")
game := NewGame()
game.rollMany(20, 1)
if score := game.Score(); score != 20 {
t.Errorf("Expected score of 20, but it was %d instead.", score)
}
}
func TestSingleSpare(t *testing.T) {
t.Log("Rolling a spare, then a 3, then all gutters... (expected score: 16)")
game := NewGame()
game.rollSpare()
game.Roll(3)
game.rollMany(17, 0)
if score := game.Score(); score != 16 {
t.Errorf("Expected score of 16, but it was %d instead.", score)
}
}
func TestSingleStrike(t *testing.T) {
t.Log("Rolling a strike, then 3, then 7, then all gutters... (expected score: 24)")
game := NewGame()
game.rollStrike()
game.Roll(3)
game.Roll(4)
game.rollMany(16, 0)
if score := game.Score(); score != 24 {
t.Errorf("Expected score of 24, but it was %d instead.", score)
}
}
func TestPerfectGame(t *testing.T) {
t.Log("Rolling all strikes... (expected score: 300)")
game := NewGame()
game.rollMany(21, 10)
if score := game.Score(); score != 300 {
t.Errorf("Expected score of 300, but it was %d instead.", score)
}
}
复制代码
对于以前使用过 xUnit 的人,下面两点会让你很难受:浏览器
Setup
函数/方法可使用,全部游戏中须要不断重复建立 game 结构。<
、>
、<=
和 >=
)的时候,这些否认断言会更加恼人。因此,咱们调研如何测试,深刻了解为何 Go 社区放弃了“咱们最爱的测试帮手”和“断言方法”的观点,转而使用“表格驱动”测试来减小模板代码。用表格驱动测试从新写一遍上面的例子:bash
import "testing"
func TestTableDrivenBowlingGame(t *testing.T) {
for _, test := range []struct {
name string
score int
rolls []int
}{
{"Gutter Balls", 0, []int{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}},
{"All Ones", 20, []int{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}},
{"A Single Spare", 16, []int{5, 5, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}},
{"A Single Strike", 24, []int{10, 3, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}},
{"The Perfect Game", 300, []int{10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10}},
} {
game := NewGame()
for _, roll := range test.rolls {
game.Roll(roll)
}
if score := game.Score(); score != test.score {
t.Errorf("FAIL: '%s' Got: [%d] Want: [%d]", test.name, score, test.score)
}
}
}
复制代码
不错,这和以前的代码彻底不同。
优势:
skip bool
来跳过一些测试缺点:
如今,咱们不能仅仅知足于开箱即用的 go test
,因而咱们开始使用 Go 提供的工具和库来实现咱们本身的测试方法。若是你仔细看过 SmartyStreets GitHub page,你会注意到一个比较有名的仓库 — GoConvey。它是咱们对 Go OSS社区贡献的最先的项目之一。
GoConvey 能够说是一个左右开弓的测试工具。首先,有一个测试运行器监控你的代码,在有变化的时候执行 go test
,并将结果渲染成炫酷的网页,而后用浏览器展现出来。其次,它提供了一个库让你能够在标准的 go test
函数中写行为驱动开发风格的测试。还有一个好消息:你能够自由选择不使用、部分使用或者所有使用 GoConvey 中的这些功能。
有两个缘由促使咱们开发了 GoConvey:从新开发一个咱们原本打算在 JetBrains IDEs 中完成的测试运行器(咱们当时用的是 ReSharper)以及创造一套咱们很喜欢的像 nUnit 和 Machine.Specifications(在开始使用 Go 以前咱们是 .Net 商店)那样的测试组合和断言。
下面是用 GoConvey 重写上面测试的效果:
import (
"testing"
. "github.com/smartystreets/goconvey/convey"
)
func TestBowlingGameScoring(t *testing.T) {
Convey("Given a fresh score card", t, func() {
game := NewGame()
Convey("When all gutter balls are thrown", func() {
game.rollMany(20, 0)
Convey("The score should be zero", func() {
So(game.Score(), ShouldEqual, 0)
})
})
Convey("When all throws knock down only one pin", func() {
game.rollMany(20, 1)
Convey("The score should be 20", func() {
So(game.Score(), ShouldEqual, 20)
})
})
Convey("When a spare is thrown", func() {
game.rollSpare()
game.Roll(3)
game.rollMany(17, 0)
Convey("The score should include a spare bonus.", func() {
So(game.Score(), ShouldEqual, 16)
})
})
Convey("When a strike is thrown", func() {
game.rollStrike()
game.Roll(3)
game.Roll(4)
game.rollMany(16, 0)
Convey("The score should include a strike bonus.", func() {
So(game.Score(), ShouldEqual, 24)
})
})
Convey("When all strikes are thrown", func() {
game.rollMany(21, 10)
Convey("The score should be 300.", func() {
So(game.Score(), ShouldEqual, 300)
})
})
})
}
复制代码
和表格驱动的方法同样,整个测试都包含在一个函数中。又像在原来的例子中同样,咱们经过一个辅助函数进行重复的 rolls/throw。不一样于其余的例子,咱们如今已经拥有了一个巧妙的、不繁琐的、基于做用域的执行模型。全部的测试共享了 game
变量,但 GoConvey 的奇妙之处在于每一个外层做用域都针对每一个内层做用域执行。因此,每个测试之间又相对隔离。显然,若是不注意初始化和做用域的话,你很容易就会陷入麻烦。
另外,当你将对 Convey 的调用加入到循环中时(例如尝试将 GoConvey 和表格驱动测试组合起来使用),可能会发生一些诡异的事情。*testing.T
彻底由顶层的 Convey
调用管理(你注意到它和其余的 Convey
稍有不一样了吗?),所以你也没必要在全部须要断言的地方都传递这个参数。可是若是用 GoConvey 写过任何稍微复杂点的测试的话,你就会发现取出辅助函数的过程至关复杂。在我决定绕过这个问题以前,我建了一个 固定结构
来存放全部测试的状态,而后在这个结构里建立 Convey
的回调会用到的函数。因此一会是 Convey 的块和做用域,一会又是固定结构和它的方法,这看起来就很奇怪了。
因此,尽管咱们花了点时间,但最终仍是意识到咱们只是想要一个 Go 版本的 xUint,它须要摒弃奇怪的点导入和下划线包等级注册变量(看看你的 GoCheck)。咱们仍是很喜欢 GoConvey 中的断言,因而从原来的项目中分裂出了一个独立的仓库,gunit 就这样诞生了:
import (
"testing"
"github.com/smartystreets/assertions/should"
"github.com/smartystreets/gunit"
)
func TestBowlingGameScoringFixture(t *testing.T) {
gunit.Run(new(BowlingGameScoringFixture), t)
}
type BowlingGameScoringFixture struct {
*gunit.Fixture
game *Game
}
func (this *BowlingGameScoringFixture) Setup() {
this.game = NewGame()
}
func (this *BowlingGameScoringFixture) TestAfterAllGutterBallsTheScoreShouldBeZero() {
this.rollMany(20, 0)
this.So(this.game.Score(), should.Equal, 0)
}
func (this *BowlingGameScoringFixture) TestAfterAllOnesTheScoreShouldBeTwenty() {
this.rollMany(20, 1)
this.So(this.game.Score(), should.Equal, 20)
}
func (this *BowlingGameScoringFixture) TestSpareReceivesSingleRollBonus() {
this.rollSpare()
this.game.Roll(4)
this.game.Roll(3)
this.rollMany(16, 0)
this.So(this.game.Score(), should.Equal, 21)
}
func (this *BowlingGameScoringFixture) TestStrikeReceivesDoubleRollBonus() {
this.rollStrike()
this.game.Roll(4)
this.game.Roll(3)
this.rollMany(16, 0)
this.So(this.game.Score(), should.Equal, 24)
}
func (this *BowlingGameScoringFixture) TestPerfectGame() {
this.rollMany(12, 10)
this.So(this.game.Score(), should.Equal, 300)
}
func (this *BowlingGameScoringFixture) rollMany(times, pins int) {
for x := 0; x < times; x++ {
this.game.Roll(pins)
}
}
func (this *BowlingGameScoringFixture) rollSpare() {
this.game.Roll(5)
this.game.Roll(5)
}
func (this *BowlingGameScoringFixture) rollStrike() {
this.game.Roll(10)
}
复制代码
能够看到,去除辅助方法的过程很繁琐,这是由于咱们是在操做结构级的状态,而不是函数的局部变量的状态。此外,xUnit 中配置/测试/清除的执行模型比 GoConvey 中的做用域执行模型好懂多了。这里,*testing.T
如今由嵌入的 *gunit.Fixture
管理。这种方式对于简单的和基于交互的复杂测试来讲一样直观好懂。
gunit 和 GoConvey 的另外一个巨大区别是,按照 xUnit 的测试模式,GoConvey 使用共享的固定结构而 gunit 使用全新的固定结构。这两种方法都有道理,主要仍是看你的应用场景。全新的固定结构一般在单元测试中更能让人满意,而共享的固定结构在一些配置消耗比较大的状况下更有利,例如集成测试或系统测试。
全新的固定结构更能保证分开的测试项之间是相互独立的,所以 gunit 默认使用 t.Parallel()
。一样的,由于咱们只用反射调用子测试,因此也可使用 -run
参数挑选特定的测试项执行:
$ go test -v -run 'BowlingGameScoringFixture/TestPerfectGame'
=== RUN TestBowlingGameScoringFixture
=== PAUSE TestBowlingGameScoringFixture
=== CONT TestBowlingGameScoringFixture
=== RUN TestBowlingGameScoringFixture/TestPerfectGame
=== PAUSE TestBowlingGameScoringFixture/TestPerfectGame
=== CONT TestBowlingGameScoringFixture/TestPerfectGame
--- PASS: TestBowlingGameScoringFixture (0.00s)
--- PASS: TestBowlingGameScoringFixture/TestPerfectGame (0.00s)
PASS
ok github.com/smartystreets/gunit/advanced_examples 0.007s
复制代码
但不能否认,一些以前的样本代码仍然存在(好比文件头部的一些代码)。咱们在 GoLand 中安装了下面的实时模板,这些会自动生成前面大部分的内容。下面是在 GoLand 中安装实时模板的命令:
编辑器/实时模板
中选中 Go
列表,而后点击 +
号并选择“实时模板”fixture
)模板文本
区域:func Test$NAME$(t *testing.T) {
gunit.Run(new($NAME$), t)
}
type $NAME$ struct {
*gunit.Fixture
}
func (this *$NAME$) Setup() {
}
func (this *$NAME$) Test$END$() {
}
复制代码
定义
。Go
前面打个勾而后点OK
。如今咱们只用打开一个测试文件,输入 fixture
而后用 tab 自动补全测试模板就好了。
让我效仿敏捷软件开发宣言的风格来作个总结:
咱们不断实践、帮助他人,最终发现了更好的方法来进行软件测试。这让咱们实现了不少有价值的东西:
- 在共享的固定结构的基础上实现了全新的固定结构
- 用巧妙的做用域语义实现了简单的执行模型
- 用局部函数(或者说包级的)变量做用域实现了结构级做用域
- 经过倒置的检查和手动建立的错误信息实现了直接的断言函数
也就是说,虽然其余的测试库也很不错(这是一方面),咱们更喜欢 gunit(这是另外一方面)。
若是发现译文存在错误或其余须要改进的地方,欢迎到 掘金翻译计划 对译文进行修改并 PR,也可得到相应奖励积分。文章开头的 本文永久连接 即为本文在 GitHub 上的 MarkDown 连接。
掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 Android、iOS、前端、后端、区块链、产品、设计、人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划、官方微博、知乎专栏。