- Go語言精進之路:從新手到高手的編程思想、方法和技巧(2)
- 白明
- 1522字
- 2022-01-04 17:42:23
44.2 stub:對返回結果有一定預設控制能力的替身
stub也是一種替身概念,和fake替身相比,stub替身增強了對替身返回結果的間接控制能力,這種控制可以通過測試前對調用結果預設置來實現。不過,stub替身通常僅針對計劃之內的結果進行設置,對計劃之外的請求也無能為力。
使用Go標準庫net/http/httptest實現的用于測試的Web服務就可以作為一些被測對象所依賴外部服務的stub替身。下面就來看一個這樣的例子。
該例子的被測代碼為一個獲取城市天氣的客戶端,它通過一個外部的天氣服務來獲得城市天氣數據:
// chapter8/sources/stubtest1/weather_cli.go type Weather struct { City string `json:"city"` Date string `json:"date"` TemP string `json:"temP"` Weather string `json:"weather"` } func GetWeatherInfo(addr string, city string) (*Weather, error) { url := fmt.Sprintf("%s/weather?city=%s", addr, city) resp, err := http.Get(url) if err != nil { return nil, err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("http status code is %d", resp.StatusCode) } body, err := ioutil.ReadAll(resp.Body) if err != nil { return nil, err } var w Weather err = json.Unmarshal(body, &w) if err != nil { return nil, err } return &w, nil }
下面是針對GetWeatherInfo函數的測試代碼:
// chapter8/sources/stubtest1/weather_cli_test.go var weatherResp = []Weather{ { City: "nanning", TemP: "26~33", Weather: "rain", Date: "05-04", }, { City: "guiyang", TemP: "25~29", Weather: "sunny", Date: "05-04", }, { City: "tianjin", TemP: "20~31", Weather: "windy", Date: "05-04", }, } func TestGetWeatherInfoOK(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { var data []byte if r.URL.EscapedPath() != "/weather" { w.WriteHeader(http.StatusForbidden) } r.ParseForm() city := r.Form.Get("city") if city == "guiyang" { data, _ = json.Marshal(&weatherResp[1]) } if city == "tianjin" { data, _ = json.Marshal(&weatherResp[2]) } if city == "nanning" { data, _ = json.Marshal(&weatherResp[0]) } w.Write(data) })) defer ts.Close() addr := ts.URL city := "guiyang" w, err := GetWeatherInfo(addr, city) if err != nil { t.Fatalf("want nil, got %v", err) } if w.City != city { t.Errorf("want %s, got %s", city, w.City) } if w.Weather != "sunny" { t.Errorf("want %s, got %s", "sunny", w.City) } }
在上面的測試代碼中,我們使用httptest建立了一個天氣服務器替身,被測函數GetWeatherInfo被傳入這個構造的替身天氣服務器的服務地址,其對外部服務的依賴需求被滿足。同時,我們看到該替身具備一定的對服務返回應答結果的控制能力,這種控制通過測試前對返回結果的預設置實現(上面例子中設置了三個城市的天氣信息結果)。這種能力可以實現對測試結果判斷的控制。
接下來,回到mailclient的例子。之前的示例只聚焦于對Send的測試,而忽略了對Compose的測試。如果要驗證郵件內容編排得是否正確,就需要對ComposeAndSend方法的返回結果進行驗證。但這里存在一個問題,那就是ComposeAndSend依賴的簽名獲取方法sign.Get中返回的時間簽名是當前時間,這對于測試代碼來說就是一個不確定的值,這也直接導致ComposeAndSend的第一個返回值的內容是不確定的。這樣一來,我們就無法對Compose部分進行測試。要想讓其具備可測性,我們需要對被測代碼進行局部重構:可以抽象出一個Signer接口(這樣就需要修改創建mailClient的New函數),當然也可以像下面這樣提取一個包級函數類型變量(考慮到演示的方便性,這里使用了此種方法,但不代表它比抽象出接口的方法更優):
// chapter8/sources/stubtest2/mailclient.go var getSign = sign.Get // 提取一個包級函數類型變量 func (c *mailClient) ComposeAndSend(subject, sender string, destinations []string, body string) (string, error) { signTxt := getSign(sender) newBody := body + "\n" + signTxt for _, dest := range destinations { err := c.mlr.SendMail(subject, sender, dest, newBody) if err != nil { return "", err } } return newBody, nil }
我們看到新版mailclient.go提取了一個名為getSign的函數類型變量,其默認值為sign包的Get函數。同時,為了演示,我們順便更新了ComposeAndSend的參數列表以及mailer的接口定義,并增加了一個sender參數:
// chapter8/sources/stubtest2/mailer/mailer.go type Mailer interface { SendMail(subject, sender string, destination string, body string) error }
由于getSign的存在,我們就可以在測試代碼中為簽名獲取函數(sign.Get)建立stub替身了。
// chapter8/sources/stubtest2/mailclient_test.go var senderSigns = map[string]string{ "tonybai@example.com": "I'm a go programmer", "jimxu@example.com": "I'm a java programmer", "stevenli@example.com": "I'm a object-c programmer", } func TestComposeAndSendWithSign(t *testing.T) { old := getSign sender := "tonybai@example.com" timestamp := "Mon, 04 May 2020 11:46:12 CST" getSign = func(sender string) string { selfSignTxt := senderSigns[sender] return selfSignTxt + "\n" + timestamp } defer func() { getSign = old //測試完畢后,恢復原值 }() m := &fakeOkMailer{} mc := New(m) body, err := mc.ComposeAndSend("hello, stub test", sender, []string{"xxx@example.com"}, "the test body") if err != nil { t.Errorf("want nil, got %v", err) } if !strings.Contains(body, timestamp) { t.Errorf("the sign of the mail does not contain [%s]", timestamp) } if !strings.Contains(body, senderSigns[sender]) { t.Errorf("the sign of the mail does not contain [%s]", senderSigns [sender]) } sender = "jimxu@example.com" body, err = mc.ComposeAndSend("hello, stub test", sender, []string{"xxx@example.com"}, "the test body") if err != nil { t.Errorf("want nil, got %v", err) } if !strings.Contains(body, senderSigns[sender]) { t.Errorf("the sign of the mail does not contain [%s]", senderSigns [sender]) } }
在新版mailclient_test.go中,我們使用自定義的匿名函數替換了getSign原先的值(通過defer在測試執行后恢復原值)。在新定義的匿名函數中,我們根據傳入的sender選擇對應的個人簽名,并將其與預定義的時間戳組合在一起返回給ComposeAndSend方法。
在這個例子中,我們預置了三個Sender的個人簽名,即以這三位sender對ComposeAndSend發起請求,返回的結果都在stub替身的控制范圍之內。
在GitHub上有一個名為gostub(https://github.com/prashantv/gostub)的第三方包可以用于簡化stub替身的管理和編寫。以上面的例子為例,如果改寫為使用gostub的測試,代碼如下:
// chapter8/sources/stubtest3/mailclient_test.go func TestComposeAndSendWithSign(t *testing.T) { sender := "tonybai@example.com" timestamp := "Mon, 04 May 2020 11:46:12 CST" stubs := gostub.Stub(&getSign, func(sender string) string { selfSignTxt := senderSigns[sender] return selfSignTxt + "\n" + timestamp }) defer stubs.Reset() ... }
- Implementing VMware Horizon 7(Second Edition)
- Cocos2d Cross-Platform Game Development Cookbook(Second Edition)
- Mastering LibGDX Game Development
- Hands-On Enterprise Automation with Python.
- Getting Started with Python Data Analysis
- Cocos2d-x學習筆記:完全掌握Lua API與游戲項目開發 (未來書庫)
- Angular開發入門與實戰
- 軟件測試教程
- PHP 8從入門到精通(視頻教學版)
- The Statistics and Calculus with Python Workshop
- Python Linux系統管理與自動化運維
- Java Web開發教程:基于Struts2+Hibernate+Spring
- 軟硬件綜合系統軟件需求建模及可靠性綜合試驗、分析、評價技術
- Microsoft Dynamics GP 2013 Cookbook
- Koa與Node.js開發實戰