Go 单元测试与接口

原文作者:Andrei Avram

  • Good code needs tests
  • Tests require good design
  • Good design implies decoupling
  • Interfaces help decouple
  • Decoupling lets you write tests
  • Tests help having good code

Good code and unit testing come hand in hand, and sometimes the bridge between them are interfaces. When you have an interface, you can easily “hide” any implementation behind it, even a mock for a unit test.

An important subject of unit testing is managing external dependencies. The tests should directly cover the unit while using fake replacements (mocks) for the dependencies.

I was given the following code and asked to write tests for it:

 1package mail
 2
 3import (
 4   "fmt"
 5   "net"
 6   "net/smtp"
 7   "strings"
 8)
 9
10func ValidateHost(email string) (err error) {
11   mx, err := net.LookupMX(host(email))
12   if err != nil {
13      return err
14   }
15
16   client, err := smtp.Dial(fmt.Sprintf("%s:%d", mx[0].Host, 25))
17   if err != nil {
18      return err
19   }
20
21   defer func() {
22      if er := client.Close(); er != nil {
23         err = er
24      }
25   }()
26
27   if err = client.Hello("checkmail.me"); err != nil {
28      return err
29   }
30   if err = client.Mail("testing-email-host@gmail.com"); err != nil {
31      return err
32   }
33   return client.Rcpt(email)
34}
35
36func host(email string) (host string) {
37   i := strings.LastIndexByte(email, '@')
38   return email[i+1:]
39}

The first steps were to identify test cases and dependencies:

  • Cases are represented by each flow that the code can go through, starting with the normal one when the last return gives no error, and then all the branches that can change this normal flow (all the if statements). So beware of complexity. More branches, mores cases, more tests, more possible issues.
  • The dependencies that will be mocked net.LookupMX and smtp.Dial.

The first test would look like this:

 1package mail
 2
 3import (
 4   "testing"
 5
 6   "github.com/stretchr/testify/assert"
 7)
 8
 9func TestValidateHost(t *testing.T) {
10   email := "mail@host.tld"
11   actual := ValidateHost(email)
12   assert.NoError(t, actual)
13}

And the result:

=== RUN   TestValidateHost
--- FAIL: TestValidateHost (0.02s)
mail_test.go:12:
Error Trace:   mail_test.go:12
Error:         Received unexpected error:
lookup host.tld on 127.0.1.1:53: no such host
Test:          TestValidateHost
FAIL

The net.LookupMX function was actually called, but it’s a dependency, so we need to mock it. Go has first class functions, so net.LookupMX can be assigned to a variable:

 1package mail
 2
 3import (
 4   "fmt"
 5   "net"
 6   "net/smtp"
 7   "strings"
 8)
 9
10var netLookupMX = net.LookupMX
11
12func ValidateHost(email string) (err error) {
13   mx, err := netLookupMX(host(email))
14   if err != nil {
15      return err
16   }
17
18   ...
19}
20
21...

Then replaced in the test:

 1package mail
 2
 3import (
 4   "net"
 5   "testing"
 6
 7   "github.com/stretchr/testify/assert"
 8)
 9
10func TestValidateHost(t *testing.T) {
11   netLookupMX = func(name string) ([]*net.MX, error) {
12      mxs := []*net.MX{
13         {
14            Host: "host.tld",
15            Pref: 1,
16         },
17      }
18
19      return mxs, nil
20   }
21
22   ...
23}

Our custom function will be called instead of the real one.

=== RUN   TestValidateHost
--- FAIL: TestValidateHost (0.01s)
mail_test.go:24:
Error Trace:   mail_test.go:24
Error:         Received unexpected error:
dial tcp: lookup host.tld on 127.0.1.1:53: no such host
Test:          TestValidateHost
FAIL

Now, the test fails because of the smtp.Dial call. We’ll handle this situation like we did for the first one, but there’s one important difference: the function smtp.Dial returns an SMTP Client that we need a mock for, and here the interface comes to help.

Let’s create a function to return the real SMTP client in the implementation and the mock in the test. This is a polymorphic situation: two implementations described by an interface. So let’s also have an interface which is implemented by both real and mock SMTP client.

 1package mail
 2
 3import (
 4   "fmt"
 5   "net"
 6   "net/smtp"
 7   "strings"
 8)
 9
10type dialer interface {
11   Close() error
12   Hello(localName string) error
13   Mail(from string) error
14   Rcpt(to string) error
15}
16
17var (
18   netLookupMX = net.LookupMX
19   smtpClient  = func(addr string) (dialer, error) {
20      // Dial the tcp connection
21      conn, err := net.Dial("tcp", addr)
22      if err != nil {
23         return nil, err
24      }
25
26      // Connect to the SMTP server
27      c, err := smtp.NewClient(conn, addr)
28      if err != nil {
29         return nil, err
30      }
31
32      return c, nil
33   }
34)
35
36func ValidateHost(email string) (err error) {
37   mx, err := netLookupMX(host(email))
38   if err != nil {
39      return err
40   }
41
42   client, err := smtpClient(fmt.Sprintf("%s:%d", mx[0].Host, 25))
43   if err != nil {
44      return err
45   }
46
47   defer func() {
48      if er := client.Close(); er != nil {
49         err = er
50      }
51   }()
52
53   if err = client.Hello("checkmail.me"); err != nil {
54      return err
55   }
56   if err = client.Mail("testing-email-host@gmail.com"); err != nil {
57      return err
58   }
59   return client.Rcpt(email)
60}
61
62func host(email string) (host string) {
63   i := strings.LastIndexByte(email, '@')
64   return email[i+1:]
65}

The real SMTP client implements our interface because it has all its methods.

 1package mail
 2
 3import (
 4   "net"
 5   "testing"
 6
 7   "github.com/stretchr/testify/assert"
 8)
 9
10type smtpDialerMock struct {
11}
12
13func (*smtpDialerMock) Close() error {
14   return nil
15}
16func (*smtpDialerMock) Hello(localName string) error {
17   return nil
18}
19func (*smtpDialerMock) Mail(from string) error {
20   return nil
21}
22func (*smtpDialerMock) Rcpt(to string) error {
23   return nil
24}
25
26func TestValidateHost(t *testing.T) {
27   netLookupMX = func(name string) ([]*net.MX, error) {
28      mxs := []*net.MX{
29         {
30            Host: "host.tld",
31            Pref: 1,
32         },
33      }
34
35      return mxs, nil
36   }
37
38   smtpClient = func(addr string) (dialer, error) {
39      client := &smtpDialerMock{}
40      return client, nil
41   }
42
43   email := "mail@host.tld"
44   actual := ValidateHost(email)
45   assert.NoError(t, actual)
46}

For our mock we’ve implemented the required methods.

=== RUN   TestValidateHost
--- PASS: TestValidateHost (0.00s)
PASS

As for the other test cases (the errors), we will create a different mock for each one.

 1package mail
 2
 3import (
 4   "fmt"
 5   "net"
 6   "net/smtp"
 7   "strings"
 8)
 9
10type dialer interface {
11   Close() error
12   Hello(localName string) error
13   Mail(from string) error
14   Rcpt(to string) error
15}
16
17var (
18   netLookupMX = net.LookupMX
19   smtpClient  = func(addr string) (dialer, error) {
20      // Dial the tcp connection
21      conn, err := net.Dial("tcp", addr)
22      if err != nil {
23         return nil, err
24      }
25
26      // Connect to the SMTP server
27      c, err := smtp.NewClient(conn, addr)
28      if err != nil {
29         return nil, err
30      }
31
32      return c, nil
33   }
34type smtpDialerMockFail struct {
35}
36
37func (*smtpDialerMockFail) Close() error {
38   return nil
39}
40func (*smtpDialerMockFail) Hello(localName string) error {
41   return errors.New("err")
42}
43func (*smtpDialerMockFail) Mail(from string) error {
44   return nil
45}
46func (*smtpDialerMockFail) Rcpt(to string) error {
47   return nil
48}
49
50func TestValidateHostWhenFails(t *testing.T) {
51   netLookupMX = func(name string) ([]*net.MX, error) {
52      mxs := []*net.MX{
53         {
54            Host: "host.tld",
55            Pref: 1,
56         },
57      }
58
59      return mxs, nil
60   }
61
62   smtpClient = func(addr string) (dialer, error) {
63      client := &smtpDialerMockFail{}
64      return client, nil
65   }
66
67   email := "mail@host.tld"
68   actual := ValidateHost(email)
69   assert.Error(t, actual)
70}

You can write a test function for each case or you can write table driven tests. When I want to be very precises about all the behaviors, I prefer writing a test function for each case.

You may have noticed I’ve used Testify, a great testing package. Besides assertions, it also offers a mocking framework. I’ve written basic mocks, but Testify helps you generate advanced and fluent mocks on which you can test more details of their behavior.


版权申明:内容来源网络,版权归原创者所有。除非无法确认,我们都会标明作者及出处,如有侵权烦请告知,我们会立即删除并表示歉意。谢谢。

原文发布于微信公众号 - Golang语言社区(Golangweb)

原文发表时间:2018-08-20

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏wannshan(javaer,RPC)

ConcurrentHashMap 锁分段 源码分析

看ConcurrentHashMap下几个属性: /** * The default concurrency level for this table...

4176
来自专栏向治洪

Android 应用安装过程分析

在之前的文章中,我们对PakageManagerService启动流程分析 做了简单的介绍,并对PMS系统的启动流程做了详细的解析。上面只是说到了Android...

7469
来自专栏小筱月

SSM 使用 mybatis 分页插件 pagehepler 实现分页

前几天在这里分享了手写 sql 分页查询实现分页,现在来看看使用 mybatis 分页插件 pagehepler 来实现分页

3952
来自专栏ml

caffe源码学习之Proto数据格式【1】

前言:   由于业务需要,接触caffe已经有接近半年,一直忙着阅读各种论文,重现大大小小的模型. 期间也总结过一些caffe源码学习笔记,断断续续,这次打算系...

6278
来自专栏Kubernetes

kube-proxy源码分析

##kube-proxy介绍 请参考我的另一篇博文:kube-proxy工作原理 ##源码目录结构分析 cmd/kube-proxy //负责kub...

7415
来自专栏岑玉海

sqoop 从sqlserver2008 导入数据到hadoop

  今天终于开始上手导入数据到hadoop了,哈哈,过程蛮崎岖的,和官方文档的还不太一样。   OK,let's go!试验对象是我第一个名为ST_Statis...

4195
来自专栏Android中高级开发

Android开发之漫漫长途 XI——从I到X的小结

该文章是一个系列文章,是本人在Android开发的漫漫长途上的一点感想和记录,我会尽量按照先易后难的顺序进行编写该系列。该系列引用了《Android开发艺术探索...

1232
来自专栏along的开发之旅

Mac os 下打开java内存分析工具 mat

版权声明:欢迎传播,请标明出处。 https://blog.csdn.net/u201011221/article/details/8305...

7943
来自专栏JackieZheng

学习SpringMVC——你们要的REST风格的CRUD来了

  来来来,让一下,客官,您要的REST清蒸CRUD来了,火候刚刚好,不油不腻,请慢用~~~   如果说前面是准备调料,洗菜,切菜,摆盘,那么今天就来完整的上道...

34810
来自专栏aCloudDeveloper

python网络编程初级

网络编程的专利权应该属于Unix,各个平台(如windows、Linux等)、各门语言(C、C++、Python、Java等)所实现的符合自身特性的语法都大同小...

2505

扫码关注云+社区

领取腾讯云代金券