引入
谷歌一搜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的单元测试案例。