为了账号安全,请及时绑定邮箱和手机立即绑定

模拟/测试基本请求

模拟/测试基本请求

Go
BIG阳 2022-09-19 17:28:42
我倾向于编写单元测试,并且我想知道对基本请求进行单元测试的正确方法。http.get我在网上找到了一个返回假数据的API,并编写了一个基本程序来获取一些用户数据并打印出一个ID:package mainimport (    "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年并打印出来。然后,我想编写一些测试来验证代码的行为是否符合预期,例如,如果返回错误,代码将正确失败,返回的数据是否可以取消编组。我一直在阅读和查看示例,但它们都比我觉得我想要实现的目标要复杂得多。我从以下测试开始,以确保传递给函数的数据可以取消编组:unmarshallDatapackage mainimport (    "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)            }        })    }}任何关于从这里去哪里的建议将不胜感激。
查看完整描述

1 回答

?
跃然一笑

TA贡献1826条经验 获得超6个赞

在继续测试之前,你的代码有一个严重的流程,如果你在未来的编程任务中不关心它,这将成为一个问题。


https://pkg.go.dev/net/http请参阅第二个示例


客户端必须在完成响应正文后将其关闭


现在让我们解决这个问题(我们稍后将不得不回到这个主题),两种可能性。


1/ 在 中,使用延迟在耗尽该资源后将其关闭;main


func main() {

    resp := sendRequest()

    defer body.Close()

    body := readBody(resp)

    id := unmarshallData(body)

    fmt.Println(id)

}

2/ 在readBody


func 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作为蛮力相等检查器,通常你可以更细粒度,得到更好的测试。 在这种情况下,它做了这项工作,但它引入了复杂性,不能证明系统地使用它是合理的。DeepEqual

func 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")

    }

}

同样,为了便于演示,我们可能希望测试该函数是否调用了 。Read


type 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

}


查看完整回答
反对 回复 2022-09-19
  • 1 回答
  • 0 关注
  • 81 浏览
慕课专栏
更多

添加回答

举报

0/150
提交
取消
意见反馈 帮助中心 APP下载
官方微信