第八章 go单元测试--表格驱动测试, 性能测试

简介: go语言单元测试写法1. 文件命令, 测试内容+ _test2. 测试的方法名以Test开头. 参数为(test *Test)3. 测试文件和源文件放在同一个目录中

一. go语言单元测试写法


1. 文件命令, 测试内容+ _test


2. 测试的方法名以Test开头. 参数为(test *Test)


3. 测试文件和源文件放在同一个目录中


例:


package TestingDebug
func Add(a, b int) int {
    return b + a
}


package TestingDebug
import (
    "fmt"
    "testing"
)
func TestAdd(t *testing.T) {
    var test = []struct{
        a, b, c int
    }{
        {1, 2, 3},
        {3, 5, 8},
        {2, 4, 5},
    }
    for _, tt := range test  {
        if  actural := Add(tt.a, tt.b) ; actural != tt.c {
            fmt.Printf("%d + %d, except:%d, actual:%d", tt.a, tt.b, tt.c, actural)
        }
    }
}


需要注意几点


1. 同目录下文件相互调用, 不需要加包名


2. 测试文件与源文件在同一个目录下,  否则技术不出来测试代码覆盖率.

 

二. 如何测试代码覆盖率


微信图片_20220510125354.png


微信图片_20220510125357.png


然后点击TestingDebug进入文件夹, 点击测试的文件. 看右侧绿线, 表示测试覆盖的代码. 这里覆盖了100%


微信图片_20220510125400.png


三. 性能测试BenchMark


性能测试使用Testing.B


比如, 我们想要测试一个复杂一些的数据的累加, 看看性能如何.

 

/**
 * 性能测试
 */
func BenchmarkAdd(bb *testing.B) {
    var a, b, c int
    a = 123
    b = 4557
    c = 4680
    for i := 0; i<bb.N ; i++  {
        if  actural := Add(a, b) ; actural != c {
            fmt.Printf("%d + %d, except:%d, actual:%d", a, b, c, actural)
        }
    }
}


bb.N表示的是系统自动计算的一个循环次数, 我们不用自己指定.


goos: darwin
goarch: amd64
pkg: aaa/TestingDebug
BenchmarkAdd-8       1000000000             0.317 ns/op
PASS


以上是测试结果. 1000000000 代表测试的次数是10亿次. 0.317 ns/op每个操作执行的时间是0.317ns


四. 性能调优--使用pprof进行性能优化


如上一步, 我们对代码进行了性能测试. 每个操作执行时间是0.317ns. 那么, 假如有一个稍微复杂一些的例子, 我如何看出花费了这么多时间, 都是在哪个步骤花的? 哪个步骤耗费时间最多呢?


我们使用命令来看


go test -bench . -cpuprofile cpu.out


执行上面的命令生成一个cpu.out的文件, 查看cpu运行的情况


微信图片_20220510125512.png


我们使用less查看文件: less cpu.out


微信图片_20220510125532.png


cpu.out是一个二进制文件,我们没办法打开. 我们可以使用命令打开


go tool pprof cpu.out


微信图片_20220510125556.png


这里我们可以通过pprof的web视图来查看到底那个步骤耗时最多. 但是看红色字, 查看web视图,需要安装一个软件Graphviz


安装Graphviz, 下载地址: http://www.graphviz.org/. 我是mac, 下载一个mac版本的:

下载命令: brew install graphviz


安装成功以后, 数据命令 go tool pprof cpu.out , 然后输入web , 会生产一个svg文件, 用浏览器打开我们就会看到效果了微信图片_20220510125635.png这就是对简单程序的一个分析. 他在每个阶段用时是多少. 因为我们这里是很简单的一个demo, 所以耗时很少了.  

 

五. 测试http服务器


http测试分为两种


1. 通过使用假的Request/Response实现 : 运行速度更快, 可以测试更多种情况, 更像单元测试.


2. 通过启服务器: 模拟的诊室服务, 覆盖代码更多.


微信图片_20220510125653.png


1. 使用假的Request和Response模拟http请求实现单元测试


分析:


首先: 明确我们的目标, 要对谁做测试. 我们要对封装的错误处理进行测试.


第二: 测试有输入和输出. 预计输入输出, 真实输入输出. 我们这里输入是谁呢?通过包装类WrapError分析, 入参是对http请求业务逻辑的处理, 出参是包装的错误处理, 包括错误码和错误消息


所以, 单元测试的表格的结构体怎么写呢?


test := []struct{
    h appHandler     // 入参
    Code int        // 出参
    Message string    // 出参
} {
}


入参是一个appHandler函数. 因此我们定义一个函数,来模拟返回的错误信息. 异常的种类有很多,先定义一个panic


func handlerPanic(writer http.ResponseWriter, request *http.Request) error {
    panic("panic error")
}


接下来模拟http请求, 判断异常处理是否正确, 完整代码如下:


package main
import (
    "io/ioutil"
    "net/http"
    "net/http/httptest"
    "strings"
    "testing"
)
func handlerPanic(writer http.ResponseWriter, request *http.Request) error {
    panic("panic error")
}
func TestWrapError(t *testing.T) {
    // 我们测试的异常的封装方法 那么对于单元测试来说, 无非就是入参和出参
    test := []struct{
        h appHandler     // 入参
        Code int        // 出参
        Message string    // 出参
    } {
        {handlerPanic, 500, "Internal Server Error"},
    }
    for _, tt := range test {
        // 要测试的方法WrapError
        f := WrapError(tt.h)
        // 模拟Request和response
        response := httptest.NewRecorder()
        request := httptest.NewRequest(http.MethodGet, "http://www.baidu.com", nil)
        f(response, request)
        //读取reponse返回的body
        b, _ := ioutil.ReadAll(response.Body)
        body := strings.Trim(string(b), "\n")
        // 测试返回值和预期是否一致
        if tt.Code != response.Code || tt.Message != body {
            t.Errorf("预期返回--code:%d, message:%s \n 实际返回--code:%d, message:%s", tt.Code,
                tt.Message, response.Code, body)
        }
    }
}


测试用户自定义异常, 我们在测试用户自定义信息


package main
import (
    "io/ioutil"
    "net/http"
    "net/http/httptest"
    "strings"
    "testing"
)
//用户自定义异常
type testUserError string
func (u testUserError) Error() string{
    return u.Message()
}
func (u testUserError) Message() string {
    return string(u)
}
func handlerUserError(writer http.ResponseWriter, request *http.Request) error {
    return testUserError("user error")
}
func handlerPanic(writer http.ResponseWriter, request *http.Request) error {
    panic("panic error")
}
func TestWrapError(t *testing.T) {
    // 我们测试的异常的封装方法 那么对于单元测试来说, 无非就是入参和出参
    test := []struct{
        h appHandler     // 入参
        Code int        // 出参
        Message string    // 出参
    } {
        {handlerPanic, 500, "Internal Server Error"},
        {handlerUserError, 400, "user error"},
    }
    for _, tt := range test {
        // 要测试的方法WrapError
        f := WrapError(tt.h)
        // 模拟Request和response
        response := httptest.NewRecorder()
        request := httptest.NewRequest(http.MethodGet, "http://www.baidu.com", nil)
        f(response, request)
        //读取reponse返回的body
        b, _ := ioutil.ReadAll(response.Body)
        body := strings.Trim(string(b), "\n")
        // 测试返回值和预期是否一致
        if tt.Code != response.Code || tt.Message != body {
            t.Errorf("预期返回--code:%d, message:%s \n 实际返回--code:%d, message:%s", tt.Code,
                tt.Message, response.Code, body)
        }
    }
}


接下来查看代码覆盖率 35%. 接下来补全测试代码, 提高代码覆盖率


package main
import (
    "github.com/pkg/errors"
    "io/ioutil"
    "net/http"
    "net/http/httptest"
    "os"
    "strings"
    "testing"
)
type testUserError string
func (u testUserError) Error() string{
    return u.Message()
}
func (u testUserError) Message() string {
    return string(u)
}
func handlerUserError(writer http.ResponseWriter, request *http.Request) error {
    return testUserError("user error")
}
func handlerPanicError(writer http.ResponseWriter, request *http.Request) error {
    panic("panic error")
}
func handlerNotFountError(writer http.ResponseWriter, request *http.Request) error {
    return os.ErrNotExist
}
func handlerForbiddenError(writer http.ResponseWriter, request *http.Request) error {
    return os.ErrPermission
}
func otherError(writer http.ResponseWriter, request *http.Request) error {
    return errors.New("123")
}
func noError(writer http.ResponseWriter, request *http.Request) error {
    return nil
}
func TestWrapError(t *testing.T) {
    // 我们测试的异常的封装方法 那么对于单元测试来说, 无非就是入参和出参
    test := []struct{
        h appHandler     // 入参
        Code int        // 出参
        Message string    // 出参
    } {
        {handlerPanicError, 500, "Internal Server Error"},
        {handlerUserError, 400, "user error"},
        {handlerNotFountError, 404, "Not Found"},
        {handlerForbiddenError, 403, "Forbidden"},
        {otherError, 500, "Internal Server Error"},
        {noError, 200, ""},
    }
    for _, tt := range test {
        // 要测试的方法WrapError
        f := WrapError(tt.h)
        // 模拟Request和response
        response := httptest.NewRecorder()
        request := httptest.NewRequest(http.MethodGet, "http://www.baidu.com", nil)
        f(response, request)
        //读取reponse返回的body
        b, _ := ioutil.ReadAll(response.Body)
        body := strings.Trim(string(b), "\n")
        // 测试返回值和预期是否一致
        if tt.Code != response.Code || tt.Message != body {
            t.Errorf("预期返回--code:%d, message:%s \n 实际返回--code:%d, message:%s", tt.Code,
                tt.Message, response.Code, body)
        }
    }
}


再次查看代码覆盖率


微信图片_20220510125824.png


达到了80%, 只有最后的main方法没有被覆盖. ok了


2. 使用真实的http请求进行测试


// 真实http请求
func TestWrapErrorForServer(t *testing.T) {
    for _, tt := range test  {
        // 要测试的方法WrapError
        f := WrapError(tt.h)
        s := httptest.NewServer(http.HandlerFunc(f))
        // 这里url不用填自己的, httptest在new server的时候帮我们定义好了一个url
        response, _ := s.Client().Get(s.URL)
        b, _ := ioutil.ReadAll(response.Body)
        body := strings.Trim(string(b), "\n")
        if tt.Code != response.StatusCode || tt.Message != body {
            t.Errorf("except--code: %d, message: %s \n actual--code:%d, message:%s",
                tt.Code, tt.Message, response.StatusCode, body)
        }
    }
}


模拟数据的部分, 两种类型的http请求是一样的, 被提取到外面了, 最终完整代码如下:


package main
import (
    "github.com/pkg/errors"
    "io/ioutil"
    "net/http"
    "net/http/httptest"
    "os"
    "strings"
    "testing"
)
type testUserError string
func (u testUserError) Error() string{
    return u.Message()
}
func (u testUserError) Message() string {
    return string(u)
}
func handlerUserError(writer http.ResponseWriter, request *http.Request) error {
    return testUserError("user error")
}
func handlerPanicError(writer http.ResponseWriter, request *http.Request) error {
    panic("panic error")
}
func handlerNotFountError(writer http.ResponseWriter, request *http.Request) error {
    return os.ErrNotExist
}
func handlerForbiddenError(writer http.ResponseWriter, request *http.Request) error {
    return os.ErrPermission
}
func otherError(writer http.ResponseWriter, request *http.Request) error {
    return errors.New("123")
}
func noError(writer http.ResponseWriter, request *http.Request) error {
    return nil
}
// 测试数据
var test = []struct{
    h appHandler     // 入参
    Code int        // 出参
    Message string    // 出参
} {
    {handlerPanicError, 500, "Internal Server Error"},
    {handlerUserError, 400, "user error"},
    {handlerNotFountError, 404, "Not Found"},
    {handlerForbiddenError, 403, "Forbidden"},
    {otherError, 500, "Internal Server Error"},
    {noError, 200, ""},
}
// 模拟http请求
func TestWrapError(t *testing.T) {
    for _, tt := range test {
        // 要测试的方法WrapError
        f := WrapError(tt.h)
        // 模拟Request和response
        response := httptest.NewRecorder()
        request := httptest.NewRequest(http.MethodGet, "http://www.baidu.com", nil)
        f(response, request)
        //读取reponse返回的body
        verify(response.Result(), tt, t)
    }
}
// 真实http请求
func TestWrapErrorForServer(t *testing.T) {
    for _, tt := range test  {
        // 要测试的方法WrapError
        f := WrapError(tt.h)
        s := httptest.NewServer(http.HandlerFunc(f))
        // 这里url不用填自己的, httptest在new server的时候帮我们定义好了一个url
        response, _ := s.Client().Get(s.URL)
        verify(response, tt, t)
    }
}
func verify(response *http.Response, tt struct {h appHandler;Code int;Message string}, t *testing.T) {
    b, _ := ioutil.ReadAll(response.Body)
    body := strings.Trim(string(b), "\n")
    if tt.Code != response.StatusCode || tt.Message != body {
        t.Errorf("except--code: %d, message: %s \n actual--code:%d, message:%s",
            tt.Code, tt.Message, response.StatusCode, body)
    }
}


package fileListener
import (
    "io/ioutil"
    "net/http"
    "os"
    "strings"
)
type UserError string
func (u UserError) Error() string{
    return u.Message()
}
func (u UserError) Message() string {
    return string(u)
}
func FileHandler(writer http.ResponseWriter, request *http.Request) error {
    // 获取url路径, 路径是/list/之后的部分
    if c := strings.Index(request.URL.Path, "/list/"); c != 0 {
        return UserError("url 不是已list开头")
    }
    path := request.URL.Path[len("/list/"):]
    // 打开文件
    file, err := os.Open(path)
    if err != nil {
        return err
    }
    defer file.Close()
    // 读出文件
    b, err := ioutil.ReadAll(file)
    if err != nil {
        return err
    }
    // 写入文件到页面
    writer.Write(b)
    return nil
}


package main
import (
    "aaa/errorhandling/filelistenerserver/fileListener"
    "github.com/kelseyhightower/confd/log"
    "net/http"
    "os"
)
// 定义一个函数类型的结构, 返回值是erro
type appHandler func(writer http.ResponseWriter, request *http.Request) error
// 封装error
func WrapError(handler appHandler) func (http.ResponseWriter, *http.Request) {
    return func(writer http.ResponseWriter, request *http.Request) {
        defer func(){
            if r := recover(); r != nil {
                log.Info("发生异常")
                http.Error(writer, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
            }
        }()
        // 执行原来的逻辑. 然后增加error的错误处理
        err := handler(writer, request)
        if err != nil {
            if userErr, ok := err.(userError); ok {
                http.Error(writer, userErr.Message(), http.StatusBadRequest)
                return
            }
            code := http.StatusOK
            switch {
            case os.IsNotExist(err):
                code = http.StatusNotFound
            case os.IsPermission(err):
                code = http.StatusForbidden
            default:
                code = http.StatusInternalServerError
            }
            http.Error(writer, http.StatusText(code), code)
        }
    }
}
type userError interface {
    error        // 系统异常
    Message() string    // 用户自定义异常
}
// 我们来模拟一个web服务器. 在url上输入一个地址, 然后显示文件内容
// 做一个显示文件的web server
func main() {
    http.HandleFunc("/", WrapError(fileListener.FileHandler))
    // 监听端口:8888
    err := http.ListenAndServe(":8888", nil)
    if  err != nil {
        panic("err")
    }
}


代码结构


微信图片_20220510125939.png

相关文章
|
4月前
|
测试技术 程序员 Go
Go语言测试简明指南:深度解读go test命令
总的来说,go test是 Go 语言中一个强而有力的工具,每个 Go 程序员都应该掌握并把它融入到日常的开发和调试过程中。就像是一个眼镜过滤出的太阳,让我们在宽阔的代码海洋中游泳,而不是淹没。用好它,让我们的代码更健壮,让我们的生产力更强效。
233 23
|
11月前
|
数据库连接 Go 数据库
Go语言中的错误注入与防御编程。错误注入通过模拟网络故障、数据库错误等,测试系统稳定性
本文探讨了Go语言中的错误注入与防御编程。错误注入通过模拟网络故障、数据库错误等,测试系统稳定性;防御编程则强调在编码时考虑各种错误情况,确保程序健壮性。文章详细介绍了这两种技术在Go语言中的实现方法及其重要性,旨在提升软件质量和可靠性。
168 1
|
11月前
|
测试技术 Go
go语言中测试工具
【10月更文挑战第22天】
152 4
|
11月前
|
测试技术 开发者 UED
探索软件测试的深度:从单元测试到自动化测试
【10月更文挑战第30天】在软件开发的世界中,测试是确保产品质量和用户满意度的关键步骤。本文将深入探讨软件测试的不同层次,从基本的单元测试到复杂的自动化测试,揭示它们如何共同构建一个坚实的质量保证体系。我们将通过实际代码示例,展示如何在开发过程中实施有效的测试策略,以确保软件的稳定性和可靠性。无论你是新手还是经验丰富的开发者,这篇文章都将为你提供宝贵的见解和实用技巧。
|
11月前
|
NoSQL 测试技术 Go
自动化测试在 Go 开源库中的应用与实践
本文介绍了 Go 语言的自动化测试及其在 `go mongox` 库中的实践。Go 语言通过 `testing` 库和 `go test` 命令提供了简洁高效的测试框架,支持单元测试、集成测试和基准测试。`go mongox` 库通过单元测试和集成测试确保与 MongoDB 交互的正确性和稳定性,使用 Docker Compose 快速搭建测试环境。文章还探讨了表驱动测试、覆盖率检查和 Mock 工具的使用,强调了自动化测试在开源库中的重要性。
248 0
|
IDE 测试技术 持续交付
Python自动化测试与单元测试框架:提升代码质量与效率
【9月更文挑战第3天】随着软件行业的迅速发展,代码质量和开发效率变得至关重要。本文探讨了Python在自动化及单元测试中的应用,介绍了Selenium、Appium、pytest等自动化测试框架,以及Python标准库中的unittest单元测试框架。通过详细阐述各框架的特点与使用方法,本文旨在帮助开发者掌握编写高效测试用例的技巧,提升代码质量与开发效率。同时,文章还提出了制定测试计划、持续集成与测试等实践建议,助力项目成功。
204 5
|
测试技术 C# 开发者
“代码守护者:详解WPF开发中的单元测试策略与实践——从选择测试框架到编写模拟对象,全方位保障你的应用程序质量”
【8月更文挑战第31天】单元测试是确保软件质量的关键实践,尤其在复杂的WPF应用中更为重要。通过为每个小模块编写独立测试用例,可以验证代码的功能正确性并在早期发现错误。本文将介绍如何在WPF项目中引入单元测试,并通过具体示例演示其实施过程。首先选择合适的测试框架如NUnit或xUnit.net,并利用Moq模拟框架隔离外部依赖。接着,通过一个简单的WPF应用程序示例,展示如何模拟`IUserRepository`接口并验证`MainViewModel`加载用户数据的正确性。这有助于确保代码质量和未来的重构与扩展。
633 0
|
测试技术 Java Spring
Spring 框架中的测试之道:揭秘单元测试与集成测试的双重保障,你的应用真的安全了吗?
【8月更文挑战第31天】本文以问答形式深入探讨了Spring框架中的测试策略,包括单元测试与集成测试的有效编写方法,及其对提升代码质量和可靠性的重要性。通过具体示例,展示了如何使用`@MockBean`、`@SpringBootTest`等注解来进行服务和控制器的测试,同时介绍了Spring Boot提供的测试工具,如`@DataJpaTest`,以简化数据库测试流程。合理运用这些测试策略和工具,将助力开发者构建更为稳健的软件系统。
177 0
|
测试技术 Java
全面保障Struts 2应用质量:掌握单元测试与集成测试的关键策略
【8月更文挑战第31天】Struts 2 的测试策略结合了单元测试与集成测试。单元测试聚焦于单个组件(如 Action 类)的功能验证,常用 Mockito 模拟依赖项;集成测试则关注组件间的交互,利用 Cactus 等框架确保框架拦截器和 Action 映射等按预期工作。通过确保高测试覆盖率并定期更新测试用例,可以提升应用的整体稳定性和质量。
157 0
|
测试技术 数据库
探索JSF单元测试秘籍!如何让您的应用更稳固、更高效?揭秘成功背后的测试之道!
【8月更文挑战第31天】在 JavaServer Faces(JSF)应用开发中,确保代码质量和可维护性至关重要。本文详细介绍了如何通过单元测试实现这一目标。首先,阐述了单元测试的重要性及其对应用稳定性的影响;其次,提出了提高 JSF 应用可测试性的设计建议,如避免直接访问外部资源和使用依赖注入;最后,通过一个具体的 `UserBean` 示例,展示了如何利用 JUnit 和 Mockito 框架编写有效的单元测试。通过这些方法,不仅能够确保代码质量,还能提高开发效率和降低维护成本。
119 0

热门文章

最新文章