基于代码架构设计 + 第三方工具 --> 改善单测代码质量
单元测试代码难写?
- 代码架构设计不够完善,从上到下的交互的边界不够清晰,可能在业务层存在调用第三方系统的地方
// 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())
}
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。