代码改变世界

.Net Core单元测试规范

2020-09-16 16:05  y-z-f  阅读(493)  评论(0编辑  收藏  举报

.Net Core单元测试规范

一、 前言

为了有效提升代码质量,保证DevOps的顺利进行。将全面开始采用单元测试进行覆盖,届时单元测试将完全纳入
到完整的持续构建生命周期中做为第一道质量把控的门槛。

二、 预期目标

1. 避免直接异常

考虑到单元测试的细化程度,在代码级别上出现的故障将能够通过单元测试进行快速的挖掘。特别对于部分通
用业务代码,在修改一处后其他关联地方一旦没有及时修改将会导致重大事故,而借助于单元可以完全避免这
类情况的发生。

2. 故障产生前置

由于单元测试不依赖外部其他环境,是完全属于代码级别的测试。所以开发者仅仅通过本地也可以进行测
试,保证在相关功能代码编写完成后立即进行相关测试保证代码在提交前就能够挖掘其中的问题。

3. 质量管控可视化

借助于持续构建,过程中所有的单元测试报告通过采集后将可以有效的反应整个项目中各个开发人员的质
量情况,并通过看板及时有效的反应。

三、 应用层单元测试

1. 相关技术

为了能够快速的进行单元测试,需要相关人员具备一定的技术基础,下面将会给出在当前情况下需要使
用的相关单元测试技术以便于开发人员进行基础的学习同时也能够基于官方文档进行更深入的学习以提
升对于对于复杂情况下的应对能力。

  • Moq
    考虑到当前架构的复杂性,其中大量采用了IOC技术,为了能够实现单元测试需要借助于相关的Mock技
    术对底层或者第三方服务接口的模拟以实现各类环境的验证作用。

  • xUnit
    核心单元测试框架,其提供了简单的操作方式。同时也能够支持dotnet指令直接进行相关测试的论证。

2. 仓储模拟

应用层服务都需要依赖外部的各类存储,为了避免对于外部环境的依赖。对于单元测试来说需要将需
要测试的应用服务依赖的仓储接口进行模拟,以便进行具体的测试工作,这里以如下的仓储接口为例:

/// <summary>
/// 产品仓储接口
/// </summary>
public interface IProductInfoRepositories : IRepository<ProductInfo, string>, ITransientDependency
{
   /// <summary>
   /// 查询产品详情
   /// </summary>
   /// <param name="id">主键</param>
   /// <returns></returns>
   Task<ProductInfo> GetProductInfoAsync(string id);

   /// <summary>
   /// 查询产品列表
   /// </summary>
   /// <returns></returns>
   Task<List<ProductInfo>> GetProductInfoListAsync();

   /// <summary>
   /// 修改产品包年折扣
   /// </summary>
   /// <param name="id">主键</param>
   /// <param name="discount">折扣</param>
   /// <returns></returns>
   Task UpdateProductInfoDiscountAsync(string id, decimal discount);

   /// <summary>
   /// 根据序号查询产品信息
   /// </summary>
   /// <param name="number">产品序号</param>
   /// <returns></returns>
   Task<ProductInfo> GetProductInfoByNumberAsync(string number);
}

确定我们需要进行模拟的仓储接口后,我们需要在对应的测试工程项目下新建Mocks文件
夹并在其中新建文件ProductInfoRepositoryMock类,其中我们需要使用moq进行仓储
接口的模拟,比如对其中的GetProductInfoAsync接口进行模拟:

mock.Setup(x => x.GetProductInfoAsync("01")).Returns(Task.FromResult(new ProductInfo
            {
                Id = "01",
                Number = "2019061800001",
                Name = "TMS全流程物流运输管理系统",
                ImgUrl = "https://avatars2.githubusercontent.com/u/16951448?s=200&v=4",
                Discount = 1
            }));

            mock.Setup(x => x.GetProductInfoAsync("05")).Returns(Task.FromResult(new ProductInfo
            {
                Id = "05",
                Number = "201906180005",
                Name = "PMS供应商自主系统",
                ImgUrl = "https://avatars2.githubusercontent.com/u/16951448?s=200&v=4",
                Discount = (decimal)0.75
            }));

mock.Setup(x => x.GetProductInfoAsync("error")).Returns(Task.FromResult<ProductInfo>(null));

利用Setup方法选择需要进行模拟的接口x => x.GetProductInfoAsync("01"),而其中可以选定特定的参数也可
以通过It.IsAny()方法传入参数,表示任意参数调用该方法均返回相同返回值,否则可以根据入参的不
同从而决定不同的出参。对于出参则通过Returns方法中直接放置需要返回的数据。

3. 应用接口测试

完成以上仓储接口的模拟后,对于ProductInfoService来说需要依赖的外部环境都具备了,这时就可以将该对
象进行实例化:

var productInfoRepositoryMock = new ProductInfoRepositoryMock();
_productInfoService = new ProductInfoService(productInfoRepositoryMock.Create())

完成对象的创建后就可以开始编写具体的单元测试代码了,比如针对上述仓储对接口GetProductInfoAsync进
行了模拟,对应的我们则只能针对应用层接口中的GetProductInfoAsync进行测试,首先进行基本的常规测试:

[Theory]
[InlineData("01")]
[InlineData("05")]
public async Task GetProductInfoAsync_IsNormal(string id)
{
   var item = await _productInfoService.GetProductInfoAsync(id);

   Assert.NotNull(item);
   Assert.Equal(id, item.Id);
   Assert.NotEmpty(item.Name);
   Assert.NotEmpty(item.Number);
   Assert.NotEmpty(item.ImgUrl);
}

从上述代码中可以看到其中存在Theory、InlineData这几个注解属性的使用,这类注解属性用于提供不同参
数情况下的单元测试的论证。单元测试往往不是仅考虑正常业务情况下对代码逻辑的测试,更多的还是对于异
常情况下业务代码的正常工作,所以以下还需要对于不存在该数据的情况进行测试。

[Fact]
public async Task GetProductInfoAsync_IsNull()
{
    var error = await _productInfoService.GetProductInfoAsync("error");

    Assert.Null(error);
}

4. 持续构建

当前我们的整体流水线采用Drone进行管理,所以需要针对.drone.yml增加相关流程节点以支持单元测试
的运行,具体需要增加的配置如下:

- name: UnitTest
  image: harbor.vip56.cn/common/sonar:2.2
  commands:
  - cd test/LogisticsWebsiteUnitTest
  - dotnet test --logger:"trx;LogFileName=unitTestLog.txt"
  when:
   event:
   - pull_request
   - push
   brancch:
   - dev

5. 报告反馈

通过上述持续构建中logger参数可以得知具体的单元测试结果将输出到当前目录下的TestResults
文件夹下,其中大致内容如下:

<UnitTest name="LogisticsWebsiteUnitTest.ProductInfoServiceUnitTest.GetProductInfoAsync_IsNormal(id: &quot;01&quot;)" storage="g:\gitlab\logisticswebsiteback\test\logisticswebsiteunittest\bin\debug\netcoreapp2.0\logisticswebsiteunittest.dll" id="e950b619-318c-720a-d6ae-08d1f3ab96ec">
    <Execution id="61675f18-f972-4f59-901f-68b9f75680de" />
    <TestMethod codeBase="G:\gitlab\logisticswebsiteback\test\LogisticswebsiteUnitTest\bin\Debug\netcoreapp2.0\LogisticsWebsiteUnitTest.dll" adapterTypeName="executor://xunit/VsTestRunner2/netcoreapp" className="LogisticsWebsiteUnitTest.ProductInfoServiceUnitTest" name="LogisticsWebsiteUnitTest.ProductInfoServiceUnitTest.GetProductInfoAsync_IsNormal(id: &quot;01&quot;)" />
</UnitTest>

四、 表现层单元测试

敬请等待