- 原文地址: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、前端、後端、區塊鏈、產品、設計、人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。