【字节跳动青训营】后端笔记整理-3 | Go语言工程实践之测试

news2025/1/8 11:52:36

**本文由博主本人整理自第六届字节跳动青训营(后端组),首发于稀土掘金:🔗Go语言工程实践之测试 | 青训营

目录

一、概述

1、回归测试

2、集成测试

3、单元测试

二、单元测试

1、流程

2、规则

3、单元测试的例子

4、assert

5、覆盖率

6、依赖

7、文件处理

三、Mock测试

四、基准测试


一、概述

测试主要包括:回归测试、集成测试、单元测试。

1、回归测试

用于验证已经修改或新增功能后,软件的既有功能是否受到影响。

它主要用于确保软件在经过修改后仍然能正确运行,并且新的更改没有引入新的错误或破坏原有的功能。

比如开发抖音产品,那么回归测试就是新增了一部分功能后,由负责质量保证的部门手动在终端创造一些特定的场景(比如刷一下抖音,看一下评论等)来检查功能是否正确。

2、集成测试

用于验证多个模块或组件在一起协同工作时的正确性。

它主要用于检查不同模块之间的交互和接口是否正常,以确保整个软件系统在集成后能够正常运行。在软件开发过程中,通常会将软件系统划分为多个模块或组件,每个模块负责实现不同的功能。

在集成测试中,这些模块会被组合在一起,并进行全面的测试,以验证它们之间的协作和接口是否正确。

3、单元测试

在开发阶段,开发者对单独的函数模块进行功能测试。单元测试用于验证程序中的各个独立模块(通常是函数或方法)是否按照预期进行工作。它是在软件开发中最小的测试单位,旨在测试代码的最小功能单元,以确保每个单元都能正确地完成其预定的功能。

层级从上到下,测试的覆盖率逐层变大,成本逐层降低。因此可以说,单元测试的覆盖率一定程度上决定了代码的质量。

  • 单元测试的成本较低,覆盖率较高,可以确保每个单元都能正确工作。
  • 集成测试的成本适中,覆盖率较低,主要用于验证不同模块之间的协作。
  • 回归测试的成本较高,主要用于验证整个系统的稳定性和功能性。

在实际开发中,这三种测试方法通常会结合使用,以便在不同的层次和阶段上确保软件的质量和稳定性。


二、单元测试

1、流程

单元测试主要包括输入、测试单元、输出以及校对。

单元测试的流程
  • “单元”的概念比较广,包括接口、函数、模块等等。
  • 用最后的校对来保证代码的功能与我们预期的相符。最后通过输出和期望值作校对,来验证代码的正确性。
  • 单元测试一方面可以保证质量。每次编写新代码并加入了单元测试,在代码整体覆盖率足够的情况下一方面保证了新功能本身的正确性,又未破坏原有的正确性;另一方面可以提升效率,在代码有bug的情况下通过编写单测,可以在一个较短的周期内定位和修复问题。

2、规则

(1)所有的测试文件都以 _test.go 结尾。这样可以很容易辨别哪些文件是 go 的源代码,哪些是测试代码。

(2)单元测试函数的命名规范,以Test开头,且Test后面的第一个字母大写。如TestDemo。

(3)单元测试提供了一个TestMain函数,TestMain是 Go 语言中测试包(testing)的一个特殊函数,用于在运行测试之前和之后执行一些初始化和收尾操作。它不是针对单个测试用例的,而是整个测试包的入口函数。它可以被用来替代测试函数的默认入口点 func TestXxx(t *testing.T)

TestMain的代码结构如下,它会在所有测试函数执行之前运行,并且可以在其中进行全局初始化和清理操作。然后,它会调用 m.Run() 启动所有测试函数的执行。

因为它是测试包的入口函数,所以只会执行一次。

需要注意的是,测试函数是并行执行的,所以不能依赖测试函数之间的执行顺序。

3、单元测试的例子

先在demo2.go文件中,创建一个HelloTom函数,预期返回"Tom"。

(这里为了测试,故意写成返回"Jerry"。)

//demo2.go

package main

func HelloTom() string {
	return "Jerry"
}

然后生成测试文件: demo2_test.go。

Tips:如果使用 GoLand 为IDE,快捷键 alt+insert ,然后选择Test file,可以直接生成一个单元测试文件。

在demo2_test.go中编写以下测试代码:

package main

import "testing"

func TestHelloTom(t *testing.T) {
	output := HelloTom()  //运行HelloTom 接收返回值
	expectOutput := "Tom" //预期输出Tom
	if output != expectOutput {
		t.Errorf("Expected %s do not match actual %s", expectOutput, output)
	}
}

通过 if 的方式校对 output 是否与 expected 相等。如果相等,则测试PASS,不相等则FAIL。

除了直接用运算符来判断,还有更加简便的方式,即使用 assert。

4、assert

除了使用运算符进行校对,也有很多开源的assert包可以帮助我们实现预期和实际输出equal或not equal的比较。

导入开源的assert包:

github.com/stretchr/testify/assert

可以在终端使用以下命令获取依赖(如果没有下载过,直接引入包会报红):

​go get github.com/stretchr/testify/assert

调用 assert.Equal来对预期和实际输出进行比较。第二个参数是预期输出,第三个参数是实际输出:

package main

import (
	"github.com/stretchr/testify/assert" // 导入开源的assert包
	"testing"
)

func TestHelloTom(t *testing.T) {
	output := HelloTom()  //运行HelloTom 接收返回值
	expectOutput := "Tom" //预期输出Tom
	//if output != expectOutput {
	//	t.Errorf("Expected %s do not match actual %s", expectOutput, output)
	//}

	assert.Equal(t, expectOutput, output)    //直接调用包中的接口
}

运行结果:actual 和 expected 不符合,测试FAIL。

若将HelloTom模块返回的值改为Tom,再次执行单元测试:

可见此时单元测试的结果是PASS。

5、覆盖率

在我们进行单元测试时需要考虑以下问题:

如何衡量代码是否经过了足够的测试?如何评价项目的测试水准?如何评估项目是否达到了高水准测试等级?

单元测试的主要评估标准是代码覆盖率,覆盖率越高则证明越多的代码经过了测试。来看下面这个例子:

创建demo3.go,其中编写了一个判断分数是否及格的功能。

package main

func judgePass(score int) bool {
	//return score >= 60
	if score >= 60 {
		return true
	}
	return false
}

 生成测试文件demo3_test.go,调用 judgePass 并传入一个70,将结果与true校对。

package main

import (
	"github.com/stretchr/testify/assert"
	"testing"
)

func Test_judgePass(t *testing.T) {
	isPass := judgePass(70)
	assert.Equal(t, true, isPass)
}

然后运行带有覆盖率(coverage)的测试。可以用命令单独测试某个模块(如demo3.go):

go test demo3_test.go demo3.go --cover

也可以在GoLand直接操作,点击右上角的run with coverage进行测试。测试工具会自动运行测试用例,并在执行过程中跟踪被执行的代码行数。最后,它会计算测试覆盖率并给出结果。(注意,这样执行的是该目录下的所有测试文件,给出的也是整个包的测试覆盖率结果。)

最终的代码覆盖率为66.7%。这是因为demo3.go中的3条语句在case为70时执行2条。

如果在 judgePass 中直接写成 return score >= 60,那么代码的覆盖率就会变成100%。因为这个函数中所有的语句都被执行过了。 

特别注意:直接点击run with coverage,执行的是该目录下的所有测试文件,给出的也是整个包的测试覆盖率结果。如果该目录下还有其它文件代码,执行测试用例后会发现覆盖率降低:

如何提升覆盖率?

刚才只是测试了70,覆盖率只有66.7%,还有部分代码没有被运行。我们希望提升覆盖率,因此可以多传入一些测试用例case。比如这里,再传入一个50:

coverage达到了 100%。

6、依赖

实际项目中,测试依赖的组件可能会很复杂。比如可能依赖一些数据库,文件,cache等。这些都属于项目中的强依赖(即是一个模块对于另一个模块存在紧密的依赖关系。当一个组件的实现或功能发生变化时,会直接影响到依赖它的其他组件的正确性或稳定性)。

而单元测试的两个目标是幂等和稳定:

  • 幂等:重复运行一个测试,其结果都是一样的。
  • 稳定:单元测试是相互隔离的,在任何时间任何函数都可运行。

其实,直接写单元测试可能是不稳定的,因为它可能存在一些依赖,如网络等。解决这个问题,可以在单元测试时使用mock测试。

7、文件处理

可以用文件依赖来演示一下单元测试中的依赖问题。

首先创建文件 log.txt,其中内容如下:

创建demo4.go,编写函数用于实现“读取文件的第一行”这一功能。

package main

import (
	"bufio"
	"os"
	"strings"
)

func ReadFirstLine() string {
	// 打开一个文件
	open, err := os.Open("log.txt")

	// defer关键字,用于延迟函数的执行
	// 这里的作用是在函数 ReadFirstLine() 执行结束后即使发生错误或提前返回,
	// 也会确保文件资源 open 被及时关闭,避免资源泄漏。
	defer open.Close()
	// 判断是否发生error
	if err != nil {
		return ""
	}

	// 创建文件扫描器scanner
	scanner := bufio.NewScanner(open)
	for scanner.Scan() {
		// 只读取第一行的内容 返回
		return scanner.Text() // 用于获取scanner当前所在位置的文本内容
	}
	return ""
}

func ProcessFirstLine() string {
	// 读取文件中的行
	line := ReadFirstLine()
	// 把读到的行进行字符串替换,把11替换成00
	destLine := strings.ReplaceAll(line, "11", "00")
	return destLine
}

生成单元测试。同时创建log.txt文件进行文件操作。 

package main

import (
	"github.com/stretchr/testify/assert"
	"testing"
)

func Test(t *testing.T) {
	firstLine := ProcessFirstLine()
	assert.Equal(t, "line00", firstLine) // 别忘了第二个参数是expected,第三个是actual
}

运行:

这里对文件 log.txt 的依赖就是强依赖。

一旦文件被别人篡改,那测试文件可能也会受到影响,甚至无法执行。这样就无法达到单元测试幂等、稳定的目标。


三、Mock测试

Mock(模拟)是一种测试技术,用于在测试代码时替代某些依赖项或功能,以便进行可控制和可预测的测试。这种技术的目的是模拟真实环境中的特定行为,从而使开发人员能够对软件的不同部分进行独立测试,而不需要依赖其他组件的完整性或稳定性。

打桩(Stubbing)是Mock技术的一种应用。它是在单元测试中使用模拟对象(通常称为“桩”或“stub”)代替真实的依赖项或功能,以模拟这些依赖项或功能的行为。打桩的目的是在测试代码的过程中隔离被测试代码,并使测试更简单、可控、可重复和高效。

举个例子,假设有一个函数A,它依赖于函数B的返回结果。在单元测试函数A时,我们可以打桩函数B的行为,使其返回我们预先设定好的值,而不是实际去调用函数B。这样就能够独立地测试函数A的逻辑,而不必担心函数B的实际行为。

演示一下对 demo4.go 中的 ReadFirstLine 进行打桩测试,不再依赖本地文件:

首先引入monkey:bou.ke/monkey

monkey是一个开源的mock测试库,可以对函数或方法进行mock。这是monkey中的打桩函数和卸载打桩函数:

我们就调用它们来实现mock。在测试文件中编写以下代码:

package main

import (
	"bou.ke/monkey"
	"github.com/stretchr/testify/assert"
	"testing"
)

func TestProcessFirstLine(t *testing.T) {
	// 调用打桩函数 打桩函数实现了mock的功能
	// 对ReadFirstLine进行打桩操作,替换函数为输出“line110”
	monkey.Patch(ReadFirstLine, func() string {
		return "line11"
	})
	// 使用defer完成打桩函数的卸载
	defer monkey.Unpatch(ReadFirstLine)
	// 再次获取函数的返回值
	firstLine := ProcessFirstLine()
	// 通过mock,就避免了对log.txt的强依赖。这个单元测试可以在任何时间任何环境去执行
	assert.Equal(t, "line00", firstLine)
}

调用打桩函数,模拟ReadFirstLine函数的功能。这样,在该测试模块中并没有实际调用ReadFirstLine函数,也没有用到 log.txt ,但是也能完成测试。而且不会因为文件遭到破坏而无法验证。


四、基准测试

go中也提供了基准测试的框架。在 Go 语言中,基准测试用于衡量代码的性能(如运行性能和cpu损耗),特别是在处理大量数据时的性能表现。在实际中,当遇到代码性能瓶颈时,为了定位问题经常要对代码做性能分析,这就用到了基准测试。基准测试的使用方法类似于单元测试。

创建demo5.go,这是一个模拟随机选择服务器的程序:

package main

import "math/rand"

var ServerIndex [10]int

func InitServerIndex() {
	for i := 0; i < 10; i++ {
		ServerIndex[i] = i + 100
	}
}

func Select() int {
	// 模拟通过下标来随机选择服务器
	return ServerIndex[rand.Intn(10)]
}

编写测试文件,下面写了两个基准测试函数:

package main

import (
	"testing"
)

func BenchmarkSelect(b *testing.B) {
	InitServerIndex()
	b.ResetTimer() //定时器重置,因为函数InitServerIndex不属于要测试的函数的损耗,所以计时要把这个时间去掉
	for i := 0; i < b.N; i++ {
		Select()
	}
}

// 基准测试也支持并行
func BenchmarkSelectParallel(b *testing.B) {
	InitServerIndex()
	b.ResetTimer()
	b.RunParallel(func(pb *testing.PB) {
		for pb.Next() {
			Select()
		}
	})
}
  1. BenchmarkSelect:这个基准测试函数用于测试 Select 函数的性能。在基准测试开始之前,调用 InitServerIndex 函数进行初始化工作。然后,通过循环运行 Select 函数 b.N 次,b.N 表示测试运行的迭代次数。

  2. BenchmarkSelectParallel:这个基准测试函数也用于测试 Select 函数的性能,不同之处在于它使用了并行测试。通过 b.RunParallel 函数,我们可以在多个 goroutine 中并行运行 Select 函数。这样可以更好地利用多核处理器的性能,加快测试的执行速度。

运行结果: 

基准测试也支持并行执行,但是可见并行去做基准测试的情况下,它的性能退化了。原因是Select中用到了rand函数,而rand为了保证全局的随机性和并发安全,它持有全局锁。因此就降低了并发的性能。

用fastrand可以提升性能。后期如果有随机场景,推荐使用fastrand。

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

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

相关文章

AQS抽象同步队列核心原理

CLH自旋锁 JUC中显式锁基于AQS抽象队列同步器&#xff0c;而AQS是CLH锁的一个变种。队列头结点可以获得锁&#xff0c;其他节点排队等候。 在争夺锁激烈的情况下&#xff0c;为了减少CAS空自旋&#xff08;CAS需要CPU进行内部通信保证缓存一致性造成流量过大引起总线风暴&…

【代码随想录day21】二叉搜索树中的众数

题目 给你一个含重复值的二叉搜索树&#xff08;BST&#xff09;的根节点 root &#xff0c;找出并返回 BST 中的所有 众数&#xff08;即&#xff0c;出现频率最高的元素&#xff09;。 如果树中有不止一个众数&#xff0c;可以按 任意顺序 返回。 假定 BST 满足如下定义&am…

Git移除commit过的大文件

前言&#xff1a;在提交推送本地更改至仓库时&#xff0c;误将大文件给提交了&#xff0c;导致push时报错文件过大&#xff0c;因此需要将已经commit的大文件移除后再push 若已知要删除的文件或文件夹路径&#xff0c;则可以从第4步开始 1.对仓库进行gc操作 $ git gc 2.查询…

ThinkPHP 一对多关联

用一对多关联的前提 多的一方的数据库表有一的一方数据库表的外键。 举例&#xff0c;用户获取自己的所有文章 数据表结构如下 // 用户表 useruser_id - integer // 用户主键name - varchar // 用户名称// 文章表 articlearticle_id - integer // 文章主键title - varchar …

WSL2安装google chrome浏览器

一. 环境: Windows 11 Ubuntu-22.04 二. 安装google-chrome步骤&#xff08;官方文档&#xff09;&#xff1a; 1. 创建文件夹&#xff1a;mkdir chrome 2. 进入目录&#xff1a;cd chrome/ 3. 下载chrome压缩包&#xff1a;sudo wget https://dl.google.com/linux/direct/go…

学习 NestJs 的第一步

安装 NestJS 的先决条件和安装 NestJS NodeJS 的版本需要大于等于 16。 安装 NestJS 的命令是&#xff1a;npm i -g nestjs/cli。 使用命令创建项目 使用 nest new <项目名称> 来创建项目&#xff0c;假如要开启 TS 的严格语法功能的话&#xff0c;可以把--strict 标…

【雕爷学编程】Arduino动手做(93)--- 0.96寸OLED液晶屏模块15

37款传感器与执行器的提法&#xff0c;在网络上广泛流传&#xff0c;其实Arduino能够兼容的传感器模块肯定是不止这37种的。鉴于本人手头积累了一些传感器和执行器模块&#xff0c;依照实践出真知&#xff08;一定要动手做&#xff09;的理念&#xff0c;以学习和交流为目的&am…

OSPF的拓展配置

OSPF的拓展配置 1.手工认证 --- 在OSPF数据包交互中&#xff0c;邻居之间的数据报中将携带认证口令&#xff0c;两边认证口令相同&#xff0c;则意味着身份合法 OSPF的手工认证总共分为三种&#xff1a; 1.接口认证 [r5-GigabitEthernet0/0/0]ospf authenticati…

GB/T 25000.51解读——软件产品的性能效率怎么测?

GB/T 25000.51-2016《软件产品质量要求和测试细则》是申请软件检测CNAS认可一定会用到的一部国家标准。在前面的文章中&#xff0c;我们为大家整体介绍了GB/T 25000.51-2016《软件产品质量要求和测试细则》国家标准的结构和所涵盖的内容以及对软件产品的八大质量特性中的功能性…

fastposter v2.16.0 让海报开发更简单

fastposter v2.16.0 让海报开发更简单 &#x1f525;&#x1f525;&#x1f525; fastposter海报生成器是一款快速开发海报的工具。只需上传一张背景图&#xff0c;在对应的位置放上组件&#xff08;文字、图片、二维&#x1f434;、头像&#xff09; 点击代码直接生成各种语言…

个人信息的编写以及头像的联动

下面这个是导航栏通过on触发的事件 与图片联动 <template><div><ul><li>{{obj.account}}</li><li>{{obj.ctime|dataFormat}}</li><li>{{obj.id}}</li><li>{{obj.userGroup}}</li><div><!-- acti…

VIOOVI精益管理:实现高效运营和持续改进的关键

关于什么是精益化管理这个问题&#xff0c;从字面上理解&#xff0c;“精”为“精良”&#xff0c;“益”为“利益”&#xff0c;意在产品更加精良&#xff0c;利益更加丰厚。而从丰田精益生产中我们可以看出精益化管理绝不是以偏概全的管理&#xff0c;而是全面的结合内部、外…

Linux操作系统~必考面试题⑧

1、pwd 命令 pwd 命令用于查看当前工作目录路径。 实例&#xff1a; 查看当前路径 pwd 查看软链接的实际路径 pwd -P 2、rmdir 命令 从一个目录中删除一个或多个子目录项&#xff0c;删除某目录时也必须具有对其父目录的写权限。 注意&#xff1a;不能删除非空目录实例&…

Java面试笔记

JAVA基础知识 语法结构 1.类 2.属性 3.方法 4.静态代码块 构造器 构造函数&#xff0c;构造类的对象&#xff0c;默认隐式&#xff0c;创建对象&#xff0c;先执行父类构造函数&#xff0c;再执行子类构造函数 父类的super必须在第一行 代码块 优先级最高&#xff0c;只…

pyspark笔记:读取 处理csv文件

pyspark cmd上的命令 1 读取文件 1.1 基本读取方式 注意读取出来的格式是Pyspark DataFrame&#xff0c;不是DataFrame&#xff0c;所以一些操作上是有区别的 1.1.1 format DataFrame spark.read.format("csv").option(name,value).load(path) format表示读取…

硬盘数据恢复怎么做?5步快速恢复数据!

“我的电脑刚买回来没多久&#xff0c;不知为啥硬盘就出现问题了&#xff0c;我很多的数据都丢失了&#xff0c;这种情况进行硬盘数据恢复还有希望吗&#xff1f;希望各位老师给我点意见&#xff01;感谢&#xff01;” 在数字化时代&#xff0c;数据已经成为我们生活中不可或缺…

美团面试官热爱考察的问题:你真的会判断链表环吗?

大家好&#xff0c;我是小米&#xff01;今天我要和大家一起来解析美团面试中经常会遇到的一道经典问题&#xff1a;如何判断链表是否为环形链表&#xff1f;这是一道考察数据结构与算法基础的问题&#xff0c;也是面试中的常客。相信通过这篇文章的学习&#xff0c;你将能够更…

【决策树-鸢尾花分类】

决策树算法简介 决策树是一种基于树状结构的分类与回归算法。它通过对数据集进行递归分割&#xff0c;将样本划分为多个类别或者回归值。决策树算法的核心思想是通过构建树来对数据进行划分&#xff0c;从而实现对未知样本的预测。 决策树的构建过程 决策树的构建过程包括以…

C#,中国福利彩票《刮刮乐》的数学算法(02)——时来运转

1 中国福利彩票 中国福利彩票始于1987年7月27日&#xff0c;以“团结各界热心社会福利事业的人士&#xff0c;发扬社会主义人道主义精神&#xff0c;筹集社会福利资金&#xff0c;兴办残疾人、老年人、孤儿福利事业和帮助有困难的人”、即“扶老、助残、救孤、济困”为宗旨。随…

儿童小学生护眼灯选哪个牌子经济好用?分享五款好用的台灯

近期&#xff0c;经济好用的护眼台灯赶上了热潮&#xff0c;许多家长反应不知道怎么选一款合适有经济的护眼台灯&#xff1f;面对市场上很多鱼龙混杂的台灯&#xff0c;真的是眼花缭乱&#xff0c;选台灯不一定要选贵的&#xff0c;但一定要选对的&#xff0c;今天小编就分享五…