小徐先生的单测经历 的学习笔记,视频真干货!

引言#

1
2
3
4
5
6
7
8
9
10
11
func LearnGO(){
// step 1
// step 2

http.Post("..","..",nil)

// step 3
// step 4

return
}

在我们写代码的过程中经常会迷茫怎么写单测,为什么单测难写

  1. 单测逻辑的不确定性
  2. 第三方依赖的复杂性导致单测结果的不稳定性

下面讲的所有内容都是针对第二点而言,看上面的代码,我们发现当我们使用http.post的时候,我们是不知道我们所要访问的网站是怎么样的,会返回什么值,网站的状态好坏,这些都会影响到我们单测的值,而我们在单测的时候,肯定会希望对功能的前提条件外界因素进行打桩,来判断最终结果是否满足我们的需求

这个时候我们肯定会寻求一些mock来模拟这些外界条件,可看上面这个代码,我们似乎很难通过mock来模拟,因为http.post嵌入在代码之中,具有强耦合性

所以我们需要思考我们应该如何书写代码架构来解决单测问题

面向对象和面向过程#

面向对象和面向过程谁都明白,但是如何用代码表现出两种形式就是重点了

大象进入冰箱为例:

  • 面向过程

    1
    2
    3
    4
    5
    6
    7
    8
    9
    func moveToReg(){
    elephantWalk()

    RegOpen()

    elephantIn()

    RegClose()
    }

    面向过程顾名思义是针对过程来撰写代码的,通过一个个函数来代表一个一个过程

  • 面向对象

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    type srv interface{
    Walk
    Cry
    }

    type elephant struct {
    name string
    Age int
    email string
    }

    func (e *elepthant)Walk(){
    ...
    }

    func (e *elephant) Cry(){

    }

    func moveToReg(){
    e := NewElephant()

    e.Walk()
    }

    在面向对象中,我们更注重于对象个体,对象有属性和行为,通过两个方面去考虑

第一次重构#

毫无疑问,第一版的代码是面向过程的,那么如果改成面向对象又会怎么样呢

首先我们肯定会对代码进行分层,http.post的代码逻辑不可能存在于service

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package client 

type CourceClient interface{
LearningGo()
LearningJAVA()
LearningC()
// ....
}

type courseClientImpl struct {}

func NewCourceClient()CouseClient{
return &courseClientImpl{}
}

func (c *courseClientImpl) LearningGo(){
http.Post(xxxx)
}

func (c *courseClientImpl) LearningJAVA(){
http.Post(xxxx)
}

// .....
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package service 

type CourceService interface {

}

type CourseServiceImpl struct {
client client.CourceClient
}

func NewCouseService(client client.CourceClient)CouseService{
return &CourseServiceImpl{
client: client
}
}

type (c *CourseServiceImpl) LearningGo(){
// step1
// step2

c.client.LearningGo()

// step3
// step4

return
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package test 

type mockService struct {}

func (m *mockService) LearningGo(){}

func (m *mockService) LearningJAVA(){}

//.......

// test:

func test(t *test.T){
mock := &mockService{}

s := service.NewCouseService(mock)

s.LearningGo()

if xxx {
t.Error(xxx)
}
}

在上面的代码中使用了面向对象的思维实现,使用了多态的思想

  1. 首先是interface的实现,以前我一直将interface看作是一种规范,但是上面的代码中,我更愿意将其称作版本,对于同一个interface可以有 mockService的版本以及正式环境courseClient的版本,这就是使用了多态的思想
  2. 面向对象的实现就是构造出一个struct,然后给struct写上方法,同时用interface来标准规范
  3. 这里的构造函数返回了interface,其实golang是不支持返回接口的
  4. 这里有个问题:就是我们发现每次我们单测都需要将courseClient interface的所有接口都实现一遍才能够实现测试,因此–肯定是interface的使用问题

Interface#

Go interface 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 types.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// DON NOT DO IT !
package producer

type Thinger interface {Thing()bool}

type defaultThinger struct{...}
func (t defaultThinger) Thing() bool{...}

func NewThinger() Thinger {return defaultThinger}{...}}

// instead as following:
package producer

type Thinger struct {Thing()bool}
func(t Thinger) Thing()bool {}

func NewThinger() Thinger {
return Thinger{...}
}

大体意思就是要把interface属于使用者,而不是实现者,同时实现的包中应该返回具体的type

第二次重构#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package client 
// 这里是interface 的实现方
// 因此直接把 courseClient 干掉了
type CouseClient struct {}

func NewCourceClient()CouseClient{
return &courseClientImpl{}
}

func (c *CouseClient) LearningGo(){
http.Post(xxxx)
}

func (c *CouseClient) LearningJAVA(){
http.Post(xxxx)
}

// .....
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package service 
// 这里是interface的使用方

type CouseService interface{

}

type CourseProxy interface {
// 因为我只用到了LearningGo(),所以只需要写一个就行了
LearningGo()
}

type CourseServiceImpl struct {
client CourseProxy
}

func NewCouseService(client CourseProxy)*CourseServiceImpl{
return &CourseServiceImpl{
client: client
}
}

type (c *CourseServiceImpl) LearningGo(){
// step1
// step2

c.client.LearningGo()

// step3
// step4

return
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package test 

type mockService struct {}

func (m *mockService) LearningGo(){}


// test:

func test(t *test.T){
mock := &mockService{}

s := service.NewCouseService(mock)

s.LearningGo()

if xxx {
t.Error(xxx)
}
}

我们将 interface 的实现放到了 service 层,会使得interface的使用更加灵活

client 作为服务的提供方,不可能每次都提供恰到好处的接口来实现,因此就会造成实现不必要的接口的情况

而将interface放入使用方的情况下,很多问题就会迎刃而解,更加方便

这样子来看 我的CourseServiceImpl这个单例他只负责 LearningGo 的行为,而不用满足LearningJAVA等情况

单测工具#

  • gomock
  • gomonkey
  • assert
  • test
  • sqlmock