热门标签 | HotTags
当前位置:  开发笔记 > 前端 > 正文

浅谈.NetCore后端单元测试的实现

这篇文章主要介绍了浅谈.NetCore后端单元测试的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧

1. 前言

单元测试一直都是"好处大家都知道很多,但是因为种种原因没有实施起来"的一个老大难问题。具体是否应该落地单元测试,以及落地的程度, 每个项目都有自己的情况。

本篇为个人认为"如何更好地写单元测试", 即更加 偏向实践向 中夹杂一些理论的分享。

下列示例的单元测试框架为 xUnit , Mock库为 Moq

2. 为什么需要单元测试

优点有很多, 这里提两点我个人认为的很明显的好处

2.1 防止回归

通常在进行新功能/模块的开发或者是重构的时候,测试会进行回归测试原有的已存在的功能,以验证以前实现的功能是否仍能按预期运行。

使用单元测试,可在每次生成后,甚至在更改一行代码后重新运行整套测试, 从而可以很大程度减少回归缺陷。

2.2 减少代码耦合

当代码紧密耦合或者一个方法过长的时候,编写单元测试会变得很困难。当不去做单元测试的时候,可能代码的耦合不会给人感觉那么明显。为代码编写测试会自然地解耦代码,变相提高代码质量和可维护性。

3. 基本原则和规范

 3.1 3A原则

3A分别是"arrange、act、assert", 分别代表一个合格的单元测试方法的三个阶段

  • 事先的准备
  • 测试方法的实际调用
  • 针对返回值的断言

一个单元测试方法可读性是编写测试时最重要的方面之一。 在测试中分离这些操作会明确地突出显示调用代码所需的依赖项、调用代码的方式以及尝试断言的内容.

所以在进行单元测试的编写的时候, 请使用注释标记出3A的各个阶段的, 如下示例

[Fact]
public async Task VisitDataCompressExport_ShouldReturnEmptyResult_WhenFileTokenDoesNotExist()
{
  // arrange
  var mockFiletokenStore = new Mock();
  mockFiletokenStore
    .Setup(it => it.Get(It.IsAny()))
    .Returns(string.Empty);

  var cOntroller= new StatController(
    mockFiletokenStore.Object,
    null);

  // act
  var actual = await controller.VisitDataCompressExport("faketoken");

  // assert
  Assert.IsType(actual);
}

3.2 尽量避免直接测试私有方法

尽管私有方法可以通过反射进行直接测试,但是在大多数情况下,不需要直接测试私有的private方法, 而是通过测试公共public方法来验证私有的private方法。

可以这样认为:private方法永远不会孤立存在。更应该关心的是调用private方法的public方法的最终结果。

3.3 重构原则

如果一个类/方法,有很多的外部依赖,造成单元测试的编写困难。那么应该考虑当前的设计和依赖项是否合理。是否有部分可以存在解耦的可能性。选择性重构原有的方法,而不是硬着头皮写下去.

3.4 避免多个断言

如果一个测试方法存在多个断言,可能会出现某一个或几个断言失败导致整个方法失败。这样不能从根本上知道是了解测试失败的原因。

所以一般有两种解决方案

  • 拆分成多个测试方法
  • 使用参数化测试, 如下示例
[Theory]
[InlineData(null)]
[InlineData("a")]
public void Add_InputNullOrAlphabetic_ThrowsArgumentException(string input)
{
  // arrange
  var stringCalculator = new StringCalculator();

  // act
  Action actual = () => stringCalculator.Add(input);

  // assert
  Assert.Throws(actual);
}

当然如果是对对象进行断言, 可能会对对象的多个属性都有断言。此为例外。

3.5 文件和方法命名规范 文件名规范

一般有两种。比如针对 UserController 下方法的单元测试应该统一放在 UserControllerTest 或者 UserController_Test

单元测试方法名

单元测试的方法名应该具有可读性,让整个测试方法在不需要注释说明的情况下可以被读懂。格式应该类似遵守如下

<被测试方法全名>_<期望的结果>_<给予的条件>

// 例子
[Fact]
public void Add_InputNullOrAlphabetic_ThrowsArgumentException()
{
 ...
}

4. 常用类库介绍

4.1 xUnit/MsTest/NUnit

编写.Net Core的单元测试绕不过要选择一个单元测试的框架, 三大单元测试框架中

  • MsTest是微软官方出品的一个测试框架
  • NUnit没用过
  • xUnit是.Net Foundation下的一个开源项目,并且被dotnet github上很多仓库(包括runtime)使用的单元测试框架

三大测试框架发展至今已是大差不差, 很多时候选择只是靠个人的喜好。

个人偏好 xUnit 简洁的断言

// xUnit
Assert.True()
Assert.Equal()

// MsTest
Assert.IsTrue()
Assert.AreEqual()

客观地功能性地分析三大框架地差异可以参考如下

https://anarsolutions.com/automated-unit-testing-tools-comparison

4.2 Moq

官方仓库

https://github.com/moq/moq4

Moq是一个非常流行的模拟库, 只要有一个接口它就可以动态生成一个对象, 底层使用的是Castle的动态代理功能.

基本用法

在实际使用中可能会有如下场景

public class UserController
{
  private readonly IUserService _userService;
  
  public UserController(IUserService userService)
  {
    _userService = userService;
  }
  
  [HttpGet("{id}")]
  public IActionResult GetUser(int id)
  {
    var user = _userService.GetUser(id);
    
    if (user == null)
    {
      return NotFound();
    }
    else
    {
      ...
    }
  }
}

在进行单元测试的时候, 可以使用 Moq_userService.GetUser 进行模拟返回值

[Fact]
public void GetUser_ShouldReturnNotFound_WhenCannotFoundUser()
{
  // arrange
  // 新建一个IUserService的mock对象
  var mockUserService = new Mock();
  // 使用moq对IUserService的GetUs方法进行mock: 当入参为233时返回null
  mockUserService
   .Setup(it => it.GetUser(233))
   .Return((User)null);
  var cOntroller= new UserController(mockUserService.Object);
  
  // act
  var actual = controller.GetUser(233) as NotFoundResult;
  
  // assert
  // 验证调用过userService的GetUser方法一次,且入参为233
  mockUserService.Verify(it => it.GetUser(233), Times.AtMostOnce());
}

4.3 AutoFixture

官方仓库

https://github.com/AutoFixture/AutoFixture

AutoFixture是一个假数据填充库,旨在最小化3A中的 arrange 阶段,使开发人员更容易创建包含测试数据的对象,从而可以更专注与测试用例的设计本身。

基本用法

直接使用如下的方式创建强类型的假数据

[Fact]
public void IntroductoryTest()
{
  // arrange
  Fixture fixture = new Fixture();

  int expectedNumber = fixture.Create();
  MyClass sut = fixture.Create();
  
  // act
  int result = sut.Echo(expectedNumber);
  
  // assert
  Assert.Equal(expectedNumber, result);
}

上述示例也可以和测试框架本身结合,比如xUnit

[Theory, AutoData]
public void IntroductoryTest(
  int expectedNumber, MyClass sut)
{
  // act
  int result = sut.Echo(expectedNumber);
  
  // assert
  Assert.Equal(expectedNumber, result);
}

5. 实践中结合Visual Studio的使用

Visual Studio提供了完备的单元测试的支持,包括运行. 编写. 调试单元测试。以及查看单元测试覆盖率等。

5.1 如何在Visual Studio中运行单元测试

5.2 如何在Visual Studio中查看单元测试覆盖率

如下功能需要Visual Studio 2019 Enterprise版本,社区版不带这个功能。

如何查看覆盖率

  • 在测试窗口下,右键相应的测试组 点
  • 点击如下的"分析代码覆盖率"

6. 实践中常见场景的Mock

主要

6.1 DbSet

使用EF Core过程中,如何mock DbSet是一个绕不过的坎。

方法一

参考如下链接的回答进行自行封装

https://stackoverflow.com/questions/31349351/how-to-add-an-item-to-a-mock-dbset-using-moq

方法二(推荐)

使用现成的库(也是基于上面的方式封装好的)

仓库地址:

https://github.com/romantitov/MockQueryable

使用范例

// 1. 测试时创建一个模拟的List
var users = new List()
{
 new UserEntity{LastName = "ExistLastName", DateOfBirth = DateTime.Parse("01/20/2012")},
 ...
};

// 2. 通过扩展方法转换成DbSet
var mockUsers = users.AsQueryable().BuildMock();

// 3. 赋值给给mock的DbContext中的Users属性
var mockDbCOntext= new Mock();
mockDbContext
 .Setup(it => it.Users)
 .Return(mockUsers);

6.2 HttpClient

使用RestEase/Refit的场景

如果使用的是 RestEase 或者 Refit 等第三方库,具体接口的定义本质上就是一个interface,所以直接使用moq进行方法mock即可。

并且建议使用这种方式。

IHttpClientFactory

如果使用的是.Net Core自带的 IHttpClientFactory 方式来请求外部接口的话,可以参考如下的方式对 IHttpClientFactory 进行mock

https://www.thecodebuzz.com/unit-test-mock-httpclientfactory-moq-net-core/

6.3 ILogger

由于ILogger的LogError等方法都是属于扩展方法,所以不需要特别的进行方法级别的mock。

针对平时的一些使用场景封装了一个帮助类, 可以使用如下的帮助类进行Mock和Verify

public static class LoggerHelper
{
  public static Mock> LoggerMock() where T : class
  {
    return new Mock>();
  }

  public static void VerifyLog(this Mock> loggerMock, LogLevel level, string containMessage, Times times)
  {
    loggerMock.Verify(
    x => x.Log(
      level,
      It.IsAny(),
      It.Is((o, t) => o.ToString().Contains(containMessage)),
      It.IsAny(),
      (Func)It.IsAny()),
    times);
  }

  public static void VerifyLog(this Mock> loggerMock, LogLevel level, Times times)
  {
    loggerMock.Verify(
    x => x.Log(
      level,
      It.IsAny(),
      It.IsAny(),
      It.IsAny(),
      (Func)It.IsAny()),
    times);
  }
}

使用方法

[Fact]
public void Echo_ShouldLogInformation()
{
  // arrange
  var mockLogger = LoggerHelpe.LoggerMock();
  var cOntroller= new UserController(mockLogger.Object);
  
  // act
  controller.Echo();
  
  // assert
  mockLogger.VerifyLog(LogLevel.Information, "hello", Times.Once());
}

7. 拓展

7.1 TDD介绍

TDD是测试驱动开发(Test-Driven Development)的英文简称. 一般是先提前设计好单元测试的各种场景再进行真实业务代码的编写,编织安全网以便将Bug扼杀在在摇篮状态。

此种开发模式以测试先行,对开发团队的要求较高, 落地可能会存在很多实际困难。详细说明可以参考如下

https://www.guru99.com/test-driven-development.html

参考链接

https://docs.microsoft.com/en-us/dotnet/core/testing/unit-testing-best-practices

https://www.kiltandcode.com/2019/06/16/best-practices-for-writing-unit-tests-in-csharp-for-bulletproof-code/

https://github.com/AutoFixture/AutoFixture

到此这篇关于浅谈.Net Core后端单元测试的实现的文章就介绍到这了,更多相关.Net Core 单元测试内容请搜索以前的文章或继续浏览下面的相关文章希望大家以后多多支持!


推荐阅读
  • EPICS Archiver Appliance存储waveform记录的尝试及资源需求分析
    本文介绍了EPICS Archiver Appliance存储waveform记录的尝试过程,并分析了其所需的资源容量。通过解决错误提示和调整内存大小,成功存储了波形数据。然后,讨论了储存环逐束团信号的意义,以及通过记录多圈的束团信号进行参数分析的可能性。波形数据的存储需求巨大,每天需要近250G,一年需要90T。然而,储存环逐束团信号具有重要意义,可以揭示出每个束团的纵向振荡频率和模式。 ... [详细]
  • 这是原文链接:sendingformdata许多情况下,我们使用表单发送数据到服务器。服务器处理数据并返回响应给用户。这看起来很简单,但是 ... [详细]
  • 目录实现效果:实现环境实现方法一:基本思路主要代码JavaScript代码总结方法二主要代码总结方法三基本思路主要代码JavaScriptHTML总结实 ... [详细]
  • Centos7.6安装Gitlab教程及注意事项
    本文介绍了在Centos7.6系统下安装Gitlab的详细教程,并提供了一些注意事项。教程包括查看系统版本、安装必要的软件包、配置防火墙等步骤。同时,还强调了使用阿里云服务器时的特殊配置需求,以及建议至少4GB的可用RAM来运行GitLab。 ... [详细]
  • 禁止程序接收鼠标事件的工具_VNC Viewer for Mac(远程桌面工具)免费版
    VNCViewerforMac是一款运行在Mac平台上的远程桌面工具,vncviewermac版可以帮助您使用Mac的键盘和鼠标来控制远程计算机,操作简 ... [详细]
  • 阿里Treebased Deep Match(TDM) 学习笔记及技术发展回顾
    本文介绍了阿里Treebased Deep Match(TDM)的学习笔记,同时回顾了工业界技术发展的几代演进。从基于统计的启发式规则方法到基于内积模型的向量检索方法,再到引入复杂深度学习模型的下一代匹配技术。文章详细解释了基于统计的启发式规则方法和基于内积模型的向量检索方法的原理和应用,并介绍了TDM的背景和优势。最后,文章提到了向量距离和基于向量聚类的索引结构对于加速匹配效率的作用。本文对于理解TDM的学习过程和了解匹配技术的发展具有重要意义。 ... [详细]
  • Lodop中特殊符号打印设计和预览样式不同的问题解析
    本文主要解析了在Lodop中使用特殊符号打印设计和预览样式不同的问题。由于调用的本机ie引擎版本可能不同,导致在不同浏览器下样式解析不同。同时,未指定文字字体和样式设置也会导致打印设计和预览的差异。文章提出了通过指定具体字体和样式来解决问题的方法,并强调了以打印预览和虚拟打印机测试为准。 ... [详细]
  • Final关键字的含义及用法详解
    本文详细介绍了Java中final关键字的含义和用法。final关键字可以修饰非抽象类、非抽象类成员方法和变量。final类不能被继承,final类中的方法默认是final的。final方法不能被子类的方法覆盖,但可以被继承。final成员变量表示常量,只能被赋值一次,赋值后值不再改变。文章还讨论了final类和final方法的应用场景,以及使用final方法的两个原因:锁定方法防止修改和提高执行效率。 ... [详细]
  • 本文介绍了求解gcdexgcd斐蜀定理的迭代法和递归法,并解释了exgcd的概念和应用。exgcd是指对于不完全为0的非负整数a和b,gcd(a,b)表示a和b的最大公约数,必然存在整数对x和y,使得gcd(a,b)=ax+by。此外,本文还给出了相应的代码示例。 ... [详细]
  • 本文介绍了在开发Android新闻App时,搭建本地服务器的步骤。通过使用XAMPP软件,可以一键式搭建起开发环境,包括Apache、MySQL、PHP、PERL。在本地服务器上新建数据库和表,并设置相应的属性。最后,给出了创建new表的SQL语句。这个教程适合初学者参考。 ... [详细]
  • 电销机器人作为一种人工智能技术载体,可以帮助企业提升电销效率并节省人工成本。然而,电销机器人市场缺乏统一的市场准入标准,产品品质良莠不齐。创业者在代理或购买电销机器人时应注意谨防用录音冒充真人语音通话以及宣传技术与实际效果不符的情况。选择电销机器人时需要考察公司资质和产品品质,尤其要关注语音识别率。 ... [详细]
  • 如何去除Win7快捷方式的箭头
    本文介绍了如何去除Win7快捷方式的箭头的方法,通过生成一个透明的ico图标并将其命名为Empty.ico,将图标复制到windows目录下,并导入注册表,即可去除箭头。这样做可以改善默认快捷方式的外观,提升桌面整洁度。 ... [详细]
  • 本文介绍了数据库的存储结构及其重要性,强调了关系数据库范例中将逻辑存储与物理存储分开的必要性。通过逻辑结构和物理结构的分离,可以实现对物理存储的重新组织和数据库的迁移,而应用程序不会察觉到任何更改。文章还展示了Oracle数据库的逻辑结构和物理结构,并介绍了表空间的概念和作用。 ... [详细]
  • Windows下配置PHP5.6的方法及注意事项
    本文介绍了在Windows系统下配置PHP5.6的步骤及注意事项,包括下载PHP5.6、解压并配置IIS、添加模块映射、测试等。同时提供了一些常见问题的解决方法,如下载缺失的msvcr110.dll文件等。通过本文的指导,读者可以轻松地在Windows系统下配置PHP5.6,并解决一些常见的配置问题。 ... [详细]
  • 本文介绍了Svn和Maven的使用说明,包括版本控制和构建工具的功能和优势。同时提供了一个相关链接,链接中详细介绍了SvnMaven的使用方法和注意事项。通过学习和使用SvnMaven,开发人员可以更好地进行代码管理、软件开发和协作开发,提高项目管理的效率和质量。 ... [详细]
author-avatar
hypothesis82235
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有