Go語言精進之路讀書筆記第44條——正確運用fake、stub和mock等輔助單元測試

brynchen發表於2024-03-10

44.1 fake:真實元件或服務的簡化實現版替身

  • fake測試就是指採用真實元件或服務的簡化版實現作為替身,以滿足被測程式碼的外部依賴需求。
  • 使用fake替身進行測試的最常見理由是在測試環境無法構造被測程式碼所依賴的外部元件或服務,或者這些元件/服務有副作用。
type fakeOkMailer struct{}

func (m *fakeOkMailer) SendMail(subject string, dest string, body string) error {
    return nil
}

func TestComposeAndSendOk(t *testing.T) {
    m := &fakeOkMailer{}
    mc := mailclient.New(m)
    _, err := mc.ComposeAndSend("hello, fake test", []string{"xxx@example.com"}, "the test body")
    if err != nil {
        t.Errorf("want nil, got %v", err)
    }
}

type fakeFailMailer struct{}

func (m *fakeFailMailer) SendMail(subject string, dest string, body string) error {
    return fmt.Errorf("can not reach the mail server of dest [%s]", dest)
}

func TestComposeAndSendFail(t *testing.T) {
    m := &fakeFailMailer{}
    mc := mailclient.New(m)
    _, err := mc.ComposeAndSend("hello, fake test", []string{"xxx@example.com"}, "the test body")
    if err == nil {
        t.Errorf("want non-nil, got nil")
    }
}

44.2 stub:對返回結果有一定預設控制能力的替身

stub替身增強了對替身返回結果的間接控制能力,這種控制可以透過測試前對呼叫結果預設定來實現。

被測程式碼

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
}

測試程式碼:我們使用httptest建立了一個天氣伺服器替身

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)
    }
}

44.3 mock:專用於行為觀察和驗證的替身

  • 能力:mock能提供測試前的預設定返回結果能力,還可以對mock替身物件在測試過程中的行為進行觀察和驗證
  • 侷限性:mock只用於實現某介面的實現型別的替身;一般需要第三方框架支援(github.com/golang/mock/gomock)

相關文章