测试代码和测试代码

在动态语言(例如python和javascript)中,可以在操作期间直接替换模块中的方法和类。 这对于测试非常方便-您可以简单地放置“补丁”,以在此测试的上下文中排除繁琐或不必要的逻辑。


但是用C ++做什么? 去吗 Java的? 在这些语言中,无法即时修改代码以进行测试,并且创建补丁需要单独的工具。


在这种情况下,您应该专门编写代码以便对其进行测试。 这不仅仅是在项目中看到100%覆盖率的狂热愿望。 这是朝着编写受支持的高质量代码迈出的一步。


在本文中,我将尝试讨论编写可测​​试代码背后的主要思想,并通过一个简单的go程序示例演示如何使用它们。


简单的程序


我们将编写一个简单的程序来向VK API发出请求。 这是一个相当简单的程序,可以生成请求,进行请求,读取响应,将JSON中的响应解码为结构并将结果显示给用户。


package main import ( "encoding/json" "fmt" "io/ioutil" "net/http" "net/url" ) const token = "token here" func main() { //     var requestURL = fmt.Sprintf( "https://api.vk.com/method/%s?&access_token=%s&v=5.95", "users.get", token, ) //   resp, err := http.PostForm(requestURL, nil) //   if err != nil { fmt.Println(err) return } //       defer resp.Body.Close() //     body, err := ioutil.ReadAll(resp.Body) //   if err != nil { fmt.Println(err) return } //      var result struct { Response []struct { ID int `json:"id"` FirstName string `json:"first_name"` LastName string `json:"last_name"` } `json:"response"` } //        err = json.Unmarshal(body, &result) //   if err != nil { fmt.Println(err) return } // ,    if len(result.Response) < 1 { fmt.Println("No values in response array") return } //    fmt.Printf( "Your id: %d\nYour full name: %s %s\n", result.Response[0].ID, result.Response[0].FirstName, result.Response[0].LastName, ) } 

作为我们领域的专业人士,我们认为有必要为我们的应用编写测试。 创建一个测试文件...


 package main import ( "testing" ) func Test_Main(t *testing.T) { main() } 

看起来不太吸引人。 此检查是我们无法影响的应用程序的简单启动。 我们不能排除与网络的合作,不能检查各种错误的可操作性,甚至无法替换令牌进行验证。 让我们尝试找出如何改进此程序。


依赖注入模式


首先,您需要实现“依赖注入”模式


 type VKClient struct { Token string } func (client VKClient) ShowUserInfo() { var requestURL = fmt.Sprintf( "https://api.vk.com/method/%s?&access_token=%s&v=5.95", "users.get", client.Token, ) // ... } 

添加了结构之后,我们为应用程序创建了一个依赖项(访问密钥),可以从不同的来源进行转移,这使我们避免了连线值并简化了测试。


 package example import ( "testing" ) const workingToken = "workingToken" func Test_ShowUserInfo_Successful(t *testing.T) { client := VKClient{workingToken} client.ShowUserInfo() } func Test_ShowUserInfo_EmptyToken(t *testing.T) { client := VKClient{""} client.ShowUserInfo() } 

接收信息及其输出的分离


现在只有一个人可以犯一个错误,只有在他知道结论应该是什么的情况下才可以犯错。 为了解决此问题,有必要不要直接将信息输出到输出流,而要添加获取信息及其输出的单独方法。 这两个独立的部分将更易于验证和维护。


让我们创建GetUserInfo()方法,该方法将返回带有用户信息和错误(如果发生)的结构。 由于此方法不输出任何内容,因此发生的错误将在不输出的情况下进一步发送,因此需要数据的代码可以确定情况。


 type UserInfo struct { ID int `json:"id"` FirstName string `json:"first_name"` LastName string `json:"last_name"` } func (client VKClient) GetUserInfo() (UserInfo, error) { var requestURL = fmt.Sprintf( "https://api.vk.com/method/%s?&access_token=%s&v=5.95", "users.get", client.Token, ) resp, err := http.PostForm(requestURL, nil) if err != nil { return UserInfo{}, err } // ... var result struct { Response []UserInfo `json:"response"` } // ... return result.Response[0], nil } 

更改ShowUserInfo() ,使其使用GetUserInfo()并处理错误。


 func (client VKClient) ShowUserInfo() { userInfo, err := client.GetUserInfo() if err != nil { fmt.Println(err) return } fmt.Printf( "Your id: %d\nYour full name: %s %s\n", userInfo.ID, userInfo.FirstName, userInfo.LastName, ) } 

现在,在测试中,您可以验证是否已从服务器收到正确的答案,并且如果令牌不正确,则会返回错误。


 func Test_GetUserInfo_Successful(t *testing.T) { client := VKClient{workingToken} userInfo, err := client.GetUserInfo() if err != nil { t.Fatal(err) } if userInfo.ID == 0 { t.Fatal("ID is empty") } if userInfo.FirstName == "" { t.Fatal("FirstName is empty") } if userInfo.LastName == "" { t.Fatal("LastName is empty") } } func Test_ShowUserInfo_EmptyToken(t *testing.T) { client := VKClient{""} _, err := client.GetUserInfo() if err == nil { t.Fatal("Expected error but found <nil>") } if err.Error() != "No values in response array" { t.Fatalf(`Expected "No values in response array", but found "%s"`, err) } } 

除了更新现有测试之外,您还需要为ShowUserInfo()方法添加新测试。


 func Test_ShowUserInfo(t *testing.T) { client := VKClient{workingToken} client.ShowUserInfo() } func Test_ShowUserInfo_WithError(t *testing.T) { client := VKClient{""} client.ShowUserInfo() } 

定制替代品


ShowUserInfo()测试类似于我们最初试图摆脱的测试。 在这种情况下,该方法的唯一目的是将信息输出到标准输出流。 一方面,您可以尝试重新定义os.Stdout并检查输出,当您可以更优雅地操作时,它似乎是一个多余的解决方案。


可以使用fmt.Fprintf代替使用fmt.Fprintf ,它可以输出到任何io.Writeros.Stdout实现了此接口,该接口使我们能够将fmt.Printf(text)替换为fmt.Fprintf(os.Stdout, text) 。 之后,我们可以将os.Stdout放在一个单独的字段中,可以将其设置为所需的值(用于测试-字符串,用于工作-标准输出流)。


由于更改输出Writer的功能很少使用(主要用于测试),因此设置默认值是有意义的。 接下来,我们将执行此操作-使VKClient类型VKClient导出,并为其创建构造函数。


 type vkClient struct { Token string OutputWriter io.Writer } func CreateVKClient(token string) vkClient { return vkClient{ token, os.Stdout, } } 

ShowUserInfo()函数中,我们用Fprintf替换Print调用。


 func (client vkClient) ShowUserInfo() { userInfo, err := client.GetUserInfo() if err != nil { fmt.Fprintf(client.OutputWriter, err.Error()) return } fmt.Fprintf( client.OutputWriter, "Your id: %d\nYour full name: %s %s\n", userInfo.ID, userInfo.FirstName, userInfo.LastName, ) } 

现在,您需要更新测试,以便它们使用构造函数创建客户端,并在必要时安装另一个Writer。


 func Test_ShowUserInfo(t *testing.T) { client := CreateVKClient(workingToken) buffer := bytes.NewBufferString("") client.OutputWriter = buffer client.ShowUserInfo() result, _ := ioutil.ReadAll(buffer) matched, err := regexp.Match( `Your id: \d+\nYour full name: [^\n]+\n`, result, ) if err != nil { t.Fatal(err) } if !matched { t.Fatalf(`Expected match but failed with "%s"`, result) } } func Test_ShowUserInfo_WithError(t *testing.T) { client := CreateVKClient("") buffer := bytes.NewBufferString("") client.OutputWriter = buffer client.ShowUserInfo() result, _ := ioutil.ReadAll(buffer) if string(result) != "No values in response array" { t.Fatal("Wrong error") } } 

对于输出某些内容的每个测试,我们创建一个缓冲区,该缓冲区将充当标准输出流的角色。 函数执行后,在正则表达式或简单比较的帮助下,检查结果是否符合我们的期望。


为什么使用正则表达式? 为了使测试能够使用我将提供给程序的任何有效令牌,无论用户名和用户ID如何。


依赖注入模式-2


目前,该计划的覆盖率为86.4%。 为什么不100%? 我们无法从http.PostForm()ioutil.ReadAll()json.Unmarshal()引发错误,这意味着我们无法检查每个“ return UserInfo, err ”。


为了使自己对情况有更多的控制,您需要创建一个接口, http.Client将在其中适合该接口,该接口的实现将在vkClient中进行,并用于网络操作。 对于我们来说,在界面中,只有一种方法很PostForm


 type Networker interface { PostForm(string, url.Values) (*http.Response, error) } type vkClient struct { Token string OutputWriter io.Writer Networker Networker } func CreateVKClient(token string) vkClient { return vkClient{ token, os.Stdout, &http.Client{}, } } 

这样的举动消除了一般执行网络操作的需要。 现在,我们可以使用伪造的Networker简单地从VKontakte返回期望的数据。 当然,不要放弃将检查对服务器的请求的测试,但是没有必要在每个测试中都发出请求。


我们将为伪造的NetworkerReader创建实现,以便我们可以在每种情况下(应要求,在读取正文和反序列化期间)测试错误。 如果在调用PostForm时需要一个错误,则只需在此方法中将其返回即可。 如果我们想要一个错误
在读取响应主体时-必须返回假的Reader ,这将引发错误。 并且,如果我们需要错误来在反序列化过程中表现出来,那么我们将在正文中返回包含空字符串的答案。 如果我们不希望出现任何错误,则只需返回具有指定内容的主体。


 type fakeReader struct{} func (fakeReader) Read(p []byte) (n int, err error) { return 0, errors.New("Error on read") } type fakeNetworker struct { ErrorOnPostForm bool ErrorOnBodyRead bool ErrorOnUnmarchal bool RawBody string } func (fn *fakeNetworker) PostForm(string, url.Values) (*http.Response, error) { if fn.ErrorOnPostForm { return nil, fmt.Errorf("Error on PostForm") } if fn.ErrorOnBodyRead { return &http.Response{Body: ioutil.NopCloser(fakeReader{})}, nil } if fn.ErrorOnUnmarchal { fakeBody := ioutil.NopCloser(bytes.NewBufferString("")) return &http.Response{Body: fakeBody}, nil } fakeBody := ioutil.NopCloser(bytes.NewBufferString(fn.RawBody)) return &http.Response{Body: fakeBody}, nil } 

对于每种问题情况,我们添加一个测试。 他们将使用必要的设置创建伪造的Networker ,据此,他将在特定点抛出错误。 此后,我们调用要检查的函数,并确保发生错误,并且我们预期该错误。


 func Test_GetUserInfo_ErrorOnPostForm(t *testing.T) { client := CreateVKClient(workingToken) client.Networker = &fakeNetworker{ErrorOnPostForm: true} _, err := client.GetUserInfo() if err == nil { t.Fatal("Expected error but none found") } if err.Error() != "Error on PostForm" { t.Fatalf(`Expected "Error on PostForm" but got "%s"`, err.Error()) } } func Test_GetUserInfo_ErrorOnBodyRead(t *testing.T) { client := CreateVKClient(workingToken) client.Networker = &fakeNetworker{ErrorOnBodyRead: true} _, err := client.GetUserInfo() if err == nil { t.Fatal("Expected error but none found") } if err.Error() != "Error on read" { t.Fatalf(`Expected "Error on read" but got "%s"`, err.Error()) } } func Test_GetUserInfo_ErrorOnUnmarchal(t *testing.T) { client := CreateVKClient(workingToken) client.Networker = &fakeNetworker{ErrorOnUnmarchal: true} _, err := client.GetUserInfo() if err == nil { t.Fatal("Expected error but none found") } const expectedError = "unexpected end of JSON input" if err.Error() != expectedError { t.Fatalf(`Expected "%s" but got "%s"`, expectedError, err.Error()) } } 

使用RawBody字段, RawBody可以摆脱网络请求(只需返回我们期望从VKontakte收到的内容)。 为避免在测试期间超出查询限制或加快测试速度,这可能是必要的。


总结


在项目上完成所有操作后,我们收到了一个长度为91行(+170行测试)的软件包,该软件包支持将输出输出到任何io.Writer ,使您可以使用替代方法来使用网络(使用接口的适配器),其中有一种类似的方法输出数据并获取它们。 该项目具有100%的覆盖率。 测试全面检查每一行,并对每个可能的错误对应用程序做出响应。


达到100%覆盖率的每一步都提高了应用程序的模块化,可维护性和可靠性,因此测试决定了包装的结构这一事实没有错。


任何代码的可测试性都是一种不可能出现的质量。 当开发人员在适当的情况下适当使用模式并编写自定义和模块化代码时,就出现了可测试性。 主要任务是展示执行重构程序时的思考过程。 类似的想法可以扩展到任何应用程序和库,以及其他语言。

Source: https://habr.com/ru/post/zh-CN452702/


All Articles