《Go 单元测试从入门到覆盖率提升》(一)
一、静态代码分析
静态代码分析是一种在不执行和运行程序的情况下检查源代码的方法。通过这种方法,自动化工具会扫描代码查找潜在的问题,如bug、安全漏洞或与编码标准的偏离。静态代码分析可以在执行或合并到main分支之前检查源代码中的问题。
动态代码分析是在代码被执行时,识别运行时的问题,例如性能瓶颈、内存泄露和逻辑缺陷。它输出的内容是程序的实际运行时行为,包括日志、跟踪和性能指标。它在实际或模拟环境中与应用程序并行运行或在应用程序内运行,系统性能开销很高。
静态代码分析工具可以集成在CI/CD管道中,可以确保每次提交都会被自动检查有无编码标准错误。
二、CI/CD管道是什么?
CI/CD管道可以理解为:把软件开发、测试、部署整个过程自动化的一条“流水线”。
1、CI(持续集成,Continuous Integration)
- 拉取代码:从git仓库获取最新代码
- 编译 & 构建:mvn clean package -DskipTests=false,使用maven打包,若编译失败,直接停止流程。
- 单元测试:JUnit测试类会自动跑起来,比如测试某个类的方法是否正确。若有测试失败,CI直接报红,开发者收到通知。
- 静态代码检查:用工具扫描代码风格与潜在bug。若发现变量名不规范、SQL注入风险、可能的空指针调用。
- 通过以上检查后,才会进入CD阶段。
2、CD(持续交付/持续部署,Continuous Delivery / Continuous Deployment)
CI成功后,进入部署阶段。
- 构建Docker镜像:docker build -t myshop/order-service:1.0。把Spring Boot项目打成镜像,方便后续在任何环境中执行。
- 推送到镜像仓库:docker push registry.company/myshop/order-service:1.0
- 部署到测试环境(自动执行):在Kubernetes上运行kubectl apply -f order-service.yaml,部署新版本到测试集群,QA团队或自动化测试程序会在测试环境验证。
- 部署到生产环境:若是持续交付,需要运维或开发手动点一下“确认上线”;若是持续集成,管道会直接把新版本发布到生产环境。
3、部署后
监控工具(如普罗米修斯)实时监控服务性能,若新版本有严重bug,可以一键回滚到上一个稳定版本。单元测试需要比其他测试运行得快(相比集成测试、系统测试等更上层的测试),因为单元测试仅验证函数/方法等最小单元的局部逻辑,通过Mock/Stub技术隔离数据库、网络等外部依赖,可以在内存中直接执行,避免资源初始化、IO等耗时操作;且需再开发阶段高频运行。
三、单元测试
1、概念
单元测试(Unit Test)是指对软件中最小的可测试单元进行验证的测试。
在面向对象的程序设计中,单元通常指一个方法(无论是基类、抽象类还是子类中的方法)。它的目的在于确认每个单元是否按照预期正确工作。
(1)编写原则
① 理论原则
-
快:执行速度要快,才能频繁运行。
-
独立:各测试之间互不依赖,顺序不影响结果。
-
单一职责:一个测试用例只验证一个场景。
-
可读性:命名清晰,让人一看就懂。
-
自动化:全程自动执行(不需要用户参与输入,便于持续集成(CI/CD)),结果直接判定为 Pass 或 Fail。
② 规约原则:实际编写代码过程中,不同团队保持自己的规约即可。
-
文件命名:测试文件必须以
_test.go结尾,例如user_service_test.go。 -
方法命名:测试函数必须以
TestXxx开头,例如TestLogin。 -
方法签名:函数参数必须是
t *testing.T。 -
文件位置:测试文件和被测文件应放在同一个包中,方便调用与隔离。
③ 衡量原则:写单元测试是开发者的责任,理论上覆盖率越高越好,但要结合效率进行权衡:
-
核心优先:先覆盖核心组件和关键业务逻辑。
-
热点优先:通用的工具类(util)要写测试,因为使用频率高,出错影响大。
-
代表优先:逻辑相似的模块,只需挑一种典型逻辑写全套测试。
-
独立性:每个测试文件独立,一个测试文件对应一个测试用户;不同用例之间不应互相依赖。
2、设计方法
(1)规范导出法
按照输入规范,把输入分成“合法”和“不合法”两类,每类挑代表值测试。
例:函数要求年龄 18–60 之间,选 30(合法,应该通过),10、70(不合法,应该不通过)。
(2)等价划分法
用“代表值”覆盖不同输入类型,避免重复。
例:年龄合法性检查
-
有效等价类:18 ≤ age ≤ 60 → 选 30 测试,返回 true
-
无效等价类1:age < 18 → 选 10,返回 false
-
无效等价类2:age > 60 → 选 70,返回 false
(3)边界值分析法
错误常出现在边界点,重点测试边界。
例:测试年龄合法性,选 17、18、60、61。
int max(int a, int b) { if (a > b) { return a; } else { return b; } } // 这里需要测两条路径:a > b 和 a <= b
(4)基本路径测试
白盒测试方法,确保每个独立逻辑路径至少执行一次。
经验公式:圈复杂度 CC = if 条件数 + 1。比如有 2 个 if 条件,就至少需要 3 条测试路径。
3、demo实战
在VSCode中新建文件和文件夹,如下图所示。

在calc.go文件中编写我们的功能函数(这里写了两个简单的功能函数,Abs和Add)。

然后在calc文件夹下新建一个以_test为后缀的文件,这里面会编写两个以Test为前缀的测试方法,如下所示。在测试方法内直接调用功能函数,若实际输出与预期输出不一致,则报错。

这时打开终端命令行,在clac文件夹路径下执行go test命令,就会默认执行该包下的所有以_test结尾的文件。这里会执行calc_test.go文件。输入go test -v可以输出更详细的信息。

若只测试文件中的指定测试方法,则使用go test -v -run=xxx(方法名)命令。

查看功能代码对于代码的覆盖情况,go test -cover。如果要查看哪25%的代码没有被覆盖到,就要使用go test -cover 。(注意,这个命令要以管理员身份运行,可以在文件中打开,然后在cmd中运行该命令)



我们发现通过这个out文件没法看出覆盖率的情况,这时候就需要根据这个文件生成HTML格式的覆盖率文件,运行下列命令后,会自动跳转到一个html页面中。

这时可以在calc.go文件中的TestAbs方法中新增一个测试用例来覆盖。再次执行go test -cover后,发现此时的测试覆盖率是100%。


如果在一个测试函数中写过多的零散的测试用例不太合适,这时候要用到测试组,即将多个测试用例汇集到一起,然后使用循环的方式来一个个测试。


若跑测试用例时报错,这时没法很方便看出是哪个测试用例出现了问题,这时候可以用子测试方法,可以清晰看到每一个测试用例的情况。(即给每个用例命名)


这样就可以清楚看到是哪个自测试FAIL了。
未完待续....
浙公网安备 33010602011771号