模拟/测试基本请求

我倾向于编写单元测试,并且我想知道对基本请求进行单元测试的正确方法。http.get


我在网上找到了一个返回假数据的API,并编写了一个基本程序来获取一些用户数据并打印出一个ID:


package main


import (

    "encoding/json"

    "fmt"

    "io/ioutil"

    "log"

    "net/http"

)


type UserData struct {

    Meta interface{} `json:"meta"`

    Data struct {

        ID     int    `json:"id"`

        Name   string `json:"name"`

        Email  string `json:"email"`

        Gender string `json:"gender"`

        Status string `json:"status"`

    } `json:"data"`

}


func main() {

    resp := sendRequest()

    body := readBody(resp)

    id := unmarshallData(body)

    fmt.Println(id)

}


func  sendRequest() *http.Response {

    resp, err := http.Get("https://gorest.co.in/public/v1/users/1841")

    if err != nil {

        log.Fatalln(err)

    }

    return resp

}


func readBody(resp *http.Response) []byte {

    body, err := ioutil.ReadAll(resp.Body)

    if err != nil {

        log.Fatalln(err)

    }

    return body

}


func unmarshallData(body []byte) int {

    var userData UserData

    json.Unmarshal(body, &userData)

    return userData.Data.ID

}

这适用于1841年并打印出来。然后,我想编写一些测试来验证代码的行为是否符合预期,例如,如果返回错误,代码将正确失败,返回的数据是否可以取消编组。我一直在阅读和查看示例,但它们都比我觉得我想要实现的目标要复杂得多。


我从以下测试开始,以确保传递给函数的数据可以取消编组:unmarshallData


package main


import (

    "testing"

)


func Test_unmarshallData(t *testing.T) {

    type args struct {

        body []byte

    }

    tests := []struct {

        name string

        args args

        want int

    }{

        {name: "Unmarshall", args: struct{ body []byte }{body: []byte("{\"meta\":null,\"data\":{\"id\":1841,\"name\":\"Piya\",\"email\":\"priya@gmai.com\",\"gender\":\"female\",\"status\":\"active\"}}")}, want: 1841},

    }

        for _, tt := range tests {

        t.Run(tt.name, func(t *testing.T) {

            if got := unmarshallData(tt.args.body); got != tt.want {

                t.Errorf("unmarshallData() = %v, want %v", got, tt.want)

            }

        })

    }

}

任何关于从这里去哪里的建议将不胜感激。


BIG阳
浏览 82回答 1
1回答

跃然一笑

在继续测试之前,你的代码有一个严重的流程,如果你在未来的编程任务中不关心它,这将成为一个问题。https://pkg.go.dev/net/http请参阅第二个示例客户端必须在完成响应正文后将其关闭现在让我们解决这个问题(我们稍后将不得不回到这个主题),两种可能性。1/ 在 中,使用延迟在耗尽该资源后将其关闭;mainfunc main() {    resp := sendRequest()    defer body.Close()    body := readBody(resp)    id := unmarshallData(body)    fmt.Println(id)}2/ 在readBodyfunc readBody(resp *http.Response) []byte {    defer resp.Body.Close()    body, err := ioutil.ReadAll(resp.Body)    if err != nil {        log.Fatalln(err)    }    return body}使用延迟是关闭资源的预期方式。它有助于读者识别资源的生存期并提高可读性。注意:我不会使用太多的表测试驱动模式,但你应该,就像你在你的OP中所做的那样。转到测试部分。测试可以写在同一个包或其同一版本下,并带有尾随,例如 。这在两个方面具有影响。_test[package target]_test使用单独的包,它们将在最终版本中被忽略。这将有助于生成更小的二进制文件。使用单独的包,以黑盒方式测试API,只能访问它显式公开的标识符。您当前的测试是白盒的,这意味着您可以访问 的任何声明,无论是否公开。main关于 ,围绕这一点编写测试并不是很有趣,因为它做得太少,并且您的测试不应该编写来测试std库。sendRequest但是,为了演示,并且出于充分的理由,我们可能不希望依赖外部资源来执行我们的测试。为了实现这一点,我们必须使其中消耗的全局依赖项成为注入的依赖项。因此,以后可以替换它所依赖的一个反应物,即http。获取方法。func  sendRequest(client interface{Get() (*http.Response, error)}) *http.Response {    resp, err := client.Get("https://gorest.co.in/public/v1/users/1841")    if err != nil {        log.Fatalln(err)    }    return resp}在这里,我使用内联接口声明。interface{Get() (*http.Response, error)}现在,我们可以添加一个新的测试,该测试注入一段代码,该代码段将准确返回将触发我们要在代码中测试的行为的值。type fakeGetter struct {    resp *http.Response    err  error}func (f fakeGetter) Get(u string) (*http.Response, error) {    return f.resp, f.err}func TestSendRequestReturnsNilResponseOnError(t *testing.T) {    c := fakeGetter{        err: fmt.Errorf("whatever error will do"),    }    resp := sendRequest(c)    if resp != nil {        t.Fatal("it should return a nil response when an error arises")    }}现在运行此测试并查看结果。它不是决定性的,因为您的函数包含对 log 的调用。致命,它依次执行操作系统。退出;我们无法对此进行检验。如果我们试图改变这一点,我们可能会认为我们可能会呼吁恐慌,因为我们可以恢复。我不建议这样做,在我看来,这是臭的和坏的,但它存在,所以我们可能会考虑。这也是对函数签名的最小可能更改。返回错误会破坏更多的当前签名。我想在那个演示中尽量减少这一点。但是,根据经验,请返回错误并始终检查它们。在函数中,将此调用替换为 并更新测试以捕获死机。sendRequestlog.Fatalln(err)panic(err)func TestSendRequestReturnsNilResponseOnError(t *testing.T) {    var hasPanicked bool    defer func() {        _ = recover() // if you capture the output value or recover, you get the error gave to the panic call. We have no use of it.        hasPanicked = true    }()    c := fakeGetter{        err: fmt.Errorf("whatever error will do"),    }    resp := sendRequest(c)    if resp != nil {        t.Fatal("it should return a nil response when an error arises")    }    if !hasPanicked {        t.Fatal("it should have panicked")    }}现在,我们可以转到另一个执行路径,即非错误返回。为此,我们锻造了所需的*http。我们想要传递到函数中的响应实例,然后我们将检查其属性,以确定函数执行的操作是否与我们预期的内联。我们将考虑要确保返回时未修改: /下面的测试只设置两个属性,我将使用它来演示如何使用 NopCloser 和字符串设置 Body。新阅读器,因为以后使用Go语言时经常需要它;我也使用反射。DeepEqual作为蛮力相等检查器,通常你可以更细粒度,得到更好的测试。 在这种情况下,它做了这项工作,但它引入了复杂性,不能证明系统地使用它是合理的。DeepEqualfunc TestSendRequestReturnsUnmodifiedResponse(t *testing.T) {    c := fakeGetter{        err: nil,        resp: &http.Response{            Status: http.StatusOK,            Body:   ioutil.NopCloser(strings.NewReader("some text")),        },    }    resp := sendRequest(c)    if !reflect.DeepEqual(resp, c.resp) {        t.Fatal("the response should not have been modified")    }}在这一点上,你可能已经认为这个小功能不好,如果你不这样做,我向你保证它不是。它做的太少,它只是包装了方法,它的测试对业务逻辑的生存没有多大兴趣。sendRequesthttp.Get继续运作。readBody所有申请的评论也适用于此。sendRequest它做得太少它是os.Exit有一件事不适用。由于对 的调用不依赖于外部资源,因此尝试注入该依赖项毫无意义。我们可以四处测试。ioutil.ReadAll但是,为了演示,现在是时候讨论缺少的呼叫了。defer resp.Body.Close()让我们假设我们去介绍中提出的第二个命题并对此进行测试。该结构充分地将其接收方公开为接口。http.ResponseBody为了确保代码调用'关闭,我们可以为它编写一个存根。该存根将记录是否进行了该调用,然后测试可以检查该调用,如果不是,则触发错误。type closeCallRecorder struct {    hasClosed bool}func (c *closeCallRecorder) Close() error {    c.hasClosed = true    return nil}func (c *closeCallRecorder) Read(p []byte) (int, error) {    return 0, nil}func TestReadBodyCallsClose(t *testing.T) {    body := &closeCallRecorder{}    res := &http.Response{        Body: body,    }    _ = readBody(res)    if !body.hasClosed {        t.Fatal("the response body was not closed")    }}同样,为了便于演示,我们可能希望测试该函数是否调用了 。Readtype readCallRecorder struct {    hasRead bool}func (c *readCallRecorder) Read(p []byte) (int, error) {    c.hasRead = true    return 0, nil}func TestReadBodyHasReadAnything(t *testing.T) {    body := &readCallRecorder{}    res := &http.Response{        Body: ioutil.NopCloser(body),    }    _ = readBody(res)    if !body.hasRead {        t.Fatal("the response body was not read")    }}我们还验证了身体之间没有修改,func TestReadBodyDidNotModifyTheResponse(t *testing.T) {    want := "this"    res := &http.Response{        Body: ioutil.NopCloser(strings.NewReader(want)),    }    resp := readBody(res)    if got := string(resp); want != got {        t.Fatal("invalid response, wanted=%q got %q", want, got)    }}我们差不多完成了,让我们把一个移到函数上。unmarshallData你已经写了一个关于它的测试。不过,这是可以的,我会这样写,以使其更精简:type UserData struct {    Meta interface{} `json:"meta"`    Data Data        `json:"data"`}type Data struct {    ID     int    `json:"id"`    Name   string `json:"name"`    Email  string `json:"email"`    Gender string `json:"gender"`    Status string `json:"status"`}func Test_unmarshallData(t *testing.T) {    type args struct {        body []byte    }    tests := []UserData{        UserData{Data: Data{ID: 1841}},    }    for _, u := range tests {        want := u.ID        b, _ := json.Marshal(u)        t.Run("Unmarshal", func(t *testing.T) {            if got := unmarshallData(b); got != want {                t.Errorf("unmarshallData() = %v, want %v", got, want)            }        })    }}然后,通常适用:不记录。致命你在测试什么?编组者 ?最后,现在我们已经收集了所有这些部分,我们可以重构以编写更合理的函数,并重新使用所有这些部分来帮助我们测试此类代码。我不会这样做,但这是一个启动器,它仍然令人恐慌,我仍然不推荐,但是前面的演示已经显示了测试返回错误的版本所需的一切。type userFetcher struct {    Requester interface {        Get(u string) (*http.Response, error)    }}func (u userFetcher) Fetch() int {    resp, err := u.Requester.Get("https://gorest.co.in/public/v1/users/1841") // it does not really matter that this string is static, using the requester we can mock the response, its body and the error.    if err != nil {        panic(err)    }    defer resp.Body.Close() //always.    body, err := ioutil.ReadAll(resp.Body)    if err != nil {        panic(err)    }    var userData UserData    err = json.Unmarshal(body, &userData)    if err != nil {        panic(err)    }    return userData.Data.ID}
打开App,查看更多内容
随时随地看视频慕课网APP

相关分类

Go