測試是軟體工程中不可或缺的一環,而同時也是 Go 原生支援的特性:只要建立 _test.go 尾綴的檔案,go test 就可以直接使用。
resp.(SimpleString) 是一個很好的起點:它足夠簡單,但也能延伸得足夠複雜。
// file: ./pkg/resp/value_test.go
func TestSimpleString_Marshal(t *testing.T) {
testcases := []struct {
data []byte
expect []byte
}{
{
data: []byte("OK"),
expect: []byte("+OK\r\n"),
},
{
data: []byte("PONG"),
expect: []byte("+PONG\r\n"),
},
}
for _, tc := range testcases {
t.Run(string(tc.data), func(t *testing.T) {
s := SimpleString(tc.data)
marshaled := s.Marshal()
if string(marshaled) != string(tc.expect) {
t.Errorf("expect '%s', got '%s'", tc.expect, marshaled)
}
})
}
}
當 *_test.go 檔案中的存在以 Test 開頭的函式,代表它是一個測試。我個人習慣會將測試函式分成兩個部份:
一個測試中,可以包括一至多組測試案例,以上面的測試為例就存在兩組測資。
testcases := []struct {
data []byte
expect []byte
}{
{
data: []byte("OK"),
expect: []byte("+OK\r\n"),
},
{
data: []byte("PONG"),
expect: []byte("+PONG\r\n"),
},
}
如果存在多組測資,則可以使用 t.Run() 派生子測試執行器,針對每一組測資都獨立進行測試:
for _, tc := range testcases {
t.Run(string(tc.data), func(t *testing.T) {
// ...
})
}
在單元測試領域有一項著名的「3A 原則」,它將測試分為三個階段:
以下述的程式碼為例:
for _, tc := range testcases {
t.Run(string(tc.data), func(t *testing.T) {
// 安排:從 tc.data 建立 SimpleString
s := SimpleString(tc.data)
// 執行:實際執行 s.Marshal()
marshaled := s.Marshal()
// 斷言:判斷 marshaled 是否等於 tc.expect
if string(marshaled) != string(tc.expect) {
t.Errorf("expect '%s', got '%s'", tc.expect, marshaled)
}
})
}
單元測試的目的在於驗證程式是否正確,而 Benchmark Testing(基準測試) 的目的則是判定程式的效能是否符合預期。
以下為 resp.(SimpleString) 實作兩種不同的序列化器:
// file: ./pkg/resp/value.go
func (v SimpleString) bufferMarshal() []byte {
var buf bytes.Buffer
buf.WriteRune(MAGIC_SIMPLE_STRING)
buf.Write(v)
buf.WriteString(SENTINEL)
return buf.Bytes()
}
func (s SimpleString) copyMarshaler() []byte {
l := len(s)
marshaled := make([]byte, l+3)
marshaled[0] = MAGIC_SIMPLE_STRING
marshaled[l+1] = '\r'
marshaled[l+2] = '\n'
copy(marshaled[1:], s)
return marshaled
}
註:過早進行效能優化並非是好的實踐,這個案例僅是作為學習基準測試。對於其它的
resp.Value實作我不會特別去設計或改動不同的序列化實作。
要撰寫基準測試,只要建立前綴為 Benchmark 的函式即可:
// file: ./pkg/resp/value_test.go
func BenchmarkSimpleString_bufferMarshal(b *testing.B) {
s := SimpleString("OK")
b.StartTimer()
for b.Loop() {
_ = s.bufferMarshal()
}
b.StopTimer()
}
func BenchmarkSimpleString_copyMarshal(b *testing.B) {
s := SimpleString("OK")
b.StartTimer()
for b.Loop() {
_ = s.copyMarshaler()
}
b.StopTimer()
}
使用 go test ./pkg/resp/value_test.go 就可以指定檔案中的測試,但是基準測試預設是會被略過的。如果要啟用基準測試,則需要使用 go test -bench=. ./pkg/resp/value_test.go。
有些開發者會在專案根目錄下使用 go test ./... 及 go test -bench=. ./...,這可以執行整個專案底下的所有測試。
我個人習慣於把它寫在 Makefile 裡:
test:
go test ./...