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

    技术2024-07-28  69

    引入

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

    Processed: 0.055, SQL: 9