代码编织梦想

如何写好单元测试

单元测试(Unit Tests, UT) 是一个优秀项目不可或缺的一部分,特别是在一些频繁变动和多人合作开发的项目中尤为重要。你或多或少都会有因为自己的提交,导致应用挂掉或服务宕机的经历。如果这个时候你的修改导致测试用例失败,你再重新审视自己的修改,发现之前的修改还有一些特殊场景没有包含,恭喜你减少了一次上库失误。也会有这样的情况,项目很大,启动环境很复杂,你优化了一个函数的性能,或是添加了某个新的特性,如果部署在正式环境上之后再进行测试,成本太高。对于这种场景,几个小小的测试用例或许就能够覆盖大部分的测试场景。而且在开发过程中,效率最高的莫过于所见即所得了,单元测试也能够帮助你做到这一点,试想一下,假如你一口气写完一千行代码,debug 的过程也不会轻松,如果在这个过程中,对于一些逻辑较为复杂的函数,同时添加一些测试用例,即时确保正确性,最后集成的时候,会是另外一番体验。

如何写好单元测试呢?

首先,学会写测试用例。比如如何测试单个函数/方法;比如如何做基准测试;比如如何写出简洁精炼的测试代码;再比如遇到数据库访问等的方法调用时,如何 mock

然后,写可测试的代码。高内聚,低耦合是软件工程的原则,同样,对测试而言,函数/方法写法不同,测试难度也是不一样的。职责单一,参数类型简单,与其他函数耦合度低的函数往往更容易测试。我们经常会说,“这种代码没法测试”,这种时候,就得思考函数的写法可不可以改得更好一些。为了代码可测试而重构是值得的。

接下来将介绍如何使用 Go 语言的标准库 testing 进行单元测试。

一个简单的例子

package example

import "testing"

func TestAdd(t *testing.T) {
   if ans := Add(1, 2); ans != 3 {
      t.Errorf("1 + 2 expected be 3, but %d got", ans)
   }
   if ans := Add(-10, -20); ans != -30 {
      t.Errorf("-10 + -20 expected be -30, but %d got", ans)
   }
}
  • 测试用例名称一般命名为Test加上待测试方法名
  • 测试用的参数有且仅有一个,在这里是t *testing.T
  • 基准测试的参数是 * testing.B,* TestMain的参数是* testing.M类习惯

子测试

子测试是 Go 语言内置支持的,可以在某个测试用例中,根据测试场景使用 t.Run创建不同的子测试用例:

func TestMul(t *testing.T) {
   t.Run("pos", func(t *testing.T) {
      if Mul(2, 3) != 6 {
         t.Fatal("fail")
      }
   })
   t.Run("neg", func(t *testing.T) {
      if Mul(2, -3) != -6 {
         t.Fatal("fail")
      }
   })
}
  • 之前的例子测试失败时使用 t.Error/t.Errorf,这个例子中使用 t.Fatal/t.Fatalf,区别在于前者遇错不停,还会继续执行其他的测试用例,后者遇错即停。

对于多个子测试的场景,更推荐如下的写法(table-driven tests):

func TestMul(t *testing.T) {
   cases := []struct {
      Name           string
      A, B, Expected int
   }{
      {"pos", 2, 3, 6},
      {"neg", 2, -3, -6},
      {"zero", 2, 0, 0},
   }

   for _, c := range cases {
      t.Run(c.Name, func(t *testing.T) {
         if ans := Mul(c.A, c.B); ans != c.Expected {
            t.Fatalf("%d * %d expected %d, but %d got",
               c.A, c.B, c.Expected, ans)
         }
      })
   }
}

帮助函数

对一些重复的逻辑,抽取出来作为公共的帮助函数(helpers),可以增加测试代码的可读性和可维护性。 借助帮助函数,可以让测试用例的主逻辑看起来更清晰。

例如,我们可以将创建子测试的逻辑抽取出来:

package example

import "testing"

type calcCase struct {
   A, B, Expected int
}

func createMulTestCase(t *testing.T, c *calcCase) {
   if ans := Mul(c.A, c.B); ans != c.Expected {
      t.Fatalf("%d * %d expected %d, but %d got",
         c.A, c.B, c.Expected, ans)
   }
}

func TestMul(t *testing.T) {
   createMulTestCase(t, &calcCase{2, 3, 6})
   createMulTestCase(t, &calcCase{
      A:        2,
      B:        0,
      Expected: 1,
   })
}

我们故意创造了一个错误的条件,接下来我们来看一个这个错误的条件:

=== RUN   TestMul
    cal_test.go:11: 2 * 0 expected 1, but 0 got
--- FAIL: TestMul (0.00s)

FAIL

可以看到,错误发生在第11行,也就是帮助函数 createMulTestCase 内部。18, 19, 20行都调用了该方法,我们第一时间并不能够确定是哪一行发生了错误。有些帮助函数还可能在不同的函数中被调用,报错信息都在同一处,不方便问题定位。因此,Go 语言在 1.9 版本中引入了 t.Helper(),用于标注该函数是帮助函数,报错时将输出帮助函数调用者的信息,而不是帮助函数的内部信息。

修改 createMulTestCase,调用 t.Helper()

func createMulTestCase(t *testing.T, c *calcCase) {
   t.Helper()
   if ans := Mul(c.A, c.B); ans != c.Expected {
      t.Fatalf("%d * %d expected %d, but %d got",
         c.A, c.B, c.Expected, ans)
   }
}

再次运行go test就可以看到错误的信息了。

=== RUN   TestMul
    cal_test.go:19: 2 * 0 expected 1, but 0 got
--- FAIL: TestMul (0.00s)

FAIL

这样就明确的可以看到是第19排出现的错误。

setup和teardown

如果在同一个测试文件中,每一个测试用例运行前后的逻辑是相同的,一般会写在 setup 和 teardown 函数中。例如执行前需要实例化待测试的对象,如果这个对象比较复杂,很适合将这一部分逻辑提取出来;执行后,可能会做一些资源回收类的工作,例如关闭网络连接,释放文件等。标准库 testing 提供了这样的机制:

package example

import (
   "fmt"
   "os"
   "testing"
)

func setup() {
   fmt.Println("Before all tests")
}

func teardown() {
   fmt.Println("After all tests")
}

func Test1(t *testing.T) {
   fmt.Println("I' m test1")
}

func Test2(t *testing.T) {
   fmt.Println("I' m test2")
}

func TestMain(m *testing.M) {
   setup()
   code := m.Run()
   teardown()
   os.Exit(code)
}
  • 在这个测试文件中,包含有2个测试用例,Test1Test2
  • 如果测试文件中包含函数 TestMain,那么生成的测试将调用 TestMain(m),而不是直接运行测试。
  • 调用 m.Run() 触发所有测试用例的执行,并使用 os.Exit() 处理返回的状态码,如果不为0,说明有用例失败。
  • 因此可以在调用 m.Run() 前后做一些额外的准备(setup)和回收(teardown)工作。

网络测试

TCP/HTTP

假设需要测试某个 API 接口的 handler 能够正常工作,例如 helloHandler

func helloHandler(w http.ResponseWriter, r *http.Request) {
	w.Write([]byte("hello world"))
}

那我们可以创建真实的网络连接进行测试:

package example

import (
   "io/ioutil"
   "net"
   "net/http"
   "testing"
)

func helloHandler(w http.ResponseWriter, r *http.Request) {
   w.Write([]byte("hello world"))
}

func handleError(t *testing.T, err error) {
   t.Helper()
   if err != nil {
      t.Fatal("failed", err)
   }
}

func TestConn(t *testing.T) {
   ln, err := net.Listen("tcp", "127.0.0.1:0")
   handleError(t, err)
   defer ln.Close()

   http.HandleFunc("/hello", helloHandler)
   go http.Serve(ln, nil)

   resp, err := http.Get("http://" + ln.Addr().String() + "/hello")
   handleError(t, err)

   defer resp.Body.Close()
   body, err := ioutil.ReadAll(resp.Body)
   handleError(t, err)

   if string(body) != "hello world" {
      t.Fatal("expected hello world, but got", string(body))
   }
}
  • net.Listen("tcp", "127.0.0.1:0"):监听一个未被占用的端口,并返回 Listener。
  • 调用 http.Serve(ln, nil) 启动 http 服务。
  • 使用 http.Get 发起一个 Get 请求,检查返回值是否正确。
  • 尽量不对 httpnet 库使用 mock,这样可以覆盖较为真实的场景。

httptest

针对 http 开发的场景,使用标准库 net/http/httptest 进行测试更为高效。

上述的测试用例改写如下:

package example

import (
   "io/ioutil"
   "net/http"
   "net/http/httptest"
   "testing"
)

func helloHandler(w http.ResponseWriter, r *http.Request) {
   w.Write([]byte("hello world"))
}

func TestConn(t *testing.T) {
   req := httptest.NewRequest("get", "http://localhost:5555/metrics", nil)
   w := httptest.NewRecorder()
   helloHandler(w, req)
   bytes, _ := ioutil.ReadAll(w.Result().Body)

   if string(bytes) != "hello world" {
      t.Fatal("expected hello world, but got", string(bytes))
   }
}

使用 httptest 模拟请求对象(req)和响应对象(w),达到了相同的目的。

Benchmark基准测试

基准测试用例的定义如下:

func BenchmarkName(b *testing.B) {
    // ...
}
  • 函数名必须以 Benchmark 开头,后面一般跟待测试的函数名
  • 参数为 b *testing.B
  • 执行基准测试时,需要添加 -bench 参数。
func BenchmarkMul(b *testing.B) {
   for i := 0; i < b.N; i++ {
      fmt.Sprintf("hello")
   }
}

测试结果如下:

goos: windows
goarch: amd64
pkg: ginTest/example
cpu: 11th Gen Intel(R) Core(TM) i5-1135G7 @ 2.40GHz
BenchmarkMul
BenchmarkMul-8          28505390                37.13 ns/op
PASS

这是一份基准报告,基准报告每一列对应的含义如下:

type BenchmarkResult struct {
    N         int           // 迭代次数
    T         time.Duration // 基准测试花费的时间
    Bytes     int64         // 一次迭代处理的字节数
    MemAllocs uint64        // 总的分配内存的次数
    MemBytes  uint64        // 总的分配内存的字节数
}

如果在运行前基准测试需要一些耗时的配置,则可以使用 b.ResetTimer() 先重置定时器,例如:

func BenchmarkMul(b *testing.B) {
   // ...耗时操作
   b.ResetTimer()
   for i := 0; i < b.N; i++ {
      fmt.Sprintf("hello")
   }
}

使用 RunParallel 测试并发性能

import (
   "bytes"
   "html/template"
   "testing"
)

func BenchmarkParallel(b *testing.B) {
   templ := template.Must(template.New("test").Parse("Hello, {{.}}!"))
   b.RunParallel(func(pb *testing.PB) {
      var buf bytes.Buffer
      for pb.Next() {
         // 所有goroutine一起,循环一共执行b.N次
         buf.Reset()
         templ.Execute(&buf, "World")
      }
   })
}

测试结果如下:

goos: windows
goarch: amd64
pkg: ginTest/example
cpu: 11th Gen Intel(R) Core(TM) i5-1135G7 @ 2.40GHz
BenchmarkParallel
BenchmarkParallel-8      3969212               306.5 ns/op
PASS
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/qq_61039408/article/details/129653739

go 单元测试 html报告,Go 单元测试-爱代码爱编程

1.单元测试 本节代码样例见code/utest文件夹 在日常开发中,我们通常需要针对现有的功能进行单元测试,以验证开发的正确性。 在go标准库中有一个叫做testing的测试框架,可以进行单元测试,命令是go test xxx。 测试文件通常是以xx_test.go命名,放在同一包下面。 11.1 初探Go单元测试 现在假设现在需求是:完成

go 单元测试 html报告,go语言单元测试-爱代码爱编程

Go本身提供了一套轻量级的测试框架.符合规则的测试代码会在运行测试时被自动识别并执行.单元测试源文件的命名规则如平衡点:在需要测试的包下面创建以”_test”结尾的go文件,开如[^.]*_test.go Go单元测试函数分为两在类.功能测试函数和性能测试函数,分别以Test和Benchmark为函数名前缀并以*testing.T 和 *testin

「GoCN酷Go推荐」golang 单元测试最佳实践-爱代码爱编程

为什么要进行单元测试? 在没工作之前,说实话没怎么写过单元测试,很多情况下就是一边写代码,一边运行,用 fmt.Println() 打印变量,再稍微复杂一点的也许会用 dlv 去 debug 代码,找出问题。 但是在做公司做大型项目时就会发现,你根本就没办法把项目跑起来,这个时候你只能通过写单元测试去看自己的逻辑对不对。 当然仍旧会出现一个问题

Golang 单元测试 go test-爱代码爱编程

文章目录 前言一、pandas是什么?二、使用步骤 1.引入库2.读入数据总结 前言         在软件领域~  如何维护越来越复杂的项目代码,提高整体代码质量 是个重要的问题,对此有个重要的编程方法是TDD (Test-Driven Development, 测试驱动开发),它强调的是先编写测试、再对代码进行设计和重构。  

golang单元测试、mock测试以及基准测试_小菜鸡本菜的博客-爱代码爱编程

之前参加字节跳动青训营而整理的笔记 Golang拥有一套单元测试和性能测试系统,仅需要添加很少的代码就可以快速测试一段需求代码。 一、单元测试 单元测试主要包括:输入、测试单元、输出、期望以及与期望的校对。 测试单元包括函数或者结合了一些函数的模块等。我们通过将输出与期望值进行校对,来验证代码的正确性。 通过单元测试,可以一方面保证

go单元测试_大隐、禅的博客-爱代码爱编程

文章目录 一、概述二、详情2.1 单元测试规则2.2 测试命令2.3 性能测试2.4 测试覆盖率 一、概述 测试驱动开发永远是一个不过时的话题,一次实现多次利用,避免了手动测试的每次费时费力。 在

go语言学习笔记——单元测试_pppsych的博客-爱代码爱编程

文章目录 Golang单元测试Golang支持的测试种类单元测试单元测试框架提供的日志方法子测试(Subtests)帮助函数(helpers)setup和teardown网络测试(Network)TCP/HTTPht

golang单元测试一(简单函数测试)_六月的的博客-爱代码爱编程

0.1、索引 https://blog.waterflow.link/articles/1663688140724 1、简介 单元测试是测试代码、组件和模块的单元函数。单元测试的目的是清除代码中的错误,增加代码的稳定性

golang单元测试指引_简说linux的博客-爱代码爱编程

一、单元测试 1. 单元测试是什么 单元是应用的最小可测试部件。在过程化编程中,一个单元就是单个程序、函数、过程等;对于面向对象编程,最小单元就是方法,包括基类、超类、抽象类等中的方法。单元测试就是软件开发中对最小单位进行正确性检验的测试工作。 不同地方对单元测试有的定义可能会有所不同,但有一些基本共识: 单元测试是比较底层的,关注代码的局部而不是

golang单元测试第一章——网络测试-爱代码爱编程

参考博客:Go单测从零到溜系列1—网络测试 | 李文周的博客 (liwenzhou.com) 原文中有这样一句话:无论我们的代码是作为server端对外提供服务或者还是我们依赖别人提供的网络服务(调用别人提供的API接口)的场景,我们通常都不想在测试过程中真正的建立网络连接。本文就专门介绍如何在上述两种场景下mock网络测试。 这句话有点似懂非懂,

golang单元测试第二章——mysql和redis测试-爱代码爱编程

参考链接:Go单测从零到溜系列2—MySQL和Redis测试 | 李文周的博客 (liwenzhou.com) 原文中有一句话: 除了网络依赖之外,我们在开发中也会经常用到各种数据库,比如常见的MySQL和Redis等。本文就分别举例来演示如何在编写单元测试的时候对MySQL和Redis进行mock。 为什么我想要学单元测试?是因为实际开发的过程中

【golang】将项目部署到docker-爱代码爱编程

1. 编写Dockerfile文件 FROM golang:1.19 MAINTAINER "xxx@gmail.com" WORKDIR /home/go/src/sanHeRecruitment ADD . /h

go 单元测试_goland test-爱代码爱编程

概念 单元测试 UT测试,针对程序来进行正确检测测试工作,一个优秀强壮代码 需要有完美的 UT测试用例 go test 基本用法 go test 测试用例放在 *_test.go 文件中,与被测函数放到同一个目录下面g

golang单元测试框架goconvey_convey golang-爱代码爱编程

GoConvey是一个非常非常好用的Go测试框架,它直接与go test集成,提供了很多丰富的断言函数,能够在终端输出可读的彩色测试结果 安装 go get github.com/smartystreets/goc

【golang第11章:单元测试】go语言单元测试_golang单元测试-爱代码爱编程

介绍 这个是在B站上看边看视频边做的笔记,这一章是Glang的单元测试 配套视频自己去B站里面搜【go语言】,最高的播放量就是 里面的注释我写的可能不太对,欢迎大佬们指出╰(°▽°)╯ 文章目录 介绍(