上一篇文章中,我们谈到了通过Roslyn进行代码分析,通过自定义代码扫描规则,将有问题的代码、不符合编码规则的代码扫描出来,禁止签入,提升团队的代码质量。

.NET Core技术研究-通过Roslyn全面提升代码质量

今天我们基于第二篇:基于Roslyn技术,扫描单元测试代码,通过单元测试覆盖率和执行通过率,严控产品提测质量,覆盖率和通过率达不到标准,无法提交测试。

首先,我们先讨论一下,什么是单元测试,单元测试的覆盖率统计。

一、什么是单元测试

单元测试(unit testing),是指对软件中的最小可测试单元进行检查和验证。对于单元测试中单元的含义,一般来说,要根据实际情况去判定其具体含义,如C语言中单元指一个函数,C#里单元指一个类,图形化的软件中可以指一个窗口或一个菜单等。

总的来说,单元就是人为规定的最小的被测功能模块。同时,

单元测试是在软件开发过程中要进行的最低级别的测试活动,软件的独立单元将在与程序的其他部分相隔离的情况下进行测试。

在实际研发中,很多团队浪费大量的时间和精力编写单元测试,那么“好的”单元测试是什么样的呢?

  • 好的单元测试可以覆盖应用程序行为的不同情况和方面
  • 好的单元测试应该结构良好的代码,包含测试准备、执行、断言、测试清理
  • 好的单元测试每个都只测试一个最新的功能、代码单元
  • 好的单元测试是独立和隔离的:什么都不依赖,不访问全局状态,文件系统或数据库。
  • 好的单元测试是可描述的:见名知意。
  • 好的单元测试是可重复执行的:无论何时运行,无论在何处运行都通过。
  • 好的单元测试运行的很快

二、如何管理、评估单元测试的覆盖率和通过率

业界通常的做法有:

1. 代码行数覆盖率
2. 类、方法、条件分支覆盖率
3. 单元测试类型覆盖情况:正常、异常、性能、边界
4. 业务场景覆盖情况

但是会产生对单元测试一些误区、错误理解:

  • 覆盖率数据只能代表你测试过哪些代码,不能代表你是否测试好这些代码。(比如上面第一个除零Bug)
  • 不要过于相信覆盖率数据。
  • 不要只拿语句覆盖率(行覆盖率)来考核研发交付质量
  • 路径覆盖率 > 判定覆盖 > 语句覆盖
  • 开发人员不能盲目追求代码覆盖率,而应该想办法设计更多更好的用例,哪怕多设计出来的用例对覆盖率一点影响也没有

经过内部架构师团队的技术交流和讨论,我们达成了以下共识:

 我们如何写好、用好、管好单元测试?

  • 面:覆盖核心微服务的实现,即:核心重要的微服务必须覆盖单元测试
  • 点:单元测试场景要尽可能地覆盖
  • 结构:单元测试要有完备的断言
  • 类型:单元测试尽可能的覆盖正常、异常、性能、边界
  • 可设计评估:概要设计时,确定并录入功能的单元测试场景,开发完成提测时保证单元测试覆盖率
  • 管理:单元测试情况能全面上报管理起来,以进一步控制开发交付的质量
  • 通过率:100%通过方可发起CI,生成补丁

在此基础上,我们启动了今年单元测试推动工作,主要的方案是这样的:

1. 增加一个单元测试注解,将一些关键的业务属性进行标注、上报,比如:微服务标识、微服务类型、单元测试集、单元测试说明、负责人、单元测试类型(正常、异常、性能、边界等)

2. CI持续集成时,必须运行单元测试工程,通过将单元测试执行结果上报到研发效能平台,保障后续补丁提测时控制单元测试通过率,同时单元测试通过率低于95%,无法生成补丁

3. 单元测试统一在研发效能平台中管理,即支持单元测试信息上报到管理平台中,方便后续代码提测时进行:核心微服务单元测试覆盖率控制

通过以上系统约束+管理规定,实现产品提测质量的控制。如何实现上述三个关键技术点呢?

增加单元测试注解、扫描单元测试注解情况、上报单元测试到研发效能平台。

接下来,第三部分,我们将引入Roslyn来完成单元测试代码分析

三、增加单元测试注解,让单元测试具备更多有价值的信息

正如上面所讲,我们增加一个了单元测试注解,将一些关键的业务属性进行标注、上报,比如:微服务标识、微服务类型、单元测试集、单元测试说明、负责人、单元测试类型(正常、异常、性能、边界等)。

UnitTestAttribute

有了自定义单元测试注解后,我们将这个单元测试注解,打到了单元测试方法上:

单元测试方法有了更多的业务信息之后,我们就可以基于Roslyn实现单元测试代码分析了。

四、基于Roslyn实现单元测试代码分析

现有的业务代码到底有多少单元测试,是否全部完成了UnitTest单元测试注解改造,这个统计工作很重要。

这里就用到了Roslyn代码分析技术,大家可以参考第一篇中对Roslyn的详细介绍:.NET Core技术研究-通过Roslyn全面提升代码质量

基于Roslyn实现单元测试代码分析,并将分析后的结果上报到研发效能平台,这样就实现了单元测试数据集中管理,方便后续分析和改进。

通过Roslyn实现单元测试方法的分析过程主要有:

① 创建一个编译工作区MSBuildWorkspace.Create()

② 打开解决方案文件OpenSolutionAsync(slnPath);

③ 遍历Project中的Document

④ 拿到代码语法树、找到所有的方法

⑤ 判断方法是否有UnitTest注解,如果有,将单元测试注解信息统计并上报

看一下实际的代码:

 public async Task<List<CodeCheckResult>> CheckSln(string slnPath)
{
var results = new List<CodeCheckResult>();
try
{
var slnFile = new FileInfo(slnPath); var solution = await MSBuildWorkspace.Create().OpenSolutionAsync(slnPath); if (solution.Projects != null && solution.Projects.Count() > 0)
{
foreach (var project in solution.Projects.ToList())
{
var documents = project.Documents.Where(x => x.Name.Contains(".cs")); foreach (var document in documents)
{
var tree = await document.GetSyntaxTreeAsync();
var root = tree.GetCompilationUnitRoot();
if (root.Members == null || root.Members.Count == 0) continue;
//member
var classDeclartions = root.DescendantNodes().Where(i => i is ClassDeclarationSyntax); foreach (var classDeclare in classDeclartions)
{
var programDeclaration = classDeclare as ClassDeclarationSyntax;
if (programDeclaration == null) continue; foreach (var method in programDeclaration.Members)
{
if (method.GetType() != typeof(MethodDeclarationSyntax)) continue; //方法 Method
var methodDeclaration = (MethodDeclarationSyntax)method;
var testAnnotations = methodDeclaration.AttributeLists.Where(i => i.Attributes.FirstOrDefault(a => a.Name.GetText().ToString() == "TestMethod") != null);
var teldUnitTestAnnotation = methodDeclaration.AttributeLists.FirstOrDefault(i => i.Attributes.FirstOrDefault(a => a.Name.GetText().ToString() == "UnitTest") != null); if (testAnnotations.Count() > 0)
{
var result = new UnitTestCodeCheckResult()
{
Sln = slnFile.Name,
ProjectName = project.Name,
ClassName = programDeclaration.Identifier.Text,
MethodName = methodDeclaration.Identifier.Text,
}; if (methodDeclaration.Body.GetText().Lines.Count <= 3)
{
result.IsEmptyMethod = true;
} var methodBody = methodDeclaration.Body.GetText().ToString();
methodBody = methodBody.Replace("{", "");
methodBody = methodBody.Replace("}", "");
methodBody = methodBody.Replace(" ", "");
methodBody = methodBody.Replace("\r\n", "");
if (methodBody.Length == 0)
{
result.IsEmptyMethod = true;
} if (teldUnitTestAnnotation != null)
{
result.IsTeldUnitTest = true;
var args = teldUnitTestAnnotation.Attributes.FirstOrDefault().ArgumentList.Arguments;
result.UnitTestCase = args[0].GetText().ToString();
result.SeqNo = args[1].GetText().ToString();
result.UnitTestName = args[2].GetText().ToString();
result.UserName = args[3].GetText().ToString();
result.ServiceType = args[4].GetText().ToString().Replace(" ", ""); if (args.Count >= 7)
{
result.ServiceID = args[5].GetText().ToString();
result.UnitTestType = args[6].GetText().ToString();
}
} results.Add(result);
}
}
}
}
}
} return results;
}
catch (Exception e)
{
Console.WriteLine(e.ToString());
return results;
}
}

  上述代码中,最关键的是以下两句:

 var methodDeclaration = (MethodDeclarationSyntax)method;
var testAnnotations = methodDeclaration.AttributeLists.Where(i => i.Attributes.FirstOrDefault(a => a.Name.GetText().ToString() == "TestMethod") != null);
var teldUnitTestAnnotation = methodDeclaration.AttributeLists.FirstOrDefault(i => i.Attributes.FirstOrDefault(a => a.Name.GetText().ToString() == "UnitTest") != null); 快速定位到打了UnitTest注解的单元测试方法,然后将注解的信息扫描上报:
if (teldUnitTestAnnotation != null)
{
result.IsTeldUnitTest = true;
var args =
teldUnitTestAnnotation.Attributes.FirstOrDefault().ArgumentList.Arguments;
result.UnitTestCase = args[0].GetText().ToString();
result.SeqNo = args[1].GetText().ToString();
result.UnitTestName = args[2].GetText().ToString();
result.UserName = args[3].GetText().ToString();
result.ServiceType = args[4].GetText().ToString().Replace(" ", ""); if (args.Count >= 7)
{
result.ServiceID = args[5].GetText().ToString();
result.UnitTestType = args[6].GetText().ToString();
}
}

  具体上报的代码在这里不做详细的描述了,大致的思路就是通过HttpClient发送JSON数据到研发效能平台中。

完成单元测试上报后,研发效能平台中就有了单元测试的基础信息了,基于这个数据,就可以实现核心微服务单元测试覆盖率统计和控制了。

然后,如何统计单元测试的执行通过率呢?

五、统计上报单元测试执行情况,并控制补丁是否满足提测要求

上一个章节中,我们提到了“代码check in后,开发人员可以通过CI触发持续构建,生成补丁,在这个CI过程中,按照要求必须添加一步单元测试扫描的工作”,同时,CI的过程中必须执行单元测试。如何获取到单元测试的执行结果?

这里我们增加了一个单元测试父类:TUnitTest,在父类中实现了单元测试执行结果统计和上报:

    /// <summary>
/// 单元测试基类,业务单元测试继承该类
/// </summary>
[TestClass]
public abstract class TUnitTest
{
bool isReportData = true;
/// <summary>
/// 构造函数
/// </summary>
public TUnitTest()
{
//
//TODO: 在此处添加构造函数逻辑
//
} private TestContext testContextInstance; public System.Diagnostics.Stopwatch _stopWatch; /// <summary>
///获取或设置测试上下文,该上下文提供
///有关当前测试运行及其功能的信息。
///</summary>
public TestContext TestContext
{
get
{
return testContextInstance;
}
set
{
testContextInstance = value;
}
} #region 附加测试特性 /// <summary>
/// 在每个测试运行完之后,使用 TestCleanup 来运行代码
/// </summary>
[TestCleanup()]
public virtual void TestCleanup()
{
var recordFilePath = "";
if (!isReportData)
{
Console.WriteLine("TESTREPORTIPS:TestCleanup设置为不上报,请检查TestInitialize代码处理");
return;
}
if (this.TestContext != null)
{
try
{
var tt = this.GetType();
var testClass = this.TestContext.FullyQualifiedTestClassName;
var type = this.GetType(); var tenantID = Convert.ToString(this.TestContext.Properties["TenantID"]);
var batchID = Convert.ToString(this.TestContext.Properties["BatchID"]); var testMethod = type.GetMethod(this.TestContext.TestName);
if (testMethod != null)
{
var utAttr = testMethod.GetCustomAttributes(false).FirstOrDefault(i => i.GetType() == typeof(UnitTestAttribute));
if (utAttr != null)
{
var unitTestAttr = utAttr as UnitTestAttribute;
var testcase = new UnitTestCase
{
SequenceNumber = unitTestAttr.SequenceNumber,
Description = unitTestAttr.Description,
Passed = this.TestContext.CurrentTestOutcome == UnitTestOutcome.Passed,
ExecuteTime = Convert.ToDateTime(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")),
HostName = System.Net.Dns.GetHostName(),
ServiceID = unitTestAttr.ServiceID,
ServiceType = unitTestAttr.ServiceType,
Tag = unitTestAttr.Tag,
UnitTestName = unitTestAttr.UnitTestName,
UserName = unitTestAttr.UserName,
TestSuiteCode = unitTestAttr.TestSuiteCode,
UnitTestAssembly = tt.Assembly.FullName,
UnitTestClass = testClass,
UnitTestMethod = testMethod.Name,
UnitTestType = unitTestAttr.UnitTestType,
TenantID = tenantID,
BatchID = batchID, };
testcase.TestFile = UnitTestUtil.GetUnitTestFile(tt.Assembly.Location,ref recordFilePath);
if (_stopWatch != null)
testcase.Duration = Math.Round(_stopWatch.Elapsed.TotalSeconds, 2);
UnitTestCaseManager.Report(testcase);
Console.WriteLine($"TestCleanup执行{testcase.TestSuiteCode}-{testcase.SequenceNumber}-{testcase.UnitTestName}上报完成");
}
else
{
Console.WriteLine($"TESTREPORTIPS:TestCleanup执行上报时测试方法{testMethod.Name}未配置UnitTestAttribute的注解");
}
}
else
{
Console.WriteLine($"TESTREPORTIPS:TestCleanup执行上报时未能通过{this.TestContext.TestName}获取到测试方法");
}
}
catch (Exception ex)
{
if (!string.IsNullOrEmpty(recordFilePath) && File.Exists(recordFilePath))
{
try
{
File.Delete(recordFilePath);
}
catch { }
}
Console.WriteLine("TestCleanup执行异常: " + ex.ToString());
}
finally
{
if (_stopWatch != null)
_stopWatch = null;
}
}
else
{
Console.WriteLine("TESTREPORTIPS:TestCleanup执行异常:context为空");
}
}
}

 如上代码所述,在每个测试运行完之后,TestCleanup 方法中进行了以下操作:

① 获取当前单元测试方法的自定义UnitTest注解信息

② 获取单元测试执行是否通过

③ 获取单元测试代码内容,这一步可以做一些Assert检查,方法是否为空检查,实现有效的单元测试代码合理性控制

④ 将单元测试执行信息上报到研发效能平台

⑤ 完成输出一些提示信息,方便排查问题

有了这个父类后,所有的单元测试类,都继承与TUnitTest,实现单元测试执行情况上报。

单元测试执行通过率如果低于某个设置值的话,可以控制在CI的过程中是否生产补丁。

同时,研发效能平台中,有了单元测试数据,单元测试注解改造数据,单元测试执行数据,可以实现补丁提测前二次质量控制:即单元测试覆盖率和执行通过率控制,如果达不到要求,补丁无法提测,进而实现产品提测质量的控制。

以上是如何写好、用好、管好单元测试,基于Roslyn分析单元测试,严控产品提测质量的一些实践分享。

周国庆

2020/5/11

如何写好、管好单元测试?基于Roslyn+CI分析单元测试,严控产品提测质量的更多相关文章

  1. .NET Core技术研究-通过Roslyn代码分析技术规范提升代码质量

    随着团队越来越多,越来越大,需求更迭越来越快,每天提交的代码变更由原先的2位数,暴涨到3位数,每天几百次代码Check In,补丁提交,大量的代码审查消耗了大量的资源投入. 如何确保提交代码的质量和提 ...

  2. [转载] JaCoCo:分析单元测试覆盖率的利器

    转载自http://www.ibm.com/developerworks/cn/java/j-lo-jacoco/和http://www.cnblogs.com/chenfengmugu/p/4937 ...

  3. 象写程序一样写博客:搭建基于github的博客

    象写程序一样写博客:搭建基于github的博客   前言 github 真是无所不能.其 Pages 功能 支持上传 html,并且在页面中显示.于是有好事者做了一个基于 github 的博客管理工具 ...

  4. unit vs2017基于nunit framework创建单元测试

    unit  vs2017基于nunit framework创建单元测试 一.简叙: 单元测试大型项目中是必备的,所以不可忽视,一个项目的成败就看是否有单元测试,对后期的扩展维护都带来了便利. 二.安装 ...

  5. 基于 Roslyn 实现动态编译

    基于 Roslyn 实现动态编译 Intro 之前做的一个数据库小工具可以支持根据 Model 代码文件生成创建表的 sql 语句,原来是基于 CodeDom 实现的,最近改成使用基于 Roslyn ...

  6. 基于roslyn的动态编译库Natasha

    人老了,玩不转博客园的编辑器,详细信息转到:https://mp.weixin.qq.com/s/1r6YKBkyovQSMUgfm_VxBg 关键字:Github, NCC, Natasha,Ros ...

  7. 基于 Roslyn 实现一个简单的条件解析引擎

    基于 Roslyn 实现一个简单的条件解析引擎 Intro 最近在做一个勋章的服务,我们想定义一些勋章的获取条件,满足条件之后就给用户颁发一个勋章,定义条件的时候会定义需要哪些参数,参数的类型,获取勋 ...

  8. [转] 基于Gitlab CI搭建持续集成环境

    [From] https://blog.csdn.net/wGL3k77y9fR1k61T1aS/article/details/78798577 前言 本文是在12月12号迅雷@赵兵在前端早读课第三 ...

  9. 基于存储过程的MVC开源分页控件--LYB.NET.SPPager

    摘要 现在基于ASP.NET MVC的分页控件我想大家都不陌生了,百度一下一大箩筐.其中有不少精品,陕北吴旗娃杨涛大哥做的分页控件MVCPager(http://www.webdiyer.com/)算 ...

随机推荐

  1. 如何用python批量生成真实的手机号码

    前言 文的文字及图片来源于网络,仅供学习.交流使用,不具有任何商业用途,版权归原作者所有,如有问题请及时联系我们以作处理. 作者:Python测试社区 1目 标 场 景 平时在工作过程中,偶尔会需要大 ...

  2. How Many Answers Are Wrong HDU - 3038 (经典带权并查集)

    题目大意:有一个区间,长度为n,然后跟着m个子区间,每个字区间的格式为x,y,z表示[x,y]的和为z.如果当前区间和与前面的区间和发生冲突,当前区间和会被判错,问:有多少个区间和会被判错. 题解:x ...

  3. [转载]MySQL中int(11)最大长度是多少?

    原文地址:https://blog.csdn.net/allenjay11/article/details/76549503 今天在添加数据的时候,发现当数据类型为 int(11) 时,我当时让用户添 ...

  4. js的中文英文排序

    本例主要实现 中文汉字按拼音排序的方法和英文按照首字母排序的方法. //要排序的数据 let data = [ {chinese: '蔡司', english: 'Chase'}, {chinese: ...

  5. 2020新Asp.NET敏捷快速开发框架7.0.5旗舰版源码asp.net mvc框架,工具类CRM,工作流

    演示地址: http://frame3.diytassel.com  用户名:system  密码:0000    需要的联系QQ:22539134 一.新添加了 1.多语言功能: 2.代码生成器模版 ...

  6. 【考试总结】欢乐模拟赛_Day1

    \(T1\) 题目描述 给出一个 \(n × n\) 的, 元素为自然数的矩阵. 这个矩阵有许许多多个子矩阵, 定义它的所有子矩阵形成的集合为 \(S\) . 对于一个矩阵 \(k\) , 定义 \( ...

  7. 使用NLP从文章中自动提取关键字

    背景 在研究和新闻文章中,关键词构成了一个重要的组成部分,因为它们提供了文章内容的简洁表示.关键词在从信息检索系统,书目数据库和搜索引擎优化中定位文章方面也起着至关重要的作用.关键词还有助于将文章分类 ...

  8. 利用 PhpQuery 随机爬取妹子图

    前言 运行下面的代码会随机得到妹子图的一张图片,代码中的phpQuery可以在这里下载:phpQuery-0.9.5.386.zip <?php require 'phpQuery.php'; ...

  9. 关于unix环境高级编程、Linux程序设计两部书浅谈

    unix环境高级编程的术语很多,概念内容,也很多,不过学习概念性质.标准规则类的东西,想必都是这样吧——需要进行拓展的内容很多. Linux程序设计,图文并茂,代码量够足,看起来,感觉难度还可以. l ...

  10. Spring Cloud 系列之 Sleuth 链路追踪(三)

    本篇文章为系列文章,未读前几集的同学请猛戳这里: Spring Cloud 系列之 Sleuth 链路追踪(一) Spring Cloud 系列之 Sleuth 链路追踪(二) 本篇文章讲解 Sleu ...