基於代碼架構設計 + 第三方工具 --> 改善單測代碼質量
單元測試代碼難寫?
- 代碼架構設計不夠完善,從上到下的交互的邊界不夠清晰,可能在業務層存在調用第三方系統的地方
// bad
package service
func learnGo() {
// ...
// 針對業務代碼,認為第三方系統不穩定,輸出的結果不固定,系統內部的是穩定的
// 單元測試需要將不穩定的X因素,需要分包分層屏蔽掉
http.Post()
// ...
}
面向對象編程
首先拋出一個經典問題:“面向對象和麪向過程有什麼區別?”這是個抽象的問題,本質上可以劃分到哲學的範疇,涉及到個人看待世界的角度.我是個俗人,不太會聊哲學,但是代碼領域的問題,我挺能聊.下面,我們就化抽象為具象,嘗試用代碼實現一個場景——“把一隻大象裝進冰箱”.
在面向過程編程的視角下:解決問題的核心是化整為零,把大問題拆解為一個個小問題,再針對小問題進行逐個擊破.在執行綱領的指導下,我們在編寫代碼時需要注重的是步驟的拆分與流程的串聯.下面展示一下偽代碼:
func putElephantIntoFridge(){
// 打開冰箱門
openFridge()
// 把大象放進冰箱
putElephant()
// 關閉冰箱門
openFridge()
}
與面向過程相對,在面向對象編程的視角之下:一切皆為對象.在本場景中,我選擇把大象和冰箱都看成是有靈魂的角色,並且準備在交互場景中給予它們更多的參與感.於是,這裏首先塑造出大象和冰箱這兩種角色(聲明對象類);其次再給對應的角色注入靈魂(賦予屬性和方法);最後,把主動權交還給各個角色,由它們完成場景下的互動:
- 構造對象/注入靈魂
就以大象裝冰箱的場景為例,我們首先我們構造出大象和冰箱兩個對象,並賦予其對應的能力,比如: - 大象是有生命的,它會有自己的情緒,會有行動的能力;
- 冰箱作為容器,除了一些基本信息之外,最重要是具有裝載事物的能力.
// 大象
type Elephant struct{
// 年齡
Age int
// 名字
Name string
// 體重
Weight int
// 身高
Height int
// ...
}
// 大象是會移動的. 試試它自己會自己爬進冰箱嗎
func (e *Elephant)Move(){
// ...
}
// 注意,大象進入冰箱可能會被凍哭
func (e *Elephant) Cry(){
// ...
}
// 冰箱
type Fridge struct{
// 冰箱裏存放的東西
Things map[string]interface{}
// 高度
Height int
// 寬度
Width int
// 品牌
Brand string
// 電壓
Voltage int
// ...
}
// 冰箱具有裝載東西的能力
func (f *Fridge)PutSomethingIn(name string, something interface{}){
// 開門
f.Open()
// 把東西放進冰箱
f.Things[name] = something
// 關門
f.Close()
}
// 打開冰箱門
func (f *Fridge)Open(){
// ...
}
// 關上冰箱門
func (f *Fridge)Close(){
// ...
}
- 由對象完成交互
接下來,在場景的描述中,我們首先構造出參與其中的各個對象,然後通過各對象本身固有的能力完成交互.
func main(){
// new 一隻大象
elephant := NewElephant()
// new 一個冰箱
fridge := NewFridge()
// 冰箱裝大象
fridge.PutSomethingIn(elephant.Name, elephant)
}
通過上述例子,希望能幫助大家對面向對象的編程哲學產生更直觀的感受.
// good
package client
type CourseClient interface {
LearnGo()
LearnC()
//...
}
func NewCourseClient() CourseClient{
return &courseClientImpl{}
}
type courseClientImpl struct {
}
type (c *courseClientImpl) LearnGo() {
http.Post()
}
type (c *courseClientImpl) LearnC() {
http.Post()
}
package service
type CourseService interface{
LearnGo() {}()
}
type courseServiceImpl struct {
courseClient *client.CourseClient // 因為是interface類型,寫單測代碼的時候定義一個mock代碼,可以規避調用第三方系統
}
func NewCourseService(c *client.CourseClient) CourseService{
return &courseServiceImpl{
courseClient: c,
}
}
type (c *courseServiceImpl) LearnGo() {
// step1
// step2
// step3
// bad2
// 面向對象service依賴的重的組件,不能每次都創建,要不單元測試還是一樣有問題
// c := client.NewCourseClient()
// c.LearnGo()
c.courseClient.LearnGo()
// step4
// step5
}
type mockCourseClient struct {}
type (c *mockCourseClient) LearnGo() {
// test
}
type (c *mockCourseClient) LearnC() {
}
// 單元測試代碼
func Test_courseServiceImpl_LearnGo(*testing.T) {
mockCourseClient := &mockCourseClient{}
mockService := NewCourseService(mockCourseClient)
mockService.LearnGo()
}
interface的正確用法
Go interfaces generally belong in the package that uses values of the interface type, not the package that implements those values. The implementing package should return concrete (usually pointer or struct) types: that way, new methods can be added to implementations without requiring extensive refactoring.
Do not define interfaces on the implementor side of an API “for mocking”; instead, design the API so that it can be tested using the public API of the real implementation.
Do not define interfaces before they are used: without a realistic example of usage, it is too difficult to see whether an interface is even necessary, let alone what methods it ought to contain.
package consumer // consumer.go
type Thinger interface { Thing() bool }
func Foo(t Thinger) string { … }
package consumer // consumer_test.go
type fakeThinger struct{ … }
func (t fakeThinger) Thing() bool { … }
…
if Foo(fakeThinger{…}) == "x" { … }
// DO NOT DO IT!!!
package producer
type Thinger interface { Thing() bool }
type defaultThinger struct{ … }
func (t defaultThinger) Thing() bool { … }
func NewThinger() Thinger { return defaultThinger{ … } }
Instead return a concrete type and let the consumer mock the producer implementation.
package producer
type Thinger struct{ … }
func (t Thinger) Thing() bool { … }
func NewThinger() Thinger { return Thinger{ … } }
https://go.dev/wiki/CodeReviewComments#interfaces
// good
package client
func NewCourseClient() CourseClient{
return &CourseClient{}
}
type CourseClient struct {
}
type (c *CourseClient) LearnGo() {
http.Post()
}
type (c *CourseClient) LearnC() {
http.Post()
}
package service
type courseProxy interface {
LearnGo()
}
type CourseService interface{
LearnGo() {}()
}
type courseServiceImpl struct {
courseClient *courseProxy // 因為是interface類型,寫單測代碼的時候定義一個mock代碼,可以規避調用第三方系統
}
func NewCourseService(c *courseProxy) CourseService{
return &courseServiceImpl{
courseClient: c,
}
}
type (c *courseServiceImpl) LearnGo() {
// step1
// step2
// step3
// bad2
// 面向對象service依賴的重的組件,不能每次都創建,要不單元測試還是一樣有問題
// c := client.NewCourseClient()
// c.LearnGo()
c.courseClient.LearnGo()
// step4
// step5
}
type mockCourseClient struct {}
type (c *mockCourseClient) LearnGo() {
// test
}
// 單元測試代碼
func Test_courseServiceImpl_LearnGo(*testing.T) {
mockCourseClient := &mockCourseClient{}
mockService := NewCourseService(mockCourseClient)
mockService.LearnGo()
}
gomock
go get github.com/golang/mock/gomock
go get github.com/golang/mock/mockgen
go install github.com/golang/mock/mockgen
// go:generate mockgen -destination=./mock/mock_human.go -package=mock -source=interface.go
// interface.go
package main
//go:generate mockgen -destination=./mock/mock_human.go -package=mock -source=interface.go
type Human interface {
Speak() string
Walk() string
}
// mock_human_test.go
package main
import (
"github.com/golang/mock/gomock"
"human/mock"
"testing"
)
func Test_mock_human(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockHuman := mock.NewMockHuman(ctrl)
// 篡改speak的執行邏輯
mockHuman.EXPECT().Speak().DoAndReturn(func() string {
return "hello"
}).Times(2)
output := mockHuman.Speak()
t.Errorf("output:%s", output)
output2 := mockHuman.Speak()
t.Errorf("output:%s", output2)
}
gomonkey
package main
import (
"github.com/agiledragon/gomonkey"
"reflect"
"testing"
)
type Boy struct {
}
func (b *Boy) Speak() string {
return "hello"
}
func Laugh() string {
return "laugh"
}
func Test_gomonkey(t *testing.T) {
b := &Boy{}
patch := gomonkey.ApplyMethod(reflect.TypeOf(&Boy{}), "Speak", func(b *Boy) string {
return "55555555"
})
defer patch.Reset()
t.Logf("speek:%s", b.Speak())
patch2 := gomonkey.ApplyFunc(Laugh, func() string {
return "1111111"
})
defer patch2.Reset()
t.Logf("laugh:%s", Laugh())
}