简单易懂的Go语言的单元测试教程

引入

谷歌一搜Go语言如何写单元测试,千篇一律都是关于整个包只有一个函数怎么实现测试。然而,很多情况下无法对复杂结构的代码进行测试。这篇博文教你在函数依赖于结构体的条件下实现单元测试。

最简单的例子

hello.go文件

package main

import (
	"fmt"
)

func hello() string {
	return "Hello, Testing!"
}

func main() {
	fmt.Println(hello())
}

hello_test.go文件

package main

import (
	"testing"
)

func TestHello(t *testing.T) {
	expectedStr := "Hello, Testing!"
	result := hello()
	if result != expectedStr {
		t.Fatalf("Expected %s, got %s", expectedStr, result)
	}
}

hello.go文件与hello_test.go文件都是在main包下的,区别在于,单元测试文件要以XXX_test.go结尾。
这里我们要测试的是helle()函数的功能,其输入为空,理想输出是"Hello, Testing!"字符串。在测试文件中,首先用result保存hello()函数的结果,接着对照这个实际结果与理想结果是否相同。注意!!!错误输出函数t.Fatalf最好保留,不要换成t.error,不然很大可能编译不出。

重点来了,进阶内容

下面我要展示一个复杂的单元测试过程,代码很长。如果你没有耐心仔细看,你可以跟随我的文字捋清楚思路。
首先需要知道的前提,这个单元测试需要完成的是job.go文件当中run()函数的测试,以及Suspend()和Resume()的测试。run()函数的测试对应这些job_test.go文件当中的estPollerJobRunLog()函数,Suspend()和Resume()的测试对应这些job_test.go文件当中的TestPollerJobSuspendResume()函数。
这些需要被测试的函数,都是依赖于一个结构体的,单独根本无法测试,那怎么办呢?
需要什么我们就给什么,先创建这个结构体实例,再用这个实例调用测试方法,比较实际结果和理想结果即可。
当然,如果想透彻的理解单元测试,就多花点时间仔细分析下面的代码结构。
job.go文件

package main

import (
	"fmt"
	"log"
	"net/http"
	"time"
)

type Logger interface {
	Log(...interface{})
}

type SuspendResumer interface {
	Suspend() error
	Resume() error
}

type Job interface {
	Logger
	SuspendResumer
	Run() error
}

type ServerPoller interface {
	PollServer() (string, error)
}

type PollerLogger struct{}

type URLServerPoller struct {
	resourceUrl string
}

type PollSuspendResumer struct {
	SuspendCh chan bool
	ResumeCh  chan bool
}

type PollerJob struct {
	WaitDuration time.Duration
	ServerPoller
	Logger
	*PollSuspendResumer
}

func NewPollerJob(resourceUrl string, waitDuration time.Duration) PollerJob {
	return PollerJob{
		WaitDuration: waitDuration,
		Logger:       &PollerLogger{},
		ServerPoller: &URLServerPoller{
			resourceUrl: resourceUrl,
		},
		PollSuspendResumer: &PollSuspendResumer{
			SuspendCh: make(chan bool),
			ResumeCh:  make(chan bool),
		},
	}
}

func (l *PollerLogger) Log(args ...interface{}) {
	log.Println(args...)
}

func (usp *URLServerPoller) PollServer() (string, error) {
	resp, err := http.Get(usp.resourceUrl)
	if err != nil {
		return "", err
	}

	return fmt.Sprint(usp.resourceUrl, " -- ", resp.Status), nil
}

func (ssr *PollSuspendResumer) Suspend() error {
	ssr.SuspendCh <- true
	return nil
}

func (ssr *PollSuspendResumer) Resume() error {
	ssr.ResumeCh <- true
	return nil
}

func (p PollerJob) Run() error {
	for {
		select {
		case <-p.PollSuspendResumer.SuspendCh:
			<-p.PollSuspendResumer.ResumeCh
		default:
			state, err := p.PollServer()
			if err != nil {
				p.Log("Error trying to get state: ", err)
			} else {
				p.Log(state)
			}

			time.Sleep(p.WaitDuration)
		}
	}

	return nil
}

func main() {
	var j Job
	j = NewPollerJob("http://nathanleclaire.com", 1*time.Second)
	go j.Run()
	time.Sleep(5 * time.Second)

	j.Log("Suspending monitoring of server for 5 seconds...")
	j.Suspend()
	time.Sleep(5 * time.Second)

	j.Log("Resuming job...")
	j.Resume()

	// Wait for a bit before exiting
	time.Sleep(5 * time.Second)
}

job_test.go文件

package main

import (
	"errors"
	"fmt"
	"testing"
	"time"
)

type ReadableLogger interface {
	Logger
	Read() string
}

type MessageReader struct {
	Msg string
}

func (mr *MessageReader) Read() string {
	return mr.Msg
}

type LastEntryLogger struct {
	*MessageReader
}

func (lel *LastEntryLogger) Log(args ...interface{}) {
	lel.Msg = fmt.Sprint(args...)
}

type DiscardFirstWriteLogger struct {
	*MessageReader
	writtenBefore bool
}

func (dfwl *DiscardFirstWriteLogger) Log(args ...interface{}) {
	if dfwl.writtenBefore {
		dfwl.Msg = fmt.Sprint(args...)
	}
	dfwl.writtenBefore = true
}

type FakeServerPoller struct {
	result string
	err    error
}

func (fsp FakeServerPoller) PollServer() (string, error) {
	return fsp.result, fsp.err
}

func TestPollerJobRunLog(t *testing.T) {
	waitBeforeReading := 100 * time.Millisecond
	shortInterval := 20 * time.Millisecond
	longInterval := 200 * time.Millisecond

	testCases := []struct {
		p           PollerJob
		logger      ReadableLogger
		sp          ServerPoller
		expectedMsg string
	}{
		{
			p:           NewPollerJob("madeup.website", shortInterval),
			logger:      &LastEntryLogger{&MessageReader{}},
			sp:          FakeServerPoller{"200 OK", nil},
			expectedMsg: "200 OK",
		},
		{
			p:           NewPollerJob("down.website", shortInterval),
			logger:      &LastEntryLogger{&MessageReader{}},
			sp:          FakeServerPoller{"500 SERVER ERROR", nil},
			expectedMsg: "500 SERVER ERROR",
		},
		{
			p:           NewPollerJob("error.website", shortInterval),
			logger:      &LastEntryLogger{&MessageReader{}},
			sp:          FakeServerPoller{"", errors.New("DNS probe failed")},
			expectedMsg: "Error trying to get state: DNS probe failed",
		},
		{
			p: NewPollerJob("some.website", longInterval),

			// Discard first write since we want to verify that no
			// additional logs get made after the first one (time
			// out)
			logger: &DiscardFirstWriteLogger{MessageReader: &MessageReader{}},

			sp:          FakeServerPoller{"200 OK", nil},
			expectedMsg: "",
		},
	}

	for _, c := range testCases {
		c.p.Logger = c.logger
		c.p.ServerPoller = c.sp

		go c.p.Run()

		time.Sleep(waitBeforeReading)

		if c.logger.Read() != c.expectedMsg {
			t.Errorf("Expected message did not align with what was written:\n\texpected: %q\n\tactual: %q", c.expectedMsg, c.logger.Read())
		}
	}
}

func TestPollerJobSuspendResume(t *testing.T) {
	p := NewPollerJob("foobar.com", 20*time.Millisecond)
	waitBeforeReading := 100 * time.Millisecond
	expectedLogLine := "200 OK"
	normalServerPoller := &FakeServerPoller{expectedLogLine, nil}

	logger := &LastEntryLogger{&MessageReader{}}
	p.Logger = logger
	p.ServerPoller = normalServerPoller

	// First start the job / polling
	go p.Run()

	time.Sleep(waitBeforeReading)

	if logger.Read() != expectedLogLine {
		t.Errorf("Line read from logger does not match what was expected:\n\texpected: %q\n\tactual: %q", expectedLogLine, logger.Read())
	}

	// Then suspend the job
	if err := p.Suspend(); err != nil {
		t.Errorf("Expected suspend error to be nil but got %q", err)
	}

	// Fake the log line to detect if poller is still running
	newExpectedLogLine := "500 Internal Server Error"
	logger.MessageReader.Msg = newExpectedLogLine

	// Give it a second to poll if it's going to poll
	time.Sleep(waitBeforeReading)

	// If this log writes, we know we are polling the server when we're not
	// supposed to (job should be suspended).
	if logger.Read() != newExpectedLogLine {
		t.Errorf("Line read from logger does not match what was expected:\n\texpected: %q\n\tactual: %q", newExpectedLogLine, logger.Read())
	}

	if err := p.Resume(); err != nil {
		t.Errorf("Expected resume error to be nil but got %q", err)
	}

	// Give it a second to poll if it's going to poll
	time.Sleep(waitBeforeReading)

	if logger.Read() != expectedLogLine {
		t.Errorf("Line read from logger does not match what was expected:\n\texpected: %q\n\tactual: %q", expectedLogLine, logger.Read())
	}
}

总结

看了这篇博文,大家以后都能够把单元测试当成小菜一碟的事情处理了。总结一句话,我们需要什么就在测试函数中创建什么。本篇代码来自GitHub的单元测试案例

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值