iT邦幫忙

2026 iThome 鐵人賽

DAY 14
0
Software Development

從零開始打造 Redis:以 Go 建立 Production Ready 應用程式 系列 第 16

[Day 16] 測試(上):單元測試與基準測試

  • 分享至 

  • xImage
  •  

測試是軟體工程中不可或缺的一環,而同時也是 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 原則

在單元測試領域有一項著名的「3A 原則」,它將測試分為三個階段:

  • Arrang 安排:初始化需要被測試的資源
  • Act 執行:執行要被測試的相應操作
  • Assert 斷言:判斷操作後的情況是否與預期相符

以下述的程式碼為例:

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 ./...

上一篇
[Day 15] 資料持久化(下):從 AOF 恢復資料
下一篇
[Day 17] 測試(下):stub 與 mock
系列文
從零開始打造 Redis:以 Go 建立 Production Ready 應用程式 29
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言