Go语言之单元测试

简介: 什么是单元测试相信我们做程序员的,对单元测试都不陌生。单元测试一般是用来测试我们的代码逻辑有没有问题,有没有按照我们期望的运行,以保证代码质量。大多数的单元测试,都是对某一个函数方法进行测试,以尽可能的保证没有问题或者问题可被我们预知。
什么是单元测试


相信我们做程序员的,对单元测试都不陌生。单元测试一般是用来测试我们的代码逻辑有没有问题,有没有按照我们期望的运行,以保证代码质量。


大多数的单元测试,都是对某一个函数方法进行测试,以尽可能的保证没有问题或者问题可被我们预知。为了达到这个目的,我们可以使用各种手段、逻辑,模拟不同的场景进行测试。


这里我们在package main里定义一个函数Add,求两个数之和的函数,然后我们使用单元测试进行求和逻辑测试。


main.go

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


main_test.go

func TestAdd(t *testing.T) {
    sum := Add(1,2)
    if sum == 3 {
        t.Log("the result is ok")
    } else {
        t.Fatal("the result is wrong")
    }
}


然后我们在终端的项目目录下运行go test -v就可以看到测试结果了。


  hello go test -v
=== RUN   TestAdd
--- PASS: TestAdd (0.00s)        main_test.go:26: the result is ok PASS ok      flysnow.org/hello       0.007s


有测试成功PASS标记,并且打印出我们想要的结果。


  • Go语言为我们提供了测试框架,以便帮助我们更容易的进行单元测试,但是要使用这个框架,需要遵循如下几点规则:


  • 含有单元测试代码的go文件必须以_test.go结尾,Go语言测试工具只认符合这个规则的文件。


  • 单元测试文件名_test.go前面的部分最好是被测试的方法所在go文件的文件名,比如例子中是main_test.go,因为测试的Add函数,在main.go文件里。


  • 单元测试的函数名必须以Test开头,是可导出公开的函数。


  • 测试函数的签名必须接收一个指向testing.T类型的指针,并且不能返回任何值。


  • 函数名最好是Test+要测试的方法函数名,比如例子中是TestAdd,表示测试的是Add这个这个函数。


遵循以上规则,我们就可以很容易的编写单元测试了,单元测试的重点在于测试代码的逻辑,场景等,以便尽可能的测试全面,保障代码质量逻辑。


表组测试


还有一种单元测试方法叫表组测试,这个和基本的单元测试非常相似,只不过它是有好几个不同的输入以及输出组成的一组单元测试。


比如上个例子中,我们测试了1+2,如果我们再加上3+49+2等,这就有了好几个输入,同时对应的也有好几个输出,这种一次性测试很多个输入输出场景的测试,就是表组测试。


func TestAdd(t *testing.T) {
    sum := Add(1,2)
    if sum == 3 {
        t.Log("the result is ok")
    } else {
        t.Fatal("the result is wrong")
    }

    sum=Add(3,4)    
    if sum == 7 {
        t.Log("the result is ok")
    } else {
        t.Fatal("the result is wrong")
    }
}


模拟调用


单元测试的原则,就是你所测试的函数方法,不要受到所依赖环境的影响,比如网络访问等,因为有时候我们运行单元测试的时候,并没有联网,那么总不能让单元测试因为这个失败吧?所以这时候模拟网络访问就有必要了。


针对模拟网络访问,标准库了提供了一个httptest包,可以让我们模拟http的网络调用,下面举个例子了解使用。


首先我们创建一个处理HTTP请求的函数,并注册路由。


package commonimport (
    "net/http"
    "encoding/json")func Routes(){
    http.HandleFunc("/sendjson",SendJSON)}func SendJSON(rw http.ResponseWriter,r *http.Request){
    u := struct {
        Name string
    }{
        Name:"张三",
    }

    rw.Header().Set("Content-Type","application/json")
    rw.WriteHeader(http.StatusOK)
    json.NewEncoder(rw).Encode(u)
}


非常简单,这里是一个/sendjsonAPI,当我们访问这个API时,会返回一个JSON字符串。现在我们对这个API服务进行测试,但是我们又不能时时刻刻都启动着服务,所以这里就用到了外部终端对API的网络访问请求。


func init()  {
    common.Routes()}func TestSendJSON(t *testing.T){
    req,err:=http.NewRequest(http.MethodGet,"/sendjson",nil)
    if err!=nil {
        t.Fatal("创建Request失败")
    }

    rw:=httptest.NewRecorder()
    http.DefaultServeMux.ServeHTTP(rw,req)

    log.Println("code:",rw.Code)

    log.Println("body:",rw.Body.String())
}


运行这个单元测试,就可以看到我们访问/sendjsonAPI的结果里,并且我们没有启动任何HTTP服务就达到了目的。这个主要利用httptest.NewRecorder()创建一个http.ResponseWriter,模拟了真实服务端的响应,这种响应时通过调用http.DefaultServeMux.ServeHTTP方法触发的。


还有一个模拟调用的方式,是真的在测试机上模拟一个服务器,然后进行调用测试。


func mockServer() *httptest.Server {
    //API调用处理函数
    sendJson := func(rw http.ResponseWriter, r *http.Request) {
        u := struct {
            Name string
        }{
            Name: "张三",
        }

        rw.Header().Set("Content-Type", "application/json")
        rw.WriteHeader(http.StatusOK)
        json.NewEncoder(rw).Encode(u)
    }    
    //适配器转换
    return httptest.NewServer(http.HandlerFunc(sendJson))
}
func TestSendJSON(t *testing.T) {    //创建一个模拟的服务器    server := mockServer()        defer server.Close()        //Get请求发往模拟服务器的地址    resq, err := http.Get(server.URL)        if err != nil {        t.Fatal("创建Get失败")    }    defer resq.Body.Close()    log.Println("code:", resq.StatusCode)    json, err := ioutil.ReadAll(resq.Body)        if err != nil {        log.Fatal(err)    }    log.Printf("body:%s\n", json)
}


模拟服务器的创建使用的是httptest.NewServer函数,它接收一个http.Handler处理API请求的接口。


代码示例中使用了Hander的适配器模式,http.HandlerFunc是一个函数类型,实现了http.Handler接口,这里是强制类型转换,不是函数的调用。


这个创建的模拟服务器,监听的是本机IP127.0.0.1,端口是随机的。接着我们发送Get请求的时候,不再发往/sendjson,而是模拟服务器的地址server.URL,剩下的就和访问正常的URL一样了,打印出结果即可。


测试覆盖率


我们尽可能的模拟更多的场景来测试我们代码的不同情况,但是有时候的确也有忘记测试的代码,这时候我们就需要测试覆盖率作为参考了。


由单元测试的代码,触发运行到的被测试代码的代码行数占所有代码行数的比例,被称为测试覆盖率,代码覆盖率不一定完全精准,但是可以作为参考,可以帮我们测量和我们预计的覆盖率之间的差距,go test工具,就为我们提供了这么一个度量测试覆盖率的能力。


main.go

func Tag(tag int){
    switch tag {    
    case 1:
        fmt.Println("Android")    
    case 2:
        fmt.Println("Go")    
    case 3:
        fmt.Println("Java")    
    default:
        fmt.Println("C")

    }
}


main_test.go

func TestTag(t *testing.T) {
    Tag(1)
    Tag(2)
}


现在我们使用go test工具运行单元测试,和前几次不一样的是,我们要显示测试覆盖率,所以要多加一个参数-coverprofile,所以完整的命令为:go test -v -coverprofile=c.out-coverprofile是指定生成的覆盖率文件,例子中是c.out,这个文件一会我们会用到。现在我们看终端输出,已经有了一个覆盖率。


=== RUN   TestTag
Android
Go
--- PASS: TestTag (0.00s)
PASS coverage: 60.0% of statements ok      flysnow.org/hello       0.005s


coverage: 60.0% of statements,60% 的测试覆盖率,还没有到 100% ,那么我们看看还有那些代码没有被测试到。这就需要我们刚刚生成的测试覆盖率文件c.out生成测试覆盖率报告了。生成报告有go为我们提供的工具,使用go tool cover -html=c.out -o=tag.html,即可生成一个名字为tag.html的HTML格式的测试覆盖率报告,这里有详细的信息告诉我们哪一行代码测试到了,哪一行代码没有测试到。



从上图中可以看到,标记为绿色的代码行已经被测试了;标记为红色的还没有测试到,有两行的,现在我们根据没有测试到的代码逻辑,完善我的单元测试代码即可。


func TestTag(t *testing.T) {
    Tag(1)
    Tag(2)
    Tag(3)
    Tag(6)
}


单元测试完善为如上代码,再运行单元测试,就可以看到测试覆盖率已经是 100% 了,大功告成。


目录
相关文章
|
21天前
|
存储 监控 算法
员工上网行为监控中的Go语言算法:布隆过滤器的应用
在信息化高速发展的时代,企业上网行为监管至关重要。布隆过滤器作为一种高效、节省空间的概率性数据结构,适用于大规模URL查询与匹配,是实现精准上网行为管理的理想选择。本文探讨了布隆过滤器的原理及其优缺点,并展示了如何使用Go语言实现该算法,以提升企业网络管理效率和安全性。尽管存在误报等局限性,但合理配置下,布隆过滤器为企业提供了经济有效的解决方案。
67 8
员工上网行为监控中的Go语言算法:布隆过滤器的应用
|
1月前
|
存储 Go 索引
go语言中数组和切片
go语言中数组和切片
44 7
|
1月前
|
Go 开发工具
百炼-千问模型通过openai接口构建assistant 等 go语言
由于阿里百炼平台通义千问大模型没有完善的go语言兼容openapi示例,并且官方答复assistant是不兼容openapi sdk的。 实际使用中发现是能够支持的,所以自己写了一个demo test示例,给大家做一个参考。
|
1月前
|
程序员 Go
go语言中结构体(Struct)
go语言中结构体(Struct)
110 71
|
1月前
|
存储 Go 索引
go语言中的数组(Array)
go语言中的数组(Array)
112 67
|
2天前
|
算法 安全 Go
Go语言中的加密和解密是如何实现的?
Go语言通过标准库中的`crypto`包提供丰富的加密和解密功能,包括对称加密(如AES)、非对称加密(如RSA、ECDSA)及散列函数(如SHA256)。`encoding/base64`包则用于Base64编码与解码。开发者可根据需求选择合适的算法和密钥,使用这些包进行加密操作。示例代码展示了如何使用`crypto/aes`包实现对称加密。加密和解密操作涉及敏感数据处理,需格外注意安全性。
27 14
|
1月前
|
Go 索引
go语言for遍历数组或切片
go语言for遍历数组或切片
110 62
|
2天前
|
Go 数据库
Go语言中的包(package)是如何组织的?
在Go语言中,包是代码组织和管理的基本单元,用于集合相关函数、类型和变量,便于复用和维护。包通过目录结构、文件命名、初始化函数(`init`)及导出规则来管理命名空间和依赖关系。合理的包组织能提高代码的可读性、可维护性和可复用性,减少耦合度。例如,`stringutils`包提供字符串处理函数,主程序导入使用这些函数,使代码结构清晰易懂。
27 11
|
2天前
|
存储 安全 Go
Go语言中的map数据结构是如何实现的?
Go 语言中的 `map` 是基于哈希表实现的键值对数据结构,支持快速查找、插入和删除操作。其原理涉及哈希函数、桶(Bucket)、动态扩容和哈希冲突处理等关键机制,平均时间复杂度为 O(1)。为了确保线程安全,Go 提供了 `sync.Map` 类型,通过分段锁实现并发访问的安全性。示例代码展示了如何使用自定义结构体和切片模拟 `map` 功能,以及如何使用 `sync.Map` 进行线程安全的操作。
|
6天前
|
监控 安全 算法
深度剖析核心科技:Go 语言赋能局域网管理监控软件进阶之旅
在局域网管理监控中,跳表作为一种高效的数据结构,能显著提升流量索引和查询效率。基于Go语言的跳表实现,通过随机化索引层生成、插入和搜索功能,在高并发场景下展现卓越性能。跳表将查询时间复杂度优化至O(log n),助力实时监控异常流量,保障网络安全与稳定。示例代码展示了其在实际应用中的精妙之处。
28 9