在产品开发中,如何使用Go进行测试和模拟的实例
我叫平山,是在DeNA做工程师的。我有大约1年的Go语言经验,主要在开发广告投放服务器方面进行应用。
在这篇文章中,我将讨论我之前如何编写Go的测试以及如何使用模拟。
Go的测试框架
在Go语言中,包含了一个名为testing的标准测试框架。这个标准的testing包只提供了最基本的功能。
参考:关于 Go 的测试思路
实际上,考试中的难题。
尽管我们追求简洁的政策如上所述,但在实际的开发中,我们还是发现仅仅依赖标准的testing包功能并不方便。
-
- Assertionがない
-
- テスティングヘルパーがほとんどない
テストスイートの仕組みがない
Setup/TearDownの機構がない
此外,在单元测试中,处理测试数据时候经常需要使用模拟对象等手段,然而,在标准的测试框架中却没有相应的功能,因此需要考虑如何处理。
问题解决的提示
当我想着如何应对上述问题时,我找到了以下的条目。
掌握Golang中的测试和模拟
这是英语,但内容并不复杂。简单来说,要点如下:
-
- CIとユニットテストについてのテストインフラ構築についての考察
-
- モックオブジェクト作成手法
-
- イケてるテスティングフレームワーク
- コードカバレッジについて
基本上,我们依照以上的条目逐步建立了测试基础架构。
前往检查包裹的gocheck包。
Go语言有好几个外部测试框架可供选择,但在之前的条目中推荐了一个名为 Gocheck 的库。
特征如下所述。
-
- 標準のテスティングフレームワークと互換
-
- テストスイートのサポート
テストを関数ではなくstructへのメソッドとして定義。3
fixtureのサポート
go test のオプションとしてのログ出力
選択的なテストの起動
在实际开发中选择使用gocheck,能够在遵循标准测试框架的同时,整理并编写庞大且容易混乱的测试代码。
实际的文件分割
在每个包中,放置名为 all_test.go 的文件,并在其中指定每个测试套件的结构体,以便简化管理测试套件。
package xxx_test
import (
"testing"
. "gopkg.in/check.v1"
)
func TestPackage(t *testing.T) {
TestingT(t)
}
var _ = Suite(&LogicSuite{})
var _ = Suite(&CacheSuite{})
package xxx_test
import (
. "gopkg.in/check.v1"
)
type LogicSuite struct{}
func (s *LogicSuite) SetUpSuite(c *C) {
// テストスイート全体での初期化処理を記述する
}
func (s *LogicSuite) SetUpTest(c *C) {
// 個々のテストごとに実行される初期化時の共通処理を記述する
}
func (s *LogicSuite) TearDownSuite(c *C) {
// テストスイート全体での終了後処理を記述する
}
func (s *LogicSuite) TearDownTest(c *C) {
// 個々のテストごとに実行される終了後の共通処理を記述する
}
func (s *LogicSuite) TestSomeProcess(c *C) {
// 実際に実施されるテストを記述
}
利用模拟对象
前出のエントリでは、いくつかのパターンにおけるモックオブジェクトの作成手法について触れられています。
-
- パーシャル(部分的)モック
gomockを利用した自動モック作成
withmockを利用した外部もしくは標準ライブラリに関するモック作成
特にgomockを利用したモックオブジェクト作成については、以下のエントリが詳しいです:
使用Go Mock生成接口的模拟并进行测试 #golang
在创建模拟对象时的挑战
インタフェースを定義しておくことで、上記の通り自動的にモックオブジェクトを作成することは容易です。しかし実際には、モックオブジェクトの挙動を定義するのに EXPECT() でデータを返すよう記述する必要があったりするので、単純なテストデータを返して欲しいだけの場合などは、割に合わず使いづらい場合があります。
また、モックオブジェクトの場合には、テストによってエラーを返したり、タイムアウトをさせたかったり、また書き込まれたデータを後から参照したい場合など、柔軟に挙動を定義したい場合もあります。そのような場合は自動モックオブジェクト作成では対応できません。
手动的模拟对象
実際の開発ではDI的な手法で、モックオブジェクトを手動で作成しています。
例として、memcacheなどのストレージに非同期書き込みする例を示します。
package mycache
// モック化対象のオブジェクト。ここではmemcachedなどに対しキャッシュのGet/Setを取り扱う
type CacheHandler interface {
Get(string) (int, error)
Set(string, int) error
}
// 現在有効なCacheHandlerオブジェクト
var cacheHandler = &defaultCacheHandler{}
// CacheHandlerオブジェクトを登録する。受け取るのはインターフェース。
func RegisterCacheHandler(c CacheHandler) {
cacheHandler = c
}
// デフォルトのCacheHandlerオブジェクトに戻す
func ClearCacheHandler() {
cacheHandler = &defaultCacheHandler{}
}
// 現在有効なCacheHandlerオブジェクトを返す。アプリケーションは必ずこれを呼び出すようにする
func CurrentCacheHandler() CacheHandler {
return cacheHandler
}
// 以下、デフォルトのCacheHandlerオブジェクトの実装
type defaultCacheHandler struct{}
func (*defaultCacheHandler) Get(key string) (val int, err error) {
// memcachedなりから実際にGetする
}
func (*defaultCacheHandler) Set(key string, val int) (err error) {
// memcachedなりに実際にSetする
}
下面的代码执行了对于memcached等存储的读写操作。
该实现位于defaultCacheHandler中。
CacheHandler というインターフェースでキャッシュの実装を抽象化しています。そして、 RegisterCacheHandler 、ClearCacheHandler でそれぞれ同様のインターフェースの登録、再初期化と、CurrentCacheHandler で現在のハンドラを返すようにしています。
package mycache
import (
"time"
)
// memcachedのフリして保存される先のハッシュ
var _data = make[string]int
type CacheHandlerMock struct {
DataWriteDuration time.Duration // 擬似書き込み遅延時間
DataWriteChan chan int // データ書き込みが成功なら1を書き込む
}
func (m *CacheHandlerMock) Get(key string) (val int, err error) {
return _data[key], nil
}
// 遅延しつつデータ書き込みする
func (m *CacheHandlerMock) Set(key string, val int) (err error) {
time.Sleep(m.DataWriteDuration)
_data[key] = val
if m.DataWriteChan != nil {
m.DataWriteChan <- 1 // 終了通知
}
}
// テスト時にデータの中身をいつでも参照できるように
func DumpCacheHandlerMock() map[string]int {
return _data
}
// モックデータの初期化。該当するテストのSetUpTest()などで呼び出す
func ResetCacheHandlerMock() {
_data[key] = make[string]int
}
上記がテストに使われるモックオブジェクトです。与えられたキーと値は通常のハッシュオブジェクトに格納しています。
合わせて、書き込みが完了したら DataWriteChan フィールドに1を送信するようにしています。これにより、通常のコードはシンプルに保ったまま、テストの方ではデータ書き込みがあったら検出できるようにしています。
また、DumpCacheHandlerMock() で格納されたハッシュをそのまま読み出せるようにすることで、テスト側でデータの検証をしやすくしたり、 ResetCacheHandlerMock() でデータを容易に初期化できるようにしています。
以降はCacheHandlerを通じてキャッシュを実装する関数の例です:
package mycache
import(
"fmt"
)
func AsyncSet(key string, input int) (err error) {
var h = CurrentCacheHandler()
// 非同期書き込みされる例
go func() {
time.Sleep(10 * time.Millisecond) // 他にいろんな処理があって書き込みに時間がかかるとする…
h.Set(key, input)
}
}
サンプルとして非同期書き込みを行う例として AsyncSet という関数を実装しています。この関数は、goroutineでいくらか重い処理を行いつつ、キャッシュに引数で与えられたキーと値をセットするものです。
CurrentCacheHandler() を通じてハンドラオブジェクトを取得し、それを通じて値をセットするようになっています。
如此一来,实现方面的代码将变得简洁明了。
package mycache_test
import (
"mycache"
"testing"
"time"
. "gopkg.in/check.v1"
)
func Test(t *testing.T) { TestingT(t) }
type CacheSuite struct{
cacheHandlerMock *mycache.CacheHandlerMock
}
var _ = Suite(&CacheSuite{})
func (s *CacheSuite) SetUpSuite(c *C) {
// モックアップを作成してRegister
s.cacheHandlerMock = &mycache.CacheHandlerMock{
DataWriteDuration: 10 * time.Millisecond,
DataWriteChan: make(chan int),
}
mycache.RegisterCacheHandler(cacheHandlerMock)
}
func (s *CacheSuite) SetUpTest(c *C) {
// モックのデータを初期化
mycache.ResetCacheHandlerMock()
}
func (s *CacheSuite) TestAsyncSet(c *C) {
// 正常
AsyncSet("foo", 10)
var timeout = time.After(50 * time.Millisecond) // ちょっと長めに待つ
select {
case <-timeout:
c.Fatal("action is not completed.")
case <- s.cacheHandlerMock.DataWriteChan // 終わったら知らせてもらえるはず
}
var mockdata = DumpCacheHandlerMock()
c.Assert(mockdata["foo"], Equals, 10)
}
以下是用于测试的代码。在SetUpSuite()函数中构建了模拟对象,并将其注册为要使用的缓存处理程序。
TestAsyncSet で前出の AsyncSet を呼び出して実際にキャッシュされるかテストしています。モックオブジェクトに書き込み完了のチャンネルを持たせて、それを待つようにしたことで、書き込み完了を待って値の検証を行えるようにしています。
このように、インターフェースを活用してモックオブジェクトを作成することで、基本部分のコードを簡潔に保ちつつ、テストしたい対象のロジックをテストすることができます。
总结
通过使用 gocheck,可以在保持简单性的同时,扩展 Go 的测试框架。
また、テストで必要となるモックオブジェクトの構築方法について、ライブラリを使う方法と、自前でモックオブジェクトを構築して活用する方法について触れました。
如果以上能对实际开发有所帮助,我将感到幸福!
不是基于TDD,而是通过传统的开发→单元测试循环进行开发。
尽管这个条目很旧。
通常情况下,Go语言不会细分包,但是使用标准的测试框架,由于函数定义为简单函数,很容易遇到函数名冲突的问题。