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

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<IFileTokenStore>();
  mockFiletokenStore
    .Setup(it => it.Get(It.IsAny<string>()))
    .Returns(string.Empty);

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

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

  // assert
  Assert.IsType<EmptyResult>(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<ArgumentException>(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<IUserService>();
  // 使用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<int>();
  MyClass sut = fixture.Create<MyClass>();

  // 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<T>
var users = new List<UserEntity>()
{
 new UserEntity{LastName = "ExistLastName", DateOfBirth = DateTime.Parse("01/20/2012")},
 ...
};

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

// 3. 赋值给给mock的DbContext中的Users属性
var mockDbContext = new Mock<DbContext>();
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<ILogger<T>> LoggerMock<T>() where T : class
  {
    return new Mock<ILogger<T>>();
  }

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

  public static void VerifyLog<T>(this Mock<ILogger<T>> loggerMock, LogLevel level, Times times)
  {
    loggerMock.Verify(
    x => x.Log(
      level,
      It.IsAny<EventId>(),
      It.IsAny<It.IsAnyType>(),
      It.IsAny<Exception>(),
      (Func<It.IsAnyType, Exception, string>)It.IsAny<object>()),
    times);
  }
}

使用方法

[Fact]
public void Echo_ShouldLogInformation()
{
  // arrange
  var mockLogger = LoggerHelpe.LoggerMock<UserController>();
  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 单元测试内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • ASP.NET Core针对一个使用HttpClient对象的类编写单元测试详解

    介绍 几年前,微软引入了HttpClient类来替代HttpWebRequest来发送Web请求.这个新的类更易于使用,更加简洁,更具有异步性,且易于扩展. HttpClient类有一个可以接受HttpMessageHandler类对象的构造函数.HttpMessageHandler类对象可以接受一个请求(HttpRequestMessage), 并返回响应(HttpResponseMessage).它的功能完全取决于它的实现.默认情况下HttpClient使用的是HttpClientHandl

  • ASP.NET Core对Controller进行单元测试的完整步骤

    前言 单元测试对我们的代码质量非常重要.很多同学都会对业务逻辑或者工具方法写测试用例,但是往往忽略了对Controller层写单元测试.我所在的公司没见过一个对Controller写过测试的.今天来演示下如果对Controller进行单元测试.以下内容默认您对单元测试有所了解,比如如何mock一个接口.在这里多叨叨一句,面向接口的好处,除了能够快速的替换实现类(其实大部分接口不会有多个实现),最大的好处就是可以进行mock,可以进行单元测试. 测试Action 下面的Action非常简单,非常常

  • xUnit 编写 ASP.NET Core 单元测试的方法

    还记得 .NET Framework 的 ASP.NET WebForm 吗?那个年代如果要在 Web 层做单元测试简直就是灾难啊..NET Core 吸取教训,在设计上考虑到了可测试性,就连 ASP.NET Core 这种 Web 或 API 应用要做单元测试也是很方便的.其中面向接口和依赖注入在这方面起到了非常重要的作用. 本文就来手把手教你如何用 xUnit 对 ASP.NET Core 应用做单元测试..NET Core 常用的测试工具还有 NUnit 和 MSTest,我本人习惯用 x

  • ASP.NET Core中使用xUnit进行单元测试

    单元测试的功能自从MVC的第一个版本诞生的时候,就是作为一个重要的卖点来介绍的,通常在拿MVC与webform比较的时候,单元测试就是必杀底牌,把webform碾压得一无是处. 单元测试的重要性不用多说了,有单元测试的做兜底的项目,好比给开发人员买了份保险,当然这个保险的质量取决于单元测试的质量,那些一路Mock的单元测试,看起来很美,但是什么都cover不到.目前工作中的一个老项目,有2万多个单元测试用例,其中不少是用心之作,真正落实到了业务逻辑,开发人员可以放心的去修改代码,当然一切都必须按

  • 浅谈.Net Core后端单元测试的实现

    1. 前言 单元测试一直都是"好处大家都知道很多,但是因为种种原因没有实施起来"的一个老大难问题.具体是否应该落地单元测试,以及落地的程度, 每个项目都有自己的情况. 本篇为个人认为"如何更好地写单元测试", 即更加 偏向实践向 中夹杂一些理论的分享. 下列示例的单元测试框架为 xUnit , Mock库为 Moq 2. 为什么需要单元测试 优点有很多, 这里提两点我个人认为的很明显的好处 2.1 防止回归 通常在进行新功能/模块的开发或者是重构的时候,测试会进行回

  • 浅谈php处理后端&接口访问超时的解决方法

    [HTTP访问] 一般我们访问HTTP方式很多,主要是:curl, socket, file_get_contents() 等方法. 如果碰到对方服务器一直没有响应的时候,我们就悲剧了,很容易把整个服务器搞死,所以在访问http的时候也需要考虑超时的问题. [ CURL 访问HTTP] CURL 是我们常用的一种比较靠谱的访问HTTP协议接口的lib库,性能高,还有一些并发支持的功能等. CURL: curl_setopt($ch, opt) 可以设置一些超时的设置,主要包括: *(重要) CU

  • 浅谈Django前端后端值传递问题

    前端后端传值问题总结 前端传给后端 通过表单传值 1.通过表单get请求传值 在前端当通过get的方式传值时,表单中的标签的name值将会被当做action的地址的参数 此时,在后端可以通过get请求相应的name值拿到对应的value值 例子: html中: <form action="{% url 'backweb:select_art' %}" method="post"> {% csrf_token %} <section class=&q

  • 浅谈Java 中的单元测试

    单元测试编写 Junit 单元测试框架 对于Java语言而言,其单元测试框架,有Junit和TestNG这两种, 下面是一个典型的JUnit测试类的结构 package com.example.demo; import org.junit.jupiter.api.*; import static org.junit.jupiter.api.Assertions.*; @DisplayName("售票器类型测试") class DemoTest { // 定义测试的实例 private

  • 浅谈react前后端同构渲染

    前后端同构渲染:当客户端请求一个包含React组件页面的时候,服务端首先响应输出这个页面,客户端和服务端有了第一次交互.然后,如果加载组件的过程需要向服务端发出Ajax请求等,客户端和服务端又进行了一次交互,这样,耗时相对较长.前后端同构渲染可以在页面初次加载时把所有地方渲染好一次性响应给客户端 实现方式:保证包管理工具和模块依赖方式一致 包管理工具-npm管理,保证前后端都使用同一个兼容包 模块依赖方式-webpack,保证前后端都采用commonjs的依赖方式,确保代码可以互相依赖 服务端如

  • 浅谈React前后端同构防止重复渲染

    什么叫前后端同构? 为了解决某些问题(比如SEO.提升渲染速度等)react 提供了2个方法在服务端生成一个HTML文本格式的字符串.在得到了这个HTML格式的字符串之后,通常会将其组装成一个页面直接返回给用户的浏览器. 到这里,服务端的活已经干完了,然后就是浏览器这边干活. 浏览器拿到HTML文本后,立刻进行渲染将内容呈现给用户.然后加载页面所需的 .js 文件,然后执行 JavaScript 脚本,然后开始初始化 react 组件---- 到这里问题就来了.react 初始化组件后会执行组件

  • 浅谈.net core 注入中的三种模式:Singleton、Scoped 和 Transient

    从上篇内容不如题的文章<.net core 并发下的线程安全问题>扩展认识.net core注入中的三种模式:Singleton.Scoped 和 Transient 我们都知道在 Startup 的ConfigureServices 可以注入我们想要的服务,那么在注入的时候有三种模式可以选择,那么我们在什么时候选择什么样的模式呢? 在讲注入模式之前,我觉得很有必要了解服务生存期的概念! 服务生存期:ASP.NET Core 提供了一个内置的服务容器 IServiceProvider负责管理服

  • 浅谈VueJS SSR 后端绘制内存泄漏的相关解决经验

    引言 Memory Leak 是最难排查调试的 Bug 种类之一,因为内存泄漏是个 undecidable problem,只有开发者才能明确一块内存是不是需要被回收.再加上内存泄漏也没有特定的报错信息,只能通过一定时间段的日志来判断是否存在内存泄漏.大家熟悉的常用调试工具对排查内存泄漏也没有用武之地.当然了,除了专门用于排查内存泄漏的工具(抓取Heap之类的工具)之外. 对于不同的语言,各种排查内存泄漏的方式方法也不尽相同.对于 JavaScript 来说,针对不同的平台,调试工具也是不一样的

  • 浅谈.Net Core 认证系统源码解析

    不知不觉.Net Core已经推出到3.1了,大多数以.Net为技术栈的公司也开始逐步的切换到了Core,从业也快3年多了,一直坚持着.不管环境怎么变,坚持自己的当初的选择,坚持信仰 .Net Core是个非常优秀的框架,如果各位是从WebForm开始,一步步走到今天,自然而然就会发现.微软慢慢的开始将整个框架组件化,不在像以前那样,所以的东西都傻瓜化,比如WebForm,拖拖控件往往能搞定大部分的事情.Core的扩展性很好,将很多选择权交给我们自己,而不是强行的让我们去接受他那一套,对第三方组

  • 浅谈maven单元测试设置代理

    背景 环境需要设置代理才能够访问外部网络,如果只是运行java程序来访问网络,我们可以通过java -jar test.jar -DproxyHost=proxy_ip -DproxyPort=proxy_port,但如果是java的maven项目中,单元测试需要访问网络,只执行mvn test则会导致单元测试的代码无法访问网络. 解决 Maven单元测试,使用的是Surefire Maven插件.当Surefire插件fork JVM时,并不会继承所有的系统属性.因此我们可以通过命令行来如下设

随机推荐