Go 单元测试完全指南(一)- 基本测试流程

news2024/10/6 21:55:24

为什么写单元测试?

关于测试,有一张很经典的图,如下:

在这里插入图片描述

说明:

测试类型成本速度频率
E2E 测试
集成测试
单元测试

也就是说,单元测试是最快、最便宜的测试方式。这不难理解,单元测试往往用来验证代码的最小单元,比如一个函数、一个方法,这样的测试我们一个命令就能跑完整个项目的单元测试,而且速度还很快,所以单元测试是我们最常用的测试方式。
而 E2E 测试和集成测试,往往需要启动整个项目,然后需要真实用户进行手动操作,这样的测试成本高,速度慢,所以我们往往不会频繁地运行这样的测试。只有在项目的最后阶段,我们才会运行这样的测试。而单元测试,我们可以在开发的过程中,随时随地地运行,这样我们就能及时发现问题,及时解决问题。

一个基本的 Go 单元测试

Go 从一开始就支持单元测试,Go 的测试代码和普通代码一般是放在同一个包下的,只是测试代码的文件名是 _test.go 结尾的。比如我们有一个 add.go 文件,那么我们的测试文件就是 add_test.go

// add.go
package main

func Add(a int, b int) int {
	return a + b
}
// add_test.go
package main

import "testing"

func TestAdd(t *testing.T) {
	if Add(1, 2) != 3 {
		t.Error("1 + 2 did not equal 3")
	}
}

我们可以通过 go test 命令来运行测试:

go test

输出:

PASS
ok      go-test 0.004s

注意:

  1. 测试函数的命名必须以 Test 开头,后面的名字必须以大写字母开头,比如 TestAdd
  2. 测试函数的参数是 *testing.T 类型。
  3. go test 加上 -v 参数可以输出详细的测试信息,加上 -cover 参数可以输出测试覆盖率。

go test 命令的参数详解

基本参数

  • -v:输出详细的测试信息。比如输出每个测试用例的名称。
  • -run regexp:只运行匹配正则表达式的测试用例。如 -run TestAdd
  • -bench regexp:运行匹配正则表达式的基准测试用例。
  • -benchtime t:设置基准测试的时间,默认是 1s,也就是让基准测试运行 1s。也可以指定基准测试的执行次数,格式如 -benchtime 100x,表示基准测试执行 100 次。
  • -count n:运行每个测试函数的次数,默认是 1 次。如果指定了 -cpu 参数,那么每个测试函数会运行 n * GOMAXPROCS 次。但是示例测试只会运行一次,该参数对模糊测试无效。
  • -cover:输出测试覆盖率。
  • -covermode set,count,atomic:设置测试覆盖率的模式。默认是 set,也就是记录哪些语句被执行过。
  • -coverpkg pkg1,pkg2,pkg3:用于指定哪些包应该生成覆盖率信息。这个参数允许你指定一个或多个包的模式,以便在运行测试时生成这些包的覆盖率信息。
  • -cpu 1,2,4:设置并行测试的 CPU 数量。默认是 GOMAXPROCS。这个参数对模糊测试无效。
  • -failfast:一旦某个测试函数失败,就停止运行其他的测试函数了。默认情况下,一个测试函数失败了,其他的测试函数还会继续运行。
  • -fullpath:测试失败的时候,输出完整的文件路径。
  • -fuzz regexp:运行模糊测试。
  • -fuzztime t:设置模糊测试的时间,默认是 1s。又或者我们可以指定模糊测试的执行次数,格式如 -fuzztime 100x,表示模糊测试执行 100 次。
  • -fuzzminimizetime t:设置模糊测试的最小化时间,默认是 1s。又或者我们可以指定模糊测试的最小化执行次数,格式如 -fuzzminimizetime 100x,表示模糊测试最小化执行 100 次。在模糊测试中,当发现一个失败的案例后,系统会尝试最小化这个失败案例,以找到导致失败的最小输入。
  • -json:以 json 格式输出
  • -list regexp:列出所有匹配正则表达式的测试用例名称。
  • -parallel n:设置并行测试的数量。默认是 GOMAXPROCS。
  • -run regexp:只运行匹配正则表达式的测试用例。
  • -short:缩短长时间运行的测试的测试时间。默认关闭。
  • -shuffle off,on,N:打乱测试用例的执行顺序。默认是 off,也就是不打乱,这会由上到下执行测试函数。
  • -skip regexp:跳过匹配正则表达式的测试用例。
  • -timeout t:设置测试的超时时间,默认是 10m,也就是 10 分钟。如果测试函数在超时时间内没有执行完,那么测试会 panic
  • -vet list:设置 go vet 的检查列表。默认是 all,也就是检查所有的。

性能相关

  • -benchmem:输出基准测试的内存分配情况(也就是 go test -bench . 的时候可以显示每次基准测试分配的内存)。
  • -blockprofile block.out:输出阻塞事件的分析数据。
  • -blockprofilerate n:设置阻塞事件的采样频率。默认是 1(单位纳秒)。如果没有设置采样频率,那么就会记录所有的阻塞事件。
  • -coverprofile coverage.out:输出测试覆盖率到文件 coverage.out
  • -cpuprofile cpu.out:输出 CPU 性能分析信息到文件 cpu.out
  • -memprofile mem.out:输出内存分析信息到文件 mem.out
  • -memprofilerate n:设置内存分析的采样频率。
  • -mutexprofile mutex.out:输出互斥锁事件的分析数据。
  • -mutexprofilefraction n:设置互斥锁事件的采样频率。
  • -outputdir directory:设置输出文件的目录。
  • -trace trace.out:输出跟踪信息到文件 trace.out

子测试

使用场景:当我们有多个测试用例的时候,我们可以使用子测试来组织测试代码,使得测试代码更具组织性和可读性。

package main

import (
	"testing"
)

func TestAdd2(t *testing.T) {
	cases := []struct {
		name      string
		a, b, sum int
	}{
		{"case1", 1, 2, 3},
		{"case2", 2, 3, 5},
		{"case3", 3, 4, 7},
	}

	for _, c := range cases {
		t.Run(c.name, func(t *testing.T) {
			sum := Add(c.a, c.b)
			if sum != c.sum {
				t.Errorf("Sum was incorrect, got: %d, want: %d.", sum, c.sum)
			}
		})
	}
}

输出:

➜  go-test go test   
--- FAIL: TestAdd2 (0.00s)
    --- FAIL: TestAdd2/case1 (0.00s)
        add_test.go:21: Sum was incorrect, got: 4, want: 3.
    --- FAIL: TestAdd2/case2 (0.00s)
        add_test.go:21: Sum was incorrect, got: 6, want: 5.
    --- FAIL: TestAdd2/case3 (0.00s)
        add_test.go:21: Sum was incorrect, got: 8, want: 7.
FAIL
exit status 1
FAIL    go-test 0.004s

我们可以看到,上面的输出中,失败的单元测试带有每个子测试的名称,这样我们就能很方便地知道是哪个测试用例失败了。

setup 和 teardown

在一般的单元测试框架中,都会提供 setupteardown 的功能,setup 用来初始化测试环境,teardown 用来清理测试环境。

方法一:通过 Go 的 TestMain 方法

很遗憾的是,Go 的测试框架并没有直接提供这样的功能,但是我们可以通过 Go 的特性来实现这样的功能。

在 Go 的测试文件中,如果有 TestMain 函数,那么执行 go test 的时候会执行这个函数,而不会执行其他测试函数了,其他的测试函数需要通过 m.Run 来执行,如下面这样:

package main

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

func setup() {
	fmt.Println("setup")
}

func teardown() {
	fmt.Println("teardown")
}

func TestAdd(t *testing.T) {
	if Add(1, 2) != 3 {
		t.Error("1 + 2 != 3")
	}
}

func TestMain(m *testing.M) {
	setup()
	code := m.Run()
	teardown()
	os.Exit(code)
}

在这个例子中,我们在 TestMain 函数中调用了 setupteardown 函数,这样我们就实现了 setupteardown 的功能。

方法二:使用 testify 框架

我们也可以使用 Go 中的第三方测试框架 testify 来实现 setupteardown 的功能(使用 testify 中的 suite 功能)。

package main

import (
	"fmt"
	"github.com/stretchr/testify/suite"
	"testing"
)

type AddSuite struct {
	suite.Suite
}

func (suite *AddSuite) SetupTest() {
	fmt.Println("Before test")
}

func (suite *AddSuite) TearDownTest() {
	fmt.Println("After test")
}

func (suite *AddSuite) TestAdd() {
	suite.Equal(Add(1, 2), 3)
}

func TestAddSuite(t *testing.T) {
	suite.Run(t, new(AddSuite))
}

go test 输出:

➜  go-test go test
Before test
After test
--- FAIL: TestAddSuite (0.00s)
    --- FAIL: TestAddSuite/TestAdd (0.00s)
        add_test.go:22: 
                Error Trace:    /Users/ruby/GolandProjects/go-test/add_test.go:22
                Error:          Not equal: 
                                expected: 4
                                actual  : 3
                Test:           TestAddSuite/TestAdd
FAIL
exit status 1
FAIL    go-test 0.006s

我们可以看到,这里也同样执行了 SetupTestTearDownTest 函数。

testing.T 可用的方法

最后,我们可以直接从 testing.T 提供的 API 来学习如何编写测试代码。

基本日志输出

  • t.Log(args ...any):打印信息,不会标记测试函数为失败。
  • t.Logf(format string, args ...any):打印格式化的信息,不会标记测试函数为失败。

可能有读者会有疑问,输出不用 fmt 而用 t.Log,这是因为:

  • t.Logt.Logf 打印的信息默认不会显示,只有在测试函数失败的时候才会显示。又或者我们使用 -v 参数的时候才显示,这让我们的测试输出更加清晰,只有必要的时候日志才会显示。
  • t.Logt.Logf 打印的时候,还会显示是哪一行代码打印的信息,这样我们就能很方便地定位问题。
  • fmt.Println 打印的信息一定会显示在控制台上,就算我们的测试函数通过了,也会显示,这样会让控制台的输出很乱。

例子:

// add.go
package main

func Add(a int, b int) int {
	return a + b
}
// add_test.go
package main

import (
	"testing"
)

func TestAdd(t *testing.T) {
	t.Log("TestAdd is running")
	if Add(1, 2) != 3 {
		t.Error("Expected 3")
	}
}

输出:

➜  go-test go test
PASS
ok      go-test 0.004s

我们修改一下 Add 函数,让测试失败,再次运行,输出如下:

➜  go-test go test
--- FAIL: TestAdd (0.00s)
    add_test.go:8: TestAdd is running
    add_test.go:10: Expected 3
FAIL
exit status 1
FAIL    go-test 0.004s

我们可以发现,在测试成功的时候,t.Log 打印的日志并没有显示,只有在测试失败的时候才会显示。

如果我们想要在测试成功的时候也显示日志,可以使用 -v 参数:go test -v

标记测试函数为失败

  • t.Fail():标记测试函数为失败,但是测试函数后续代码会继续执行。(让你在测试函数中标记失败情况,并收集所有失败的情况,而不是在遇到第一个失败时就立即停止测试函数的执行。)
  • t.FailNow():标记测试函数为失败,并立即返回,后续代码不会执行(通过调用 runtime.Goexit,但是 defer 语句还是会被执行)。
  • t.Failed():返回测试函数是否失败。
  • t.Fatal(args ...any):标记测试函数为失败,并输出信息,然后立即返回。等价于 t.Log + t.FailNow
  • t.Fatalf(format string, args ...any):标记测试函数为失败,并输出格式化的信息,然后立即返回。等价于 t.Logf + t.FailNow

如:

package main

import (
	"testing"
)

func TestAdd(t *testing.T) {
	if Add(1, 2) != 3 {
		t.Fatal("Expected 3")
	}

	if Add(2, 3) != 5 {
		t.Fatal("Expected 4")
	}
}

这里只会输出第一个失败的测试用例,因为 t.Fatal 会立即返回。

标记测试函数为失败并输出信息

  • t.Error(args ...any):标记测试函数为失败,并打印错误信息。等价于 t.Log + t.Fail
  • t.Errorf(format string, args ...any):标记测试函数为失败,并打印格式化的错误信息。等价于 t.Logf + t.Fail

这两个方法会让测试函数立即返回,不会继续执行后面的代码。

测试超时控制

  • t.Deadline():返回测试函数的截止时间(这是通过 go test -timeout 60s 这种形式指定的超时时间)。

注意:如果我们通过 -timeout 指定了超时时间,当测试函数超时的时候,测试会 panic

跳过测试函数中后续代码

作用:可以帮助测试代码在特定条件下灵活地跳过测试,避免不必要的测试执行,同时提供清晰的信息说明为什么跳过测试。

  • t.Skip(args ...any):跳过测试函数中后续代码,标记测试函数为跳过。等同于 t.Log + t.SkipNow
  • t.Skipf(format string, args ...any):跳过测试函数中后续代码,并打印格式化的跳过信息。等同于 t.Logf + t.SkipNow
  • t.SkipNow():跳过测试函数中后续代码,标记测试函数为跳过。这个方法不会输出内容,前面两个会输出一些信息
  • t.Skipped():返回测试函数是否被跳过。

测试清理函数

  • t.Cleanup(f func()):注册一个函数,这个函数会在测试函数结束后执行。这个函数会在测试函数结束后执行,不管测试函数是否失败,都会执行。(可以注册多个,执行顺序类似 defer,后注册的先执行)
package main

import (
	"fmt"
	"testing"
)

func TestAdd(t *testing.T) {
	t.Cleanup(func() {
		fmt.Println("cleanup 0")
	})
	t.Cleanup(func() {
		fmt.Println("cleanup 1")
	})
}

输出:

➜  go-test go test
cleanup 1
cleanup 0
PASS
ok      go-test 0.004s

使用临时文件夹

  • t.TempDir():返回一个临时文件夹,这个文件夹会在测试函数结束后被删除。可以调用多次,每次都是不同的文件夹。
package main

import (
	"fmt"
	"testing"
)

func TestAdd(t *testing.T) {
	fmt.Println(t.TempDir())
	fmt.Println(t.TempDir())
}

输出:

➜  go-test go test
/var/folders/dm/r_hly4w5557b000jh31_43gh0000gp/T/TestAdd4259799402/001
/var/folders/dm/r_hly4w5557b000jh31_43gh0000gp/T/TestAdd4259799402/002
PASS
ok      go-test 0.004s

临时的环境变量

  • t.Setenv(key, value string):设置一个临时的环境变量,这个环境变量会在测试函数结束后被还原。

在单元测试中,使用 Setenv 函数可以模拟不同的环境变量设置,从而测试代码在不同环境下的行为。例如,你可以在测试中设置特定的环境变量值,然后运行被测试的代码,以验证代码在这些环境变量设置下的正确性。

子测试

可以将一个大的测试函数拆分成多个子测试,使得测试代码更具组织性和可读性。

  • t.Run(name string, f func(t *testing.T)):创建一个子测试,这个子测试会在父测试中执行。子测试可以有自己的测试函数,也可以有自己的子测试。

获取当前测试的名称

  • t.Name():返回当前测试的名称(也就是测试函数名)。

t.Helper()

  • t.Helper():标记当前测试函数是一个辅助函数,这样会让测试输出更加清晰,只有真正的测试函数会被标记为失败。

例子:

// add.go
package main

func Add(a int, b int) int {
	return a + b + 1
}
// add_test.go
package main

import (
	"testing"
)

func test(a, b, sum int, t *testing.T) {
	result := Add(a, b)
	if result != sum {
		t.Errorf("Add(%d, %d) = %d; want %d", a, b, result, sum)
	}
}

func TestAdd(t *testing.T) {
	test(1, 2, 3, t)
	test(2, 3, 5, t)
}

输出如下:

➜  go-test go test -v
=== RUN   TestAdd
    add_test.go:10: Add(1, 2) = 4; want 3
    add_test.go:10: Add(2, 3) = 6; want 5
--- FAIL: TestAdd (0.00s)
FAIL
exit status 1
FAIL    go-test 0.004s

我们可以看到,两个测试失败输出的报错行都是 test 函数里面的 t.Errorf,而不是 test 函数的调用者 TestAdd,也就是说,在这种情况下我们不好知道是 test(1, 2, 3, t) 还是 test(2, 3, 5, t) 失败了(当然我们这里还是挺明显的,只是举个例子),这时我们可以使用 t.Helper()

func test(a, b, sum int, t *testing.T) {
    t.Helper() // 在助手函数中加上这一行
	result := Add(a, b)
	if result != sum {
		t.Errorf("Add(%d, %d) = %d; want %d", a, b, result, sum)
	}
}

输出如下:

➜  go-test go test -v
=== RUN   TestAdd
    add_test.go:16: Add(1, 2) = 4; want 3
    add_test.go:17: Add(2, 3) = 6; want 5
--- FAIL: TestAdd (0.00s)
FAIL
exit status 1
FAIL    go-test 0.004s

这个时候,我们就很容易知道是哪一个测试用例失败了,这对于我们需要封装 helper 函数的时候很有用。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1659340.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

人形机器人大热,优必选、傅利叶智能的春天还有多远?

配图来自Canva可画 进入2024年以来,机器人领域仍然十分热闹。前脚,斯坦福大学华人团队开源的既会做家务、又会煎蛋煮饭的MobileALOHA机器人,在全网刷屏,让不少人直呼“机器人养老有望”;后脚,马斯克就发了…

ERP系统:驱动企业高效、准确发展的核心引擎

企业资源规划(ERP)系统作为大型组织内部不同计算机系统的桥梁,发挥着至关重要的作用。在没有ERP系统的情况下,各部门可能各自拥有一套为其特定任务定制的系统,但这些系统之间是相互孤立的。而引入ERP软件后&#xff0c…

【STM32 |程序实测】LED灯闪烁、LED灯流水线、蜂鸣器

LED闪烁&LED流水灯&蜂鸣器的面包板接线图,及对应程序示例 LED闪烁 面包板接线图如下 开启APB2时钟,并且在GPIOA上进行配置,推挽输出,引脚A0,50HZ速度 #include "stm32f10x.h" /…

如何在Springboot项目的Mapper中增加一个新的sql语句

在做项目的过程中,我发现有的时候需要用到一些不在springboot的Mapper中的Sql语句,那么应该如何进行操作呐?? 平常我们创建springbootmybatisPlus项目的时候是这样创建的:: 1、创建实体类 2、创建Mappe…

OBS插件--自定义着色器

自定义着色器 自定义着色器是一个滤镜插件,可以用于源和场景。插件自带一百多款滤镜效果,支持自己编写效果代码。 下面截图演示下操作步骤: 首先,打开 OBS直播助手 在插件中心左侧导航栏,选择 滤镜 项,然…

【快讯】山东省第四批软件产业高质量发展重点项目开始申报

为加快落实《山东省高端软件“铸魂”工程实施方案(2023-2025)》,提高软件产业规模能级,提升关键软件技术创新和供给能力,塑强数字经济发展核心竞争力,确定开展第四批软件产业高质量发展重点项目申报工作&am…

docker-compose集成elasticsearch7.17.14+kibana7.17.14

1.docker和compose版本必须要高 2.准备ik分词器(elasticsearch-analysis-ik-7.17.14),下面会用到 https://github.com/infinilabs/analysis-ik/releases?page2 3.配置es-compose.yml(切记映射容器内路径不能更改,es和kibana服务…

【文化课学习笔记】【物理】功与能

【物理】功与能 功 基础概念 定义 一个物体在力的作用下,沿力的方向,通过一段距离(位移),则称这个力做了功。 公式 功的定义式: \[W Fx \] 这里的 \(x\) 指的是物体沿力的方向上发生的位移。由于力 \(F\) 和位移 \(x\) 都是矢量&…

无人机+人工智能:多智能体,智能蜂群技术详解

无人机与人工智能的结合,特别是在多智能体和智能蜂群技术方面,已经成为当今科技领域的前沿。这种技术的核心在于利用人工智能的决策和学习能力,结合无人机的机动性和传感器能力,实现一种高度协同、自主、智能的作战或任务执行方式…

APScheduler定时器使用【重写SQLAlchemyJobStore版】:django中使用apscheduler,使用mysql做存储后端

一、环境配置 python3.8.10 包: APScheduler3.10.4 Django3.2.7 djangorestframework3.15.1 SQLAlchemy2.0.29 PyMySQL1.1.0 项目目录情况 gs_scheduler 应用 commands : 主要用来自定义命令,python manage.py crontab schedulers&#…

利用BACnet分布式IO控制器优化Niagara楼宇自动化系统

在智能建筑领域,随着物联网技术的飞速发展,如何实现高效、灵活且安全的楼宇自动化控制成为了行业关注的焦点。BACnet IP分布式远程I/O模块,作为这一领域的创新成果,正逐渐成为连接智能建筑各子系统的关键桥梁,尤其在与…

黏土滤镜制作方法:探索黏土特效制作的魅力

在数字时代,图像处理已经成为我们生活的一部分,无论是社交媒体上的照片分享,还是专业设计领域的创作,都离不开对图像的精心处理。而黏土滤镜,作为一种独特而富有艺术感的图像处理效果,受到了越来越多人的喜…

ArcGIS如何计算地级市间的距离

一、数据准备 加载配套实验数据包中的地级市和行政区划矢量数据(订阅专栏后,从私信查收数据),如下图所示: 二、计算距离 1. 计算邻近表 ArcGIS提供了计算点和另外点之间距离的工具:分析工具→邻域分析→生成临近表。 计算一个或多个要素类或图层中的要素间距离和其他邻…

AI写的论文AI疑似度太高怎么办?教你一招降低aigc痕迹

随着 AI 技术迅猛发展,各种AI辅助论文写作的工具层出不穷! 为了防止有人利用AI工具进行论文代写,在最新的学位法中已经明确规定“已经获得学位者,在获得该学位过程中如有人工智能代写等学术不端行为,经学位评定委员会…

Docker 怎么将映射出的路径设置为非root用户权限

在Docker中,容器的根文件系统默认是由root用户拥有的。如果想要在映射到宿主机的路径时设置为非root用户权限,可以通过以下几种方式来实现: 1. 使用具有特定UID和GID的非root用户运行容器: 在运行容器时,你可以使用-u…

huggingface-cli + hf_transfer 加速大模型下载

huggingface-cli hf_transfer 加速大模型下载 如果用的是autodl的话,可以直接开学术加速用 lfs 下也挺快 pip install -U huggingface_hub设置环境变量 vim .bashrcexport HF_ENDPOINThttps://hf-mirror.comsource ~/.bashrc下载模型: huggingface-cl…

H5 css动画效果

你可以使用 CSS 动画来实现这个效果。下面是一个简单的示例代码&#xff0c;展示了如何使用 CSS 中的关键帧动画来放大然后缩小一张图片&#xff0c;并使动画循环播放&#xff1a; html <!DOCTYPE html> <html lang"en"><head><meta charset&qu…

Numpy的数组操作

文章目录 数组的创建创建全0的二维数组a(3,3)全1的二维数组b&#xff08;3,4&#xff09;随机数二维数数组c&#xff08;2,3&#xff09;效果截图 数组的属性查看b数组的维度查看b数组元素的个数效果截图 数组的维度操作将数组c的行变列&#xff0c;返回最后一个元素返回数组c第…

Linux中的日志系统简介

在的Linux系统上使用的日志系统一般为rsyslogd。rsyslogd守护进程既能接收用户进程输出的日志&#xff0c;又能接收内核日志。用户进程是通过调用syslog函数生成系统日志的。该函数将日志输出到一个UNIX本地域socket类型&#xff08;AF_UNIX&#xff09;的文件/dev/log中&#…

c++11 标准模板(STL)本地化库 - 平面类别(std::money_get) - 从输入字符序列中解析并构造货币值

本地化库 本地环境设施包含字符分类和字符串校对、数值、货币及日期/时间格式化和分析&#xff0c;以及消息取得的国际化支持。本地环境设置控制流 I/O 、正则表达式库和 C 标准库的其他组件的行为。 平面类别 从输入字符序列中解析并构造货币值 std::money_get template<…