.Net常用单元测试框架MsUnit/XUnit/NUnit,目前主流使用前两种较多,通常开发者会在Vs中使用自带单元测试方式进行单元测试验证,这种模式一旦需要全量重新跑单元测试时会花费较长时间,而且需要开发者每修改一部分代码就要手动执行一遍,效率较低下,同时也不利于整体统一质量管理;
本文我们将通过Jenkins集成自动完成.NetCore单元测试,并提取单元测试相关指标(如分支覆盖率,行覆盖率),将指标数据推送到我们自己的管理端进行开发质量管理 。
本文使用了MsUnit进行单元测试,您也可以换成XUint同样支持
1.准备项目
以下代码写的是模拟用户注册的功能(16岁后方可注册),没可参考价值。
业务代码:
public class BLLUserService
{
public int AddUser(string userId, string userName, int age)
{
if (string.IsNullOrEmpty(userId)) return 0;
if (age < 5) return -2; //为测试多做了一些场景
if (age < 10) return -1; //为测试多做了一些场景
if (age < 16) return 0;
return 1;
}
}
单元测试代码参考:
public class BLLUserServiceTests
{
[TestMethod()]
public void AddUserTest0()
{
BLLUserService service = new BLLUserService();
int result = service.AddUser("zhangsan", "张三", 20);
Assert.IsTrue(result == 1);
}
[TestMethod()]
public void AddUserTest1()
{
BLLUserService service = new BLLUserService();
int result = service.AddUser("zhangsan", "张三", 15);
Assert.IsTrue(result == 0);
result = service.AddUser("zhangsan", "张三", 16);
Assert.IsTrue(result == 1);
}
[TestMethod()]
public void AddUserTest2()
{
BLLUserService service = new BLLUserService();
int result = service.AddUser("", "张三", 20);
Assert.IsTrue(result == 0);
}
[TestMethod()]
public void AddUserTest3()
{
BLLUserService service = new BLLUserService();
int result = service.AddUser("", "", 20);
Assert.IsTrue(result == 0);
}
[TestMethod()]
public void AddUserTest4()
{
BLLUserService service = new BLLUserService();
int result = service.AddUser("zhangsan", "", 20);
Assert.IsTrue(result == 1);
}
[DataTestMethod]
[DataRow(15)]
[DataRow(10)]
[DataRow(5)]
public void IsPrime_01(int value)
{
BLLUserService service = new BLLUserService();
int result = service.AddUser("zhangsan", "aa", value);
Assert.IsTrue(result<=0, $"{value}");
}
}
单元测试脚本验证,同一个函数的不同单元测试返回验证方式尽量使用同样的语义,如我们对AddUser返回结果,均使用AssertIsTrue进行验证。
2.服务器环境准备
步骤1:coverlet.console安装
https://www.nuget.org/packages/coverlet.console/
下载最新版本包,目前是1.7.1
上传到服务器目录中(离线安装需要上传,如果服务器有网则不需要)
dotnet tool install --global coverlet.console --version 1.7.1
步骤2:安装完毕后 vim /etc/profile 添加 /root/.dotnet/tools到Path目录
/etc/profile添加如下一行即可。
export PATH=$PATH:/root/.dotnet/tools
保存
source /etc/profile 使修改生效
手动验证
git拉代码到服务器目录中,然后到代码所在目录,我们执行如下脚本:
步骤1、2也可以开发阶段由开发人员添加好相关包,由于不容易约束,因而我们采用命令统一后期添加。
步骤1:项目中添加coverlet.msbuild -v 2.8.1包
coverlet.msbuild可以为我们输出单元测试覆盖
[root@k8s-master netCore02]# ls
build docker netCore02.Service netCore02.sln netCore02.UTest README.md unit_test
[root@k8s-master netCore02]# /root/jenkins/tools/dotnetsdk3.1/dotnet add netCore02.UTest/ package coverlet.msbuild -v 2.8.1
Writing /tmp/tmptRfH2t.tmp
info : Adding PackageReference for package 'coverlet.msbuild' into project '/root/jenkins/workspace/netCore02/netCore02.UTest/netCore02.UTest.csproj'.
info : Restoring packages for /root/jenkins/workspace/netCore02/netCore02.UTest/netCore02.UTest.csproj...
info : Package 'coverlet.msbuild' is compatible with all the specified frameworks in project '/root/jenkins/workspace/netCore02/netCore02.UTest/netCore02.UTest.csproj'.
info : PackageReference for package 'coverlet.msbuild' version '2.8.1' updated in file '/root/jenkins/workspace/netCore02/netCore02.UTest/netCore02.UTest.csproj'.
info : Committing restore...
info : Assets file has not changed. Skipping assets file writing. Path: /root/jenkins/workspace/netCore02/netCore02.UTest/obj/project.assets.json
log : Restore completed in 459.72 ms for /root/jenkins/workspace/netCore02/netCore02.UTest/netCore02.UTest.csproj.
步骤2:在项目中添加ReportGenerator -v 4.5.6包
使用ReportGenerator 可以输出比较友好的报告,不是必选
[root@k8s-master netCore02]# /root/jenkins/tools/dotnetsdk3.1/dotnet add netCore02.UTest/ package ReportGenerator -v 4.5.6
Writing /tmp/tmpVocxxx.tmp
info : Adding PackageReference for package 'ReportGenerator' into project '/root/jenkins/workspace/netCore02/netCore02.UTest/netCore02.UTest.csproj'.
info : Restoring packages for /root/jenkins/workspace/netCore02/netCore02.UTest/netCore02.UTest.csproj...
info : Package 'ReportGenerator' is compatible with all the specified frameworks in project '/root/jenkins/workspace/netCore02/netCore02.UTest/netCore02.UTest.csproj'.
info : PackageReference for package 'ReportGenerator' version '4.5.6' updated in file '/root/jenkins/workspace/netCore02/netCore02.UTest/netCore02.UTest.csproj'.
info : Committing restore...
info : Assets file has not changed. Skipping assets file writing. Path: /root/jenkins/workspace/netCore02/netCore02.UTest/obj/project.assets.json
log : Restore completed in 394.22 ms for /root/jenkins/workspace/netCore02/netCore02.UTest/netCore02.UTest.csproj.
[root@k8s-master netCore02]
步骤3:Build项目
[root@k8s-master netCore02]# /root/jenkins/tools/dotnetsdk3.1/dotnet build
Microsoft (R) Build Engine version 16.4.0+e901037fe for .NET Core
Copyright (C) Microsoft Corporation. All rights reserved.
Restore completed in 29.62 ms for /root/jenkins/workspace/netCore02/netCore02.Service/netCore02.Service.csproj.
Restore completed in 34.73 ms for /root/jenkins/workspace/netCore02/netCore02.UTest/netCore02.UTest.csproj.
netCore02.Service -> /root/jenkins/workspace/netCore02/netCore02.Service/bin/Debug/netcoreapp3.1/netCore02.Service.dll
netCore02.UTest -> /root/jenkins/workspace/netCore02/netCore02.UTest/bin/Debug/netcoreapp3.1/netCore02.UTest.dll
Build succeeded.
0 Warning(s)
0 Error(s)
Time Elapsed 00:00:02.30
步骤4:执行单元测试并输出覆盖率
[root@k8s-master netCore02]# /root/jenkins/tools/dotnetsdk3.1/dotnet test . /p:CollectCoverage=true '/p:CoverletOutputFormat="lcov,opencover"' /p:CoverletOutput=/root/unit_test/netCore02/ --logger 'trx;LogFileName=/root/unit_test/netCore02/result.xml' /p:failOnError=true /p:keepLongStdio=true
Test run for /root/jenkins/workspace/netCore02/netCore02.UTest/bin/Debug/netcoreapp3.1/netCore02.UTest.dll(.NETCoreApp,Version=v3.1)
Microsoft (R) Test Execution Command Line Tool Version 16.3.0
Copyright (c) Microsoft Corporation. All rights reserved.
Starting test execution, please wait...
A total of 1 test files matched the specified pattern.
Results File: /root/unit_test/netCore02/result.xml
Test Run Successful.
Total tests: 9
Passed: 9
Total time: 1.4794 Seconds
Calculating coverage result...
Generating report '/root/unit_test/netCore02/coverage.info'
Generating report '/root/unit_test/netCore02/coverage.opencover.xml'
+-------------------+--------+--------+--------+
| Module | Line | Branch | Method |
+-------------------+--------+--------+--------+
| netCore02.Service | 11.11% | 60% | 7.69% |
+-------------------+--------+--------+--------+
+---------+--------+--------+--------+
| | Line | Branch | Method |
+---------+--------+--------+--------+
| Total | 11.11% | 60% | 7.69% |
+---------+--------+--------+--------+
| Average | 11.11% | 60% | 7.69% |
+---------+--------+--------+--------+
此时查看我们目录 /root/unit_test/netCore02,可以看到产生了三个文件
[root@k8s-master netCore02]# ls
coverage.info coverage.opencover.xml result.xml
其中我们可以从coverage.opencover.xml获取单元测试覆盖率
说明:CoverletOutputFormat目前支持这几种格式:
json (default)
lcov
opencover
cobertura
步骤5:生成单元测试报告
#生成在/root/unit_test/netCore02/目录中,后续我们将unit_test通过nginx暴露出来,即可直接查看报告了。
[root@k8s-master netCore02]# /root/jenkins/tools/dotnetsdk3.1/dotnet /root/.nuget/packages/reportgenerator/4.5.6/tools/netcoreapp3.0/ReportGenerator.dll -reports:/root/unit_test/netCore02/coverage.opencover.xml -targetdir:/root/unit_test/netCore02/
2020-05-03T14:52:18: Arguments
2020-05-03T14:52:18: -reports:/root/unit_test/netCore02/coverage.opencover.xml
2020-05-03T14:52:18: -targetdir:/root/unit_test/netCore02/
2020-05-03T14:52:18: Executable: /root/.nuget/packages/reportgenerator/4.5.6/tools/netcoreapp3.0/ReportGenerator.Core.dll
2020-05-03T14:52:18: Working directory: /root/jenkins/workspace/netCore02
2020-05-03T14:52:18: Writing report file '/root/unit_test/netCore02/index.htm'
2020-05-03T14:52:18: Report generation took 0.2 seconds
上述步骤完成后,我们再次查看生成目录中的文件,发现生成了一些Html/js/css文件,这些文件是格式化后的可以直接查看的报告页面。
[root@k8s-master netCore02]# ls /root/unit_test/netCore02
class.js icon_fork.svg icon_search-plus.svg icon_wrench.svg netCore02.Service_Program.htm
coverage.info icon_info-circled.svg icon_sponsor.svg index.htm netCore02.Service_Startup.htm
coverage.opencover.xml icon_minus.svg icon_star.svg main.js netCore02.Service_WeatherForecast.htm
icon_cube.svg icon_plus.svg icon_up-dir_active.svg netCore02.Service_BLLUserService.htm report.css
icon_down-dir_active.svg icon_search-minus.svg icon_up-dir.svg netCore02.Service_HomeController.htm result.xml
3.实现Jenkins自动化
上述过程我们是通过手动脚本模式验证该过程,现我们计划将整个过程自动化掉
编写Jenkins脚本
stage('Unit Test') {
if (unitTest?.trim()) {
println("#############################################开始单元测试##################################################")
withEnv(["DOTNET_ROOT=/root/jenkins/tools/dotnetsdk3.0"]) {
sh(script: '/root/jenkins/tools/' + sdkVersion+ '/dotnet add ' + unitTest + ' package coverlet.msbuild -v 2.8.1',returnStdout: true )
sh(script: '/root/jenkins/tools/' + sdkVersion+ '/dotnet add '+ unitTest + ' package ReportGenerator -v 4.5.6',returnStdout: true )
sh(script: '/root/jenkins/tools/'+sdkVersion+'/dotnet build')
def consoleLog= sh(script: '/root/jenkins/tools/'+sdkVersion+ '/dotnet test . /p:CollectCoverage=true /p:CoverletOutputFormat=\\"lcov,opencover\\" /p:CoverletOutput=\"${WORKSPACE}/unit_test/\" --logger "trx;LogFileName=${WORKSPACE}/unit_test/result.xml" /p:failOnError=true /p:keepLongStdio=true',returnStdout: true )
println("#############################################单元测试完毕##################################################")
println(consoleLog)
println("#############################################开始请求单元测试结果##################################################")
def temp_out
temp_out=sh(script:"ls ./unit_test/coverage.opencover.xml",returnStatus:true)
println(temp_out)
if(temp_out==0)
{
sh(script: '/root/jenkins/tools/'+sdkVersion+ '/dotnet /root/.nuget/packages/reportgenerator/4.5.6/tools/netcoreapp3.0/ReportGenerator.dll -reports::${WORKSPACE}/unit_test/coverage.opencover.xml -targetdir:/root/unit_test/'+service+'/',returnStdout: true )
}
else{
println("##################################coverage.opencover.xml不存在#############################");
sh "exit 1"
}
}
}
else {
println("#############################################单元测试未启用,跳过单元测试##################################################")
}
}
执行脚本验证
我们到服务器上查看,相关html已生成。
4.部署Nginx查看生成报告
部署一个nginx,将其目录指向 /root/unit_test/
chmod 777 /root/unit_test/
制作一个nginx的Yaml文件,内容如下:
#deploy
apiVersion: apps/v1
kind: Deployment
metadata:
name: unit-nginx
namespace: my-system
spec:
selector:
matchLabels:
app: unit-nginx
replicas: 1
template:
metadata:
labels:
app: unit-nginx
spec:
nodeSelector:
kubernetes.io/hostname: k8s-master
containers:
- name: unit-nginx
image: nginx:latest
ports:
- containerPort: 80
volumeMounts:
- mountPath: /usr/share/nginx/html
name: html
- mountPath: /etc/nginx/nginx.conf
name: conf
- mountPath: /etc/nginx/conf.d
name: confd
volumes:
- name: html
hostPath:
path: /root/unit_test
- name: conf
hostPath:
path: /root/unit_test/nginx.conf
- name: confd
hostPath:
path: /root/unit_test/conf.d
---
#service
apiVersion: v1
kind: Service
metadata:
name: unit-nginx
namespace: my-system
spec:
#type: NodePort
ports:
- port: 3080
protocol: TCP
targetPort: 80
#nodePort: 31680
selector:
app: unit-nginx
---
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: unit-nginx
namespace: my-system
spec:
rules:
- host: u.xxx.cn
http:
paths:
- path: /
backend:
serviceName: unit-nginx
servicePort: 3080
kubectl apply -f unit_nginx.yaml #创建nginx pod
然后我们访问该项目的报告页面:
5.结语
通过该模式我们实现了.netCore程序单元的自动化,并生成相关报告。
后续我们还可以扩展功能,提取相关报告数据用于对发布过程进行跟踪与管控,进一步规范开发及部署过程。