官术网_书友最值得收藏!

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()
    ...
}
主站蜘蛛池模板: 资溪县| 沽源县| 哈尔滨市| 通州区| 新化县| 河北区| 乳山市| 绵阳市| 左云县| 竹溪县| 东海县| 曲阜市| 左贡县| 新河县| 安庆市| 汕尾市| 禄丰县| 昂仁县| 台湾省| 凌海市| 邹城市| 福州市| 商都县| 邮箱| 洪洞县| 崇阳县| 博罗县| 甘南县| 沭阳县| 台北县| 江安县| 石棉县| 河源市| 泰和县| 彰武县| 巧家县| 南召县| 舒兰市| 德昌县| 平邑县| 宜良县|