如何按顺序在单元测试中执行 Go 函数?

huangapple go评论90阅读模式
英文:

How to execute Go functions by order in Unit Test?

问题

我是新手,想为这个小型API编写一个单元测试:

  • 登录请求。
  • 注销请求。
    我希望它们按顺序执行,并且两个请求都成功。

然而,当我执行TestAPI时,最后一个断言总是错误的,并告诉我当前用户(abc@abc)未登录。我知道它们是并行运行的(因此在处理logout请求时,后端cookie尚未存储此用户名),但我不知道如何重写代码,以便login请求始终发生在logout请求之前。

我不想浪费您的时间,但我已经在谷歌上搜索了很长时间,但没有找到适用于我的情况的解决方案。
非常感谢您的帮助!

func PostJson(uri string, param map[string]string, router *gin.Engine) *httptest.ResponseRecorder {
	jsonByte, _ := json.Marshal(param)
	req := httptest.NewRequest("POST", uri, bytes.NewReader(jsonByte))
	w := httptest.NewRecorder()
	router.ServeHTTP(w, req)
	return w
}

func TestAPI(t *testing.T) {
	server := setUpServer()
	var w *httptest.ResponseRecorder

	param := make(map[string]string)
	param["email"] = "abc@abc"
	param["password"] = "123"

	param2 := make(map[string]string)
	param2["email"] = "abc@abc"

	urlLogin := "/login"
	w = PostJson(urlLogin, param, server)
	assert.Equal(t, 200, w.Code)
	assert.Equal(t, w.Body.String(), "{\"msg\":\"login success\",\"status\":\"success\"}")

	urlLogout := "/logout"
	w = PostJson(urlLogout, param2, server)
	assert.Equal(t, 200, w.Code)
	assert.Equal(t, w.Body.String(), "{\"msg\":\"logout success\",\"status\":\"success\"}")
}
英文:

I am new to Go and want to write a unit test for this small API:

  • login request.
  • logout request.
    I expect them to be executed by order and both requests are successful.

However, when I execute the TestAPI, the last assert is always wrong and tells me the current user (abc@abc) is not logged in. I know they run in parallel (thus when handling logout request, the backend cookie hasn't stored this username yet) but I don't know how to rewrite so that the login request always happens before the logout request.

I don't want to waste your time, but I did google it for quite a while but found no solution for my case.
Many thanks for your help!

func PostJson(uri string, param map[string]string, router *gin.Engine) *httptest.ResponseRecorder {
	jsonByte, _ := json.Marshal(param)
	req := httptest.NewRequest("POST", uri, bytes.NewReader(jsonByte))
	w := httptest.NewRecorder()
	router.ServeHTTP(w, req)
	return w
}

func TestAPI(t *testing.T) {
	server := setUpServer()
	var w *httptest.ResponseRecorder

	param := make(map[string]string)
	param["email"] = "abc@abc"
	param["password"] = "123"

	param2 := make(map[string]string)
	param2["email"] = "abc@abc"

	urlLogin := "/login"
	w = PostJson(urlLogin, param, server)
	assert.Equal(t, 200, w.Code)
	assert.Equal(t, w.Body.String(), "{\"msg\":\"login success\",\"status\":\"success\"}")

	urlLogout := "/logout"
	w = PostJson(urlLogout, param2, server)
	assert.Equal(t, 200, w.Code)
	assert.Equal(t, w.Body.String(), "{\"msg\":\"logout success\",\"status\":\"success\"}")

答案1

得分: 2

单个测试运行中的语句按顺序执行

我知道它们是并行运行的

单个测试中的语句是按顺序执行的。这些语句都在同一个顶级的TestAPI测试函数中,所以它们按顺序运行。

关于Cookies

我知道它们是并行运行的(因此在处理注销请求时,后端的cookie尚未存储该用户名)

Cookies由前端持有,其内容不可信

并不存在所谓的“后端”cookie。Cookie是后端的响应头,客户端在后续请求的请求头中包含该cookie。

这意味着客户端对其cookie的内容具有完全控制权。通常,cookie包含一个无法猜测的随机值,用于标识用户,并与后端的用户数据关联在一起,存储在后端的会话存储中。

如果你真的将用户名放在cookie中,并且信任该用户名,那么你的应用程序的安全性很容易被绕过(我只需在发出请求之前在我的cookie中设置任何用户的名称)。

Gin应该提供一个安全的会话管理系统,可以使用类似Redis的后端进行支持。确保你正在使用它,而不是真的将用户名放在cookie中。

你的HTTP客户端有责任在后续请求中包含先前响应的cookie。你没有做任何事情来将登录响应的cookie包含在注销请求的cookie中。因此,注销请求是“未登录”的。

你需要一个“cookie jar”,这是一个常用术语,用于存储和包含后续请求的cookie。可以使用https://pkg.go.dev/net/http/cookiejar中提供的机制来创建一个http.Client,但你实际上没有使用http.Client,而是直接在请求上调用了你的服务器处理程序。

简单选项:让http.Client为你处理请求的cookie:

重构你的测试,使用testhttp运行一个“真实”的HTTP服务器,然后对该服务器进行“真实”的请求。

使用https://pkg.go.dev/net/http/httptest#Server在本地主机上启动一个测试HTTP服务器,并使用你从路由器提供的处理程序。

创建一个带有cookie jar(https://pkg.go.dev/net/http#Client.Jar)的http.Client,然后简单地重用该HTTP客户端进行请求,包括cookie。

https://pkg.go.dev/net/http/cookiejar#example-New中有一个很好的示例,展示了如何实现这一点。

更多工作:在你的情况下使用ResponseRecorder和直接调用HTTP响应处理程序来存储cookie

如果你想要做http.Client可以为你做的事情,可以在测试级别创建一个cookie jar。

jar, err := cookiejar.New(&cookiejar.Options{PublicSuffixList: publicsuffix.List})
if err != nil {
    t.Fail(err)
}

在每个请求中包含cookie jar中的cookie,并将响应中的cookie添加到jar中。可以将jar作为附加参数传递给你的PostJson代码。

在发出请求之前,使用https://pkg.go.dev/net/http#CookieJar.Cookies获取jar在该URL上的cookie。然后遍历cookie列表,并对每个cookie调用req的https://pkg.go.dev/net/http#Request.AddCookie。

然后像现在一样“发出”请求。

在返回响应后,你必须将cookie添加到cookie jar中。

你可以通过在响应记录器上使用https://pkg.go.dev/net/http/httptest#ResponseRecorder.Result来访问响应cookie,它将返回一个*http.Response。然后可以使用https://pkg.go.dev/net/http#Response.Cookies在该响应上调用。

使用https://pkg.go.dev/net/http#CookieJar.SetCookies将这些cookie添加到jar中。

代码可能如下所示:

func PostJson(uri string, param map[string]string, router *gin.Engine, jar http.CookieJar) *httptest.ResponseRecorder {
    jsonByte, _ := json.Marshal(param)
    req := httptest.NewRequest("POST", uri, bytes.NewReader(jsonByte))
    for _, cookie := range jar.Cookies(uri) {
        req.AddCookie(cookie)
    }
    w := httptest.NewRecorder()
    router.ServeHTTP(w, req)
    jar.SetCookies(uri, w.Result().Cookies())
    return w
}

关于在问题中提供可重现代码的价值的最后一点说明。

如果你能够提供一个我可以直接运行的代码,而不需要进行重大修改,我会花时间实现和测试我的建议更改,然后给你一个可工作的示例。所以下次当你有一个好的Stack Overflow问题时,花时间投入到能够让我们运行的完整代码中,也许可以在Go Playground共享中运行。除了更容易提供高质量的答案之外,这对你来说也是很好的练习!

英文:

Statements within a single test run sequentially

> I know they run in parallel

The statements within a single test run sequentially. Those statements are all in the same top level TestAPI test function, so they run sequentially.

On Cookies

> I know they run in parallel (thus when handling logout request, the backend cookie hasn't stored this username yet)

Cookies are held by the frontend, and their content can't be trusted

There is no such thing as a "backend" cookie. A cookie is a response header from the backend that the client includes in the request headers of subsequent requests.

The implication of this is that clients have full control over the content of their cookies. Generally cookies contain an unguessable random value to identify the user, which is associated with user data in backend session storage.

If you really are putting the username in the cookie, and then trusting that username, your application's security is trivial to bypass (I just set the name of whatever user I'd like to be in my cookie before making a request).

Gin should supply a secure session management system, maybe backed by something like redis. Make sure you're using it, and not really putting user names in cookies.

Your http client bears the responsibility of including cookies from prior responses in subsequent requests. You're doing nothing to include the response cookie from the login, within the request cookie from the logout. Thus the logout request is "not logged in".

What you need is a "cookie jar," a common term for a mechanism to store and include cookies for subsequent requests. One is provided by https://pkg.go.dev/net/http/cookiejar and can be added to an http.Client, but you're not actually using an http.Client - you're calling your server handler on the request directly.

Simple option: let http.Client handle request cookies for you:

Refactor your test to run a "real" http server with testhttp and then make
"real" requests against the server.

Use https://pkg.go.dev/net/http/httptest#Server to start a test http server on localhost, with the handler you provide from you router.

Create an http.Client with a jar (https://pkg.go.dev/net/http#Client.Jar)

Then simply reuse the http client to make the requests, cookies included.

https://pkg.go.dev/net/http/cookiejar#example-New has a good example of how this could be done.

More Work: store cookies in your case, with ResponseRecorder and a direct c all to the http response handler

If you want to do what http.Client could do for you, create a cookie jar at the test level.

jar, err := cookiejar.New(&cookiejar.Options{PublicSuffixList: publicsuffix.List})
if err != nil {
t.Fail(err)
}

Include the cookies from the jar in every request and return add the cookies from the response to the jar. The obvious place to do this is your PostJson code, to which the jar could be passed as an additional argument.

Before you make the request, get the jar's cookies for the url with https://pkg.go.dev/net/http#CookieJar.Cookies. Then add those cookies to the request by iterating over the list of cookies and calling req's https://pkg.go.dev/net/http#Request.AddCookie for each.

Then "make" the request as you do now.

After the response is returned, you must add the cookies to the cookie jar.

You can access response cookies via https://pkg.go.dev/net/http/httptest#ResponseRecorder.Result on your response recorder, which will return an * http.Response. You can then call https://pkg.go.dev/net/http#Response.Cookies on that response.

Add those cookies to the jar with https://pkg.go.dev/net/http#CookieJar.SetCookies.

The code might look something like:

func PostJson(uri string, param map[string]string, router *gin.Engine, jar http.CookieJar) *httptest.ResponseRecorder {
jsonByte, _ := json.Marshal(param)
req := httptest.NewRequest("POST", uri, bytes.NewReader(jsonByte))
for _, cookie := range jar.Cookies(uri) {
req.AddCookie(cookie)
}
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
jar.SetCookies(uri, w.Result().Cookies())
return w
}

A final note on the value of reproducible code in questions.

If you would have made your code something I could run without significant modification, I would have spent time implementing and testing my suggested changes, and then I could give you a working example. So next time you have a good stack over flow question, take the time to invest in complete code that we can run, maybe in a Go Playground share. In addition to making it a lot easier to provide a high qualityu answer, it's also excellent practice for you!

huangapple
  • 本文由 发表于 2022年2月23日 09:03:06
  • 转载请务必保留本文链接:https://go.coder-hub.com/71230190.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定