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")
}
}
在这一点上,你可能已经认为这个小功能不好,如果你不这样做,我向你保证它不是。它做的太少,它只是包装了方法,它的测试对业务逻辑的生存没有多大兴趣。sendRequest
http.Get
继续运作。readBody
所有申请的评论也适用于此。sendRequest
它做得太少
它是
os.Exit
有一件事不适用。由于对 的调用不依赖于外部资源,因此尝试注入该依赖项毫无意义。我们可以四处测试。ioutil.ReadAll
但是,为了演示,现在是时候讨论缺少的呼叫了。defer resp.Body.Close()
让我们假设我们去介绍中提出的第二个命题并对此进行测试。
该结构充分地将其接收方公开为接口。http.Response
Body
为了确保代码调用'关闭,我们可以为它编写一个存根。该存根将记录是否进行了该调用,然后测试可以检查该调用,如果不是,则触发错误。
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
}
- 1 回答
- 0 关注
- 81 浏览
添加回答
举报