日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 人文社科 > 生活经验 >内容正文

生活经验

如何编写可测试的golang代码

發布時間:2023/11/27 生活经验 23 豆豆
生活随笔 收集整理的這篇文章主要介紹了 如何编写可测试的golang代码 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

每次在開發之前,我都會考慮寫好單元測試,但是隨著開發的進行,就會發現事情沒有這么簡單,因為更多時候項目中間夾雜著很多的數據庫操作,網絡操作,文件操作等等,每次涉及到有這些操作的單元測試,都要花費很大的代價取初始化各種環境,拖到最后單元測試只能不了了之,因此這里的一個重點是寫出來的代碼本身不可測試,因此在這篇文章中,重點是如何寫出可測試的代碼,如何把一些無關的操作屏蔽掉,文章是我幾個月之前翻譯的,最近在項目中進行了實踐,感覺不錯,因此放到這里,希望能有更多的人看到。原文地址

在golang中通過接口和組合來實現高效的單元測試

go單元測試提供了:

  • Increased enforcement of behavior expectations beyond the compiler (providing critical assurance around rapidly changing code paths)
  • 快速的執行速度(許多流行的模塊測試能在秒級完成)
  • 易于集成到CI環境(go test為內建)
  • 通過-race標志進行競態檢測

因此,單元測試是確保代碼質量和防止回歸的最佳方式之一。不幸的是,單元測試經常是很多go項目中最容易被忽視的方面之一。

這種情況有一部分是由于缺乏高質量的資源來解釋如何正確構建一個可以被測試的go程序導致的。這份文檔嘗試提供這兩方面的努力,提高go社區中可用程序的總體質量。

不要因為程序運行了就讓你陷入錯誤的安全感:你不久就會慶幸你開始了測試。

預覽

在文章中我會介紹下面的內容

  • 確保可測試的概念
  • 4個具體的例子來學習如何在go中進行有效的測試

最后你應該使用你學到的東西應用到實踐中

概念
如果你從一開始就沒有正確的構建和測試你的程序,那么測試這條路將會非常困難。這在編程界是一個相當普遍的格言在go測試中尤其正確

為了有效的測試go程序,有三個重要的概念:

  • 在你的go代碼中使用接口
  • 通過組合構建更高層次的接口
  • 熟悉go test和testing模塊

下面來詳細解釋一下

使用接口

你通過閱讀go官方文檔已經在工作中熟悉了go接口的使用。你可能不明白為什么接口如此重要,以及為什么你應該盡可能快的開始使用你自己的接口

對于那些不熟悉接口的,我建議去讀一下go的官方文檔?來理解接口是怎么工作的

長話短說

  • 接口是一組被定義的被考慮實現的方法類型的集合
  • 當任何給出的類型實現了該接口的所有方法時,go編譯器就認為它實現了該接口

這在go的標準庫中被用的很頻繁。例如,在database/sql中使用相同的接口來編寫與不同數據庫進行交互的功能

新手go程序員可能已經能熟練的在其他編程語言中寫單元測試像java,python或者php,通過使用stubs或者mocks技術來偽造方法調用的結果并且使用一種細粒度的方式來探索各種代碼的路徑。然而許多人并沒有意識到,接口已經把上面的都實現了。

由于被嵌入到語言中,并被標準庫所支持,接口為測試者提供了大量的功能和靈活性。可以在接口中封裝給定測試之外的操作,并選擇性的將其重新實現以用于相關測試。這允許作者控制測試中行為的每個方面。

使用組合
接口對增加靈活性和控制非常重要,但還不夠。例如,考慮一下我們有一個struct將大量方法公開給外部消費者的情況,但是在其他的某些操作中也依賴于這些方法。我們不能將所有的對象封裝在一個接口中,我們只需要實現我們需要測試的方法就夠了。

因此,這變得至關重要,通過使用較小的接口來組成更大的接口,以便能夠控制我們想要改變哪些方法和不想改變哪些方法去適應測試。在一個實際的例子中這樣看起來更容易一點,因此我會避免更抽象的討論直到文章的結束。

go test和testing模塊

很明顯,你至少應該瀏覽一下go test和testing模塊的文檔,熟悉一下每塊能夠讓你更有效的進行單元測試。如果你不熟悉這些工具和庫,用起來就會有些生疏(但是一旦熟悉了就好了)

那是很有吸引力的,第三方工具可以幫助測試,但是我強烈建議你避免這樣做,直到你掌握了基礎知識,并且確定依賴帶給你的好處多于壞處。

首先你要有下面的一些基礎知識

  • 對于任何給定的foo.go,測試被放置在相同的目錄中并被命名為foo_test.go
  • go test . 運行當前目中的的單元測試。 go test ./... 將運行當前目錄和該目錄之下的測試, go test foo_test.go不工作因為被測試的文件不包括在內
  • -v標志對go test是有用的,它會打印出詳細的輸出(每個單獨的測試結果)
  • Tests是個函數接受一個testing.T結構指針作為一個參數,并調用TestFoo,其中Foo是被測試的函數名稱
  • 通常不會一直如你所期望的那樣為真,相反,測試失敗可以調用t.Fatal,如果你確定條件與你期望的不同
  • 在測試中打印輸出可能不會如你所期望的那樣工作。如果你在測試中需要打印信息可以使用t.Log或者t.Logf

例子

討論的夠多了,來寫一些測試

下面的例子都可以在github上找到代碼

例子1: Hello, Testing!
我假定你已經安裝并且配置好了go開發環境

新建一個go的包在GOPATH下:

$ mkdir -p ~/go/src/github.com/nathanleclaire/testing-article
$ cd ~/go/src/github.com/nathanleclaire/testing-article

創建一個hello.go的文件

package mainimport ("fmt"
)func hello() string {return "Hello, Testing!"
}func main() {fmt.Println(hello())
}

  

現在為hello.go寫一個測試
新建一個hello_test.go的文件在相同的文件夾下

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

  

這個測試很簡單。我們注入了一個*testing.T的實例到測試中,這被用來控制測試流和輸出。我們把對函數調用的期望設置在一個變量中,然后在函數真正返回的時候檢查它。

運行測試

$ go test -v
=== RUN TestHello
---PASS:TestHello(0.00s)
PASS
OK  github.com/nathanleclaire/testing-article 0.006s

例子2:用一個接口來模擬結果

作為程序的一部分,我們希望從GitHubAPI中獲取一些數據。在這種情況下,假設我們想要查詢一個給出的庫的最新的tag

我們很容易寫出下面的代碼

package mainimport ("encoding/json""fmt""io/ioutil""log""net/http"
)type ReleasesInfo struct {Id      uint   `json:"id"`TagName string `json:"tag_name"`
}// Function to actually query the GitHub API for the release information.
func getLatestReleaseTag(repo string) (string, error) {apiUrl := fmt.Sprintf("https://api.github.com/repos/%s/releases", repo)response, err := http.Get(apiUrl)if err != nil {return "", err}defer response.Body.Close()body, err := ioutil.ReadAll(response.Body)if err != nil {return "", err}releases := []ReleasesInfo{}if err := json.Unmarshal(body, &releases); err != nil {return "", err}tag := releases[0].TagNamereturn tag, nil
}// Function to get the message to display to the end user.
func getReleaseTagMessage(repo string) (string, error) {tag, err := getLatestReleaseTag(repo)if err != nil {return "", fmt.Errorf("Error querying GitHub API: %s", err)}return fmt.Sprintf("The latest release is %s", tag), nil
}func main() {msg, err := getReleaseTagMessage("docker/machine")if err != nil {fmt.Fprintln(os.Stderr, msg)}fmt.Println(msg)
}

  

事實上,這是一個自然而然想到的go程序結構

但這是不可測試的。如果我們要getLatestReleaseTag 直接測試這個函數,那么如果GitHub API關閉了,或者GitHub決定限制我們(如果在CI環境中頻繁地運行測試,那很可能會影響我們)。另外,每當最新版本標簽更改時,我們都必須更新測試。

該怎么辦?我們可以重新定義這個實現的方式,使其更具可測性。如果我們查詢Github API使用interface來代替直接調用函數 ,那么我們實際上可以控制通過測試返回的結果。

我們重新定義這個程序有一點就是讓他有個接口,ReleaseInfoer其中一個實現可以是GithubReleaseInfoer。ReleaseInfoer只有一個方法,GetLatestReleaseTag它在性質上與我們上面的函數類似(它接受一個存儲庫名稱作為參數并返回一個 string和/或error作為結果)。

該接口看著像下面這樣

type ReleaseInfoer interface {GetLatestReleaseTag(string) (string, error)
}

  

然后我們更新上面的函數直接調用使用GithubReleaseInfoer結構代替

type GithubReleaseInfoer struct {}// Function to actually query the GitHub API for the release information.
func (gh GithubReleaseInfoer) GetLatestReleaseTag(repo string) (string, error) {// ... same code as above
}

更新后,getReleaseTagMessage和main像下面這樣

// Function to get the message to display to the end user.
func getReleaseTagMessage(ri ReleaseInfoer, repo string) (string, error) {tag, err := ri.GetLatestReleaseTag(repo)if err != nil {return "", fmt.Errorf("Error query GitHub API: %s", err)}return fmt.Sprintf("The latest release is %s", tag), nil
}func main() {gh := GithubReleaseInfoer{}msg, err := getReleaseTagMessage(gh, "docker/machine")if err != nil {fmt.Fprintln(os.Stderr, err)os.Exit(1)}fmt.Println(msg)
}

  

為什么要這么干?現在我們可以測試getReleaseTagMessage函數通過定義一個新的結構,只要實現了具有一個方法的ReleaseInfoer接口。這樣,在測試的時候,我們就可以確保我們所依賴的方法的行為完全如我們期望的那樣。

我們能定義一個FakeReleaseInfoer結構來表現我們想要的結構。我們只需要在結構中定義要返回的內容

package mainimport "testing"type FakeReleaseInfoer struct {Tag stringErr error
}func (f FakeReleaseInfoer) GetLatestReleaseTag(repo string) (string, error) {if f.Err != nil {return "", f.Err}return f.Tag, nil
}func TestGetReleaseTagMessage(t *testing.T) {f := FakeReleaseInfoer{Tag: "v0.1.0",Err: nil,}expectedMsg := "The latest release is v0.1.0"msg, err := getReleaseTagMessage(f, "dev/null")if err != nil {t.Fatalf("Expected err to be nil but it was %s", err)}if expectedMsg != msg {t.Fatalf("Expected %s but got %s", expectedMsg, msg)}
}

  

從上面可以看到,FakeReleaseInfoer被設置為返回Tag v0.1.0和Err nil

這個測試很好,但是我們沒有測試錯誤返回。這種情況最好也要測一下

在單元測試中有什么方法可以表達這個函數的各種測試用例和我們期望的返回值呢。當然,我們可以在一個函數中用一個匿名的結構體來構造測試用例和所期望的返回值

func TestGetReleaseTagMessage(t *testing.T) {cases := []struct {f           FakeReleaseInfoerrepo        stringexpectedMsg stringexpectedErr error}{{f: FakeReleaseInfoer{Tag: "v0.1.0",Err: nil,},repo:        "doesnt/matter",expectedMsg: "The latest release is v0.1.0",expectedErr: nil,},{f: FakeReleaseInfoer{Tag: "v0.1.0",Err: errors.New("TCP timeout"),},repo:        "doesnt/foo",expectedMsg: "",expectedErr: errors.New("Error querying GitHub API: TCP timeout"),},}for _, c := range cases {msg, err := getReleaseTagMessage(c.f, c.repo)if !reflect.DeepEqual(err, c.expectedErr) {t.Errorf("Expected err to be %q but it was %q", c.expectedErr, err)}if c.expectedMsg != msg {t.Errorf("Expected %q but got %q", c.expectedMsg, msg)}}
}

  

注意reflect.DeepEqual的使用。這是來自標準庫中的用來檢查兩個結構體是否相等的方法。這里用來檢查錯誤是否相等,但也可以用來比較兩個結構體的內容。僅僅使用 == 在這里并不能比較出相等,由于errors.New的使用(我嘗試使用Error方法,但是對于nil值不工作,如果你有更好的方法可以告訴我)

這種技術在測試中可以獲得更多對第三方庫的控制。例如,Sam Alba的Golang Docker客戶端會給你一個type DockerClient struct的交互,這對測試來說并不容易mock。但是你可以用type DockerClient interface在你自己的模塊中創建一個模塊,它指定你正在使用的方法dockerclient.DockerClient作為要實現的東西,在你的代碼中使用它,然后創建你自己的接口版本來測試。

除了我在這里重點討論的可測試性的好處外,使用接口可能會為您的程序的未來可擴展性帶來巨大的利益。如果您已經構建了與GitHub API交互的每個組件,例如通過接口工作,則根本不需要更改程序的架構,以添加對其他源代碼托管平臺的支持。你可以簡單地實現一個BitbucketReleaseInfoer 并使用它來包裝Bitbucket API而不是GitHub。當然,這種類型的包裝抽象將不適用于每個用例,但它可以用來強有力地模擬出外部和內部的依賴關系。

例子3 使用組合來測試一個更大的struct

上面的例子說明了一個可能非常有用的介紹性概念,但是有時候我們可能想要模擬一個struct相互依賴的部分,并分別測試每個部分。

如果你發現自己的一個interface或struct在一系列的要暴露方法中開始變大,那么分成幾個更小的解耦并且相互組合可能是個好主意。例如,假設我們有個Job接口,它暴露了一個Log方法的內部和外部的結構。可以傳遞可變數量的參數傳遞給這個方法。它也提供Runing,Suspending和Resume方法

type Job interface {Log(...interface{})Suspend() errorResume() errorRun() error
}

  

如果我們在工作中開發了一個struct并且實現了該接口,我們想使用內部的Log方法來記錄日志。因此,像上面的例子那樣實現整個接口是行不通的。那我們如何mock接口的部分來測試整個結構呢?

我們可以通過定義幾個更小的接口然后使用組合。考慮一個Job,PollerJob的實現,用來做系統監控軟件。我的第一版代碼如下:

package mainimport ("log""net/http""time"
)type Job interface {Log(...interface{})Suspend() errorResume() errorRun() error
}type PollerJob struct {suspend     chan boolresume      chan boolresourceUrl stringinMemLog    string
}func NewPollerJob(resourceUrl string) PollerJob {return PollerJob{resourceUrl: resourceUrl,suspend:     make(chan bool),resume:      make(chan bool),}
}func (p PollerJob) Log(args ...interface{}) {log.Println(args...)
}func (p PollerJob) Suspend() error {p.suspend <- truereturn nil
}func (p PollerJob) PollServer() error {resp, err := http.Get(p.resourceUrl)if err != nil {return err}p.Log(p.resourceUrl, "--", resp.Status)return nil
}func (p PollerJob) Run() error {for {select {case <-p.suspend:<-p.resumedefault:if err := p.PollServer(); err != nil {p.Log("Error trying to get resource: ", err)}time.Sleep(1 * time.Second)}}
}func (p PollerJob) Resume() error {p.resume <- truereturn nil
}func main() {p := NewPollerJob("https://nathanleclaire.com")go p.Run()time.Sleep(5 * time.Second)p.Log("Suspending monitoring of server for 5 seconds...")p.Suspend()time.Sleep(5 * time.Second)p.Log("Resuming job...")p.Resume()// Wait for a bit before exitingtime.Sleep(5 * time.Second)
}

  

上面程序的輸出結構如下:

$ go run -race job.go
2015/10/11 20:37:59 https://nathanleclaire.com -- 200 OK
2015/10/11 20:38:01 https://nathanleclaire.com -- 200 OK
2015/10/11 20:38:02 https://nathanleclaire.com -- 200 OK
2015/10/11 20:38:03 https://nathanleclaire.com -- 200 OK
2015/10/11 20:38:04 https://nathanleclaire.com -- 200 OK
2015/10/11 20:38:04 Suspending monitoring of server for 5 seconds...
2015/10/11 20:38:10 Resuming job...
2015/10/11 20:38:10 https://nathanleclaire.com -- 200 OK
2015/10/11 20:38:11 https://nathanleclaire.com -- 200 OK
2015/10/11 20:38:12 https://nathanleclaire.com -- 200 OK
2015/10/11 20:38:14 https://nathanleclaire.com -- 200 OK
2015/10/11 20:38:15 https://nathanleclaire.com -- 200 OK
2015/10/11 20:38:16 https://nathanleclaire.com -- 200 OK

如果我們想測試各種復雜的互動,怎么辦呢?所有的方法都放在一起,不使用外部資源測試程序的每個組件似乎是一件令人頭疼的事情。

解決方案是將更高層的Job接口分解為幾個其他接口,并將它們全部嵌入到PollerJob結構中,這樣我們就可以在測試的時候將每個接口單獨模擬出來。

我們能將Job接口拆分成幾個不同的接口,如下所示:

type Logger interface {Log(...interface{})
}type SuspendResumer interface {Suspend() errorResume() error
}type Job interface {LoggerSuspendResumerRun() error
}

  

您可以看到有一個SuspendResumer用于處理掛起/恢復功能的接口,并且一個Log僅用于管理Log方法的接口。另外,我們將創建一個PollServer接口來控制對我們正在輪詢的服務器的狀態調用:

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

  

有了所有這些組件接口,我們就可以開始重新構建我們PollerJob的Job接口實現。通過嵌入Logger 和ServerPoller(兩個接口)和一個指向PollSuspendResumer結構的指針,我們保證對于PollerJob作為一個Job的定義能通過編譯。我們提供了一個NewPollerJob函數,它將提供一個結構的實例,并且正確地設置和初始化所有的組件。請注意,我們使用我們自己的組件實現了這個函數的返回。

type PollerLogger struct{}type URLServerPoller struct {resourceUrl string
}type PollSuspendResumer struct {SuspendCh chan boolResumeCh  chan bool
}type PollerJob struct {WaitDuration time.DurationServerPollerLogger*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),},}
}

  

其余的代碼定義了相關的結構,并且可以在github上獲得

這為我們提供了靈活性,當我們進行測試時,我們需要將PollerJob結構中的每個組件單獨虛擬出來。每個組件可以在需要的地方重新使用和重復工作,更靈活,使我們能夠從我們依賴的組件中獲得更多的可能。

我們現在能單獨測試Run,而不必與任何實際的服務器通信。我們只需要簡單的控制ServerPoller的返回并且驗證被寫入的內容是否與我們預期的那樣。因此測試文件看起來像下面這樣。

package mainimport ("errors""fmt""testing""time"
)type ReadableLogger interface {LoggerRead() 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 {*MessageReaderwrittenBefore bool
}func (dfwl *DiscardFirstWriteLogger) Log(args ...interface{}) {if dfwl.writtenBefore {dfwl.Msg = fmt.Sprint(args...)}dfwl.writtenBefore = true
}type FakeServerPoller struct {result stringerr    error
}func (fsp FakeServerPoller) PollServer() (string, error) {return fsp.result, fsp.err
}func TestPollerJobRunLog(t *testing.T) {waitBeforeReading := 100 * time.MillisecondshortInterval := 20 * time.MillisecondlongInterval := 200 * time.MillisecondtestCases := []struct {p           PollerJoblogger      ReadableLoggersp          ServerPollerexpectedMsg 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.loggerc.p.ServerPoller = c.spgo 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())}}
}

  

請注意,創建我們自己的ReadableLogger接口進行測試并能夠以各種方式實現Logger為我們提供了靈活性的幫助。Suspend而且Resume也同樣能夠被測試通過控制JobPoller組件的ServerPoller接口

func TestPollerJobSuspendResume(t *testing.T) {p := NewPollerJob("foobar.com", 20*time.Millisecond)waitBeforeReading := 100 * time.MillisecondexpectedLogLine := "200 OK"normalServerPoller := &FakeServerPoller{expectedLogLine, nil}logger := &LastEntryLogger{&MessageReader{}}p.Logger = loggerp.ServerPoller = normalServerPoller// First start the job / pollinggo 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 jobif 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 runningnewExpectedLogLine := "500 Internal Server Error"logger.MessageReader.Msg = newExpectedLogLine// Give it a second to poll if it's going to polltime.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 polltime.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())}
}

  

測試一個小功能會有很多方法,但是隨著代碼量的增長,它要有很好的擴展。照這樣mock可以使其更容易指定錯誤情況下的表現或者在復雜的并發情況下控制邏輯。

由于接口為測試提供了實用性和創造性,最好將外部依賴關系封裝在一個中,然后將它們組合起來,以盡可能創建更高級的接口。正如你所希望的那樣,即使小的單一方法的接口也可以被用來組合成更大的功能。

例子4:使用和構造標準庫功能
如上圖所示的概念是為自己的程序非常有用,但你也將注意到,許多在go標準庫的構建可以在單元測試中以類似的方式進行管理.

我們來看看測試一個HTTP服務器的例子。在goroutine中實際啟動HTTP服務器并向它發送你希望能夠直接處理的請求(例如http.Get),但是這更像一個集成測試而不是一個合適的單元測試。下面看一個小型的http服務,并討論如何進行測試。

package mainimport ("fmt""log""net/http"
)func mainHandler(w http.ResponseWriter, r *http.Request) {token := r.Header.Get("X-Access-Token")if token == "magic" {fmt.Fprintf(w, "You have some magic in you\n")log.Println("Allowed an access attempt")} else {http.Error(w, "You don't have enough magic in you", http.StatusForbidden)log.Println("Denied an access attempt")}
}func main() {http.HandleFunc("/", mainHandler)log.Fatal(http.ListenAndServe(":8080", nil))
}

  

上面的Http服務監聽在8080端口,并且檢查是否有一個X-Access-Token的header被設置。如果token匹配上了我們的"magic"值,我們允許用戶訪問并且返回一個HTTP 200 OK的狀態碼。否則我們拒絕請求,返回一個403.這是對一些API服務器如何處理授權的簡單模仿,該如何測試它呢?

正如你所看到的。這個mainHandler函數接收兩個參數,a http.ResponseWriter(注意它是一個interface你可以通過閱讀源http源碼或文檔來驗證)和一個http.Request結構體指針。為了測試這個handler,我們能構造http.ResponseWriter 接口的實現,今后也可以繼續使用,幸運的是,Go作者已經提供了一個httptest包含ResponseRecorder 結構的包,以幫助解決這個問題。這樣的模塊提供了通用的測試功能用一個有用而常見的模式。

鑒于此,我們也能手工構造一個http.Request結構通過調用NewRequest帶上我們期望的參數。我們只需要簡單的調用Header.Set在Request上來設置header。我們在NewRequest方法中指定它應該是GET方法,并且不再請求體中包含任何信息,同樣我們也可以測試POST請求

初始化的測試如下:

package mainimport ("bytes""net/http""net/http/httptest""testing"
)func TestMainHandler(t *testing.T) {rootRequest, err := http.NewRequest("GET", "/", nil)if err != nil {t.Fatal("Root request error: %s", err)}cases := []struct {w                    *httptest.ResponseRecorderr                    *http.RequestaccessTokenHeader    stringexpectedResponseCode intexpectedResponseBody []byte}{{w:                    httptest.NewRecorder(),r:                    rootRequest,accessTokenHeader:    "magic",expectedResponseCode: http.StatusOK,expectedResponseBody: []byte("You have some magic in you\n"),},{w:                    httptest.NewRecorder(),r:                    rootRequest,accessTokenHeader:    "",expectedResponseCode: http.StatusForbidden,expectedResponseBody: []byte("You don't have enough magic in you\n"),},}for _, c := range cases {c.r.Header.Set("X-Access-Token", c.accessTokenHeader)mainHandler(c.w, c.r)if c.expectedResponseCode != c.w.Code {t.Errorf("Status Code didn't match:\n\t%q\n\t%q", c.expectedResponseCode, c.w.Code)}if !bytes.Equal(c.expectedResponseBody, c.w.Body.Bytes()) {t.Errorf("Body didn't match:\n\t%q\n\t%q", string(c.expectedResponseBody), c.w.Body.String())}}
}

  

但是,我們可以考慮的測試功能有一個顯而易見的缺失。我們不檢查寫入的log內容是我們所期望的。我們該怎么做?

如果我們檢查標準庫的log包的源代碼,我們就可以看到這個log.Println方法直接封裝了一個Logger 結構的實例,內部調用了Write方法在Writer接口上(在使用std結構的情況下,如果你直接引用 log.*,那么Writer就是os.Stdout).我想知道是否有任何方法可以將接口設置為我們期望的那樣,以便可以驗證所寫的就是我們期望的。

當然,有一種方法可以這樣做,我們能引用log.SetOutput方法來指定我們自定義的writer為了記錄日志。我們使用io.Pipe來創建Writer。這將為我們提供一個Reader,我們能用它來讀隨后的writer調用在Logger中。我們用bufio.Reader封裝了給出的PipeReader,因此我們可以調用bufio.Reader的ReadString方法一行一行的讀。

注意PipeWriter的文檔:

Write實現了標準的寫接口,它寫入數據到管道,阻塞直到readers讀完所有的數據或者read端被關閉。

因此,我們必須并發的讀從PipeReader中,在mainHandler函數正在寫入時,我在自己的goroutine中運行這個測試。在我原來的版本中我得到這個錯誤,并通過使用go test的-timeout標志發現了這個錯誤,如果超時的話它會導致panic。

最后組合起來,像下面這樣:

func TestMainHandler(t *testing.T) {rootRequest, err := http.NewRequest("GET", "/", nil)if err != nil {t.Fatal("Root request error: %s", err)}cases := []struct {w                    *httptest.ResponseRecorderr                    *http.RequestaccessTokenHeader    stringexpectedResponseCode intexpectedResponseBody []byteexpectedLogs         []string}{{w:                    httptest.NewRecorder(),r:                    rootRequest,accessTokenHeader:    "magic",expectedResponseCode: http.StatusOK,expectedResponseBody: []byte("You have some magic in you\n"),expectedLogs: []string{"Allowed an access attempt\n",},},{w:                    httptest.NewRecorder(),r:                    rootRequest,accessTokenHeader:    "",expectedResponseCode: http.StatusForbidden,expectedResponseBody: []byte("You don't have enough magic in you\n"),expectedLogs: []string{"Denied an access attempt\n",},},}for _, c := range cases {logReader, logWriter := io.Pipe()bufLogReader := bufio.NewReader(logReader)log.SetOutput(logWriter)c.r.Header.Set("X-Access-Token", c.accessTokenHeader)go func() {for _, expectedLine := range c.expectedLogs {msg, err := bufLogReader.ReadString('\n')if err != nil {t.Errorf("Expected to be able to read from log but got error: %s", err)}if !strings.HasSuffix(msg, expectedLine) {t.Errorf("Log line didn't match suffix:\n\t%q\n\t%q", expectedLine, msg)}}}()mainHandler(c.w, c.r)if c.expectedResponseCode != c.w.Code {t.Errorf("Status Code didn't match:\n\t%q\n\t%q", c.expectedResponseCode, c.w.Code)}if !bytes.Equal(c.expectedResponseBody, c.w.Body.Bytes()) {t.Errorf("Body didn't match:\n\t%q\n\t%q", string(c.expectedResponseBody), c.w.Body.String())}}
}

  

我希望這些例子清楚地說明了在Go標準庫中以及在你自己的代碼中具有良好架構的接口的價值,以及如何讀取你所依賴的模塊的源代碼(包括Go標準庫,它是很準確的文檔)可以讓你更好的理解你正在使用的代碼,以及簡化測試。

?

轉載地址? :??https://github.com/AmateurEvents/article/issues/1

轉載于:https://www.cnblogs.com/iceiceiceice/p/9303516.html

總結

以上是生活随笔為你收集整理的如何编写可测试的golang代码的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。

主站蜘蛛池模板: 女性女同性aⅴ免费观女性恋 | 国产一区二区视频在线观看 | 国产农村乱对白刺激视频 | 91丨porny在线| 久操超碰| 美腿丝袜一区二区三区 | 经典三级av在线 | 一区二区三区 日韩 | 欧美亚洲国产一区 | av资源部| 国产深夜福利 | 老妇女性较大毛片 | 超碰96在线 | 呦呦网 | 中文字幕123 | 日韩精品一区二区三区高清免费 | 久久久www| 末路1997全集免费观看完整版 | 国产欧美一区二区三区国产幕精品 | av大片免费在线观看 | 国产一区麻豆 | 亚洲成人久久久 | 国内av在线 | 九色av | 影音先锋精品 | 日韩欧美啪啪 | 你懂的在线播放 | 亚洲AV无码一区二区三区蜜桃 | 久久精品一区二区三区不卡牛牛 | 日韩人妻精品一区二区三区视频 | 男人日女人b视频 | ass精品国模裸体欣赏pics | 中文在线a∨在线 | 韩国三级中文字幕hd浴缸戏 | 欧美美女一区二区三区 | 亚洲一区二区三区成人 | 色日韩| 囯产精品一品二区三区 | 国模无码大尺度一区二区三区 | 91精品国产色综合久久不卡电影 | 黄瓜视频91| 重囗味sm一区二区三区 | 性欧美videos另类hd | 精品国产AV色欲天媒传媒 | 日日操视频 | 天天视频污 | 嫩草视频在线观看视频 | 中文字幕在线视频一区二区三区 | 久久精品久久久久久久 | 淫欲少妇 | 91高清网站 | 久久精品6 | 黄色网址在线免费 | 久久精品国产一区二区 | 91波多野结衣 | 成人高清视频免费观看 | 狠狠爱网站 | 色诱av| 欧美整片sss | 国产精品视频1区 | 在线免费观看污视频 | 四虎4hu永久免费网站影院 | 美女色诱男人激情视频 | 福利视频99 | 成人av高清在线观看 | av在线男人天堂 | 亚洲精品视频在线看 | 天天天天天天天天干 | 得得的爱在线视频 | 91丨porny丨在线中文 | 亚洲成人一级 | 日本欧美中文字幕 | 韩国三级做爰高潮 | 亚洲中文字幕第一区 | 人妻久久久一区二区三区 | www.偷拍.com | 又黄又爽无遮挡 | 蜜桃视频久久一区免费观看入口 | 亚洲不卡在线 | 久久网一区 | 日本aa大片| 亚洲欧美强伦一区二区 | 久久r这里只有精品 | 九七超碰在线 | 91免费观看视频 | 欧美精品在欧美一区二区少妇 | 欧美老女人xx | 超碰女优| 亚洲视频一二区 | 亚洲人成无码www久久久 | 中文字幕一区二区三区在线播放 | 天天想夜夜操 | 国产一级做a爰片久久毛片男男 | 天堂视频免费在线观看 | 黄色小说视频网站 | 伊人网在线播放 | 国产丰满美女做爰 | 亚洲天堂av一区二区 | 伊人久久久久噜噜噜亚洲熟女综合 |