.Net常用單元測試框架MsUnit/XUnit/NUnit,目前主流使用前兩種較多,通常開發(fā)者會在Vs中使用自帶單元測試方式進行單元測試驗證,這種模式一旦需要全量重新跑單元測試時會花費較長時間,而且需要開發(fā)者每修改一部分代碼就要手動執(zhí)行一遍,效率較低下,同時也不利于整體統(tǒng)一質(zhì)量管理;
本文我們將通過Jenkins集成自動完成.NetCore單元測試,并提取單元測試相關(guān)指標(biāo)(如分支覆蓋率,行覆蓋率),將指標(biāo)數(shù)據(jù)推送到我們自己的管理端進行開發(fā)質(zhì)量管理 。
本文使用了MsUnit進行單元測試,您也可以換成XUint同樣支持
1.準(zhǔn)備項目
以下代碼寫的是模擬用戶注冊的功能(16歲后方可注冊),沒可參考價值。
業(yè)務(wù)代碼:
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}");
}
}
單元測試腳本驗證,同一個函數(shù)的不同單元測試返回驗證方式盡量使用同樣的語義,如我們對AddUser返回結(jié)果,均使用AssertIsTrue進行驗證。
2.服務(wù)器環(huán)境準(zhǔn)備
步驟1:coverlet.console安裝
https://www.nuget.org/packages/coverlet.console/
下載最新版本包,目前是1.7.1
上傳到服務(wù)器目錄中(離線安裝需要上傳,如果服務(wù)器有網(wǎng)則不需要)
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拉代碼到服務(wù)器目錄中,然后到代碼所在目錄,我們執(zhí)行如下腳本:
步驟1、2也可以開發(fā)階段由開發(fā)人員添加好相關(guān)包,由于不容易約束,因而我們采用命令統(tǒng)一后期添加。
步驟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:執(zhí)行單元測試并輸出覆蓋率
[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,可以看到產(chǎn)生了三個文件
[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/目錄中,后續(xù)我們將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
上述步驟完成后,我們再次查看生成目錄中的文件,發(fā)現(xiàn)生成了一些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.實現(xiàn)Jenkins自動化
上述過程我們是通過手動腳本模式驗證該過程,現(xiàn)我們計劃將整個過程自動化掉
編寫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("#############################################開始請求單元測試結(jié)果##################################################")
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("#############################################單元測試未啟用,跳過單元測試##################################################")
}
}
執(zhí)行腳本驗證
我們到服務(wù)器上查看,相關(guān)html已生成。
4.部署Nginx查看生成報告
部署一個nginx,將其目錄指向 /root/unit_test/
chmod 777 /root/unit_test/
制作一個nginx的Yaml文件,內(nèi)容如下:
#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 #創(chuàng)建nginx pod
然后我們訪問該項目的報告頁面:
5.結(jié)語
通過該模式我們實現(xiàn)了.netCore程序單元的自動化,并生成相關(guān)報告。
后續(xù)我們還可以擴展功能,提取相關(guān)報告數(shù)據(jù)用于對發(fā)布過程進行跟蹤與管控,進一步規(guī)范開發(fā)及部署過程。