.NET Core 单元测试从零基础到项目应用

本文讲述了如何在 .NET Core 的项目中从零开始搭建单元测试,然后达到项目应用的程度。通过本文,你可以 get 以下知识:

  • .NET 中现有单元测试框架有哪些
  • 为什么选择 MSTest 框架
  • 如何创建一个单元测试
  • 怎么运行单元测试

框架选型

我们在使用一种技术时,往往需要对现有技术调研,通过比较最终确定使用哪个。.NET 官方推荐的单元测试有 3 种:xUnit、NUnit、MSTest。

除了标注测试类和方法的特性用的不一样之外,它们是非常相似的。而 MSTest 与 VisualStudio 集成度更高,所以本人建议使用 MSTest。

StackOverflow 看到一条我很赞同的看法:

其实不用顾虑那么多,随便选择吧,MSTest 对 VS 的集成是最好的,而且也很容易上手,如果哪一天碰到它所无法解决的事情,切换到其他框架也非常简单,仅仅只是Nuget下个包,换下特性而已。

添加单元测试

在 VS 中,选中方法名,右键 -> 创建单元测试,点击确定。

通过上述步骤,VS 会自动创建一个单元测试项目,在该项目里面自动生成单元测试内容。

1
2
3
4
5
6
7
8
9
10
11
12
// 标记测试类
[TestClass()]
public class MinioAdapterTests
{
// 标记测试方法
[TestMethod()]
public void BucketExistsAsyncTest()
{
// 在此处编写单元测试代码
Assert.Fail();
}
}

编写测试案例

依赖注入怎么测试

ASP.NET Core 支持依赖关系注入 (DI) 软件设计模式,并且默认注入了很多服务,具体可以参考 官方文档, 相信只要使用过依赖注入框架的同学,都会对此有不同深入的理解,在此无需赘言。

然而,在引入 IOC 框架之后,对于之前常规的对于类的依赖(new Class)变成通过构造函数对于接口的依赖(ASP.NET CORE 默认注入方式),这本身更加符合依赖倒置原则,但是对于单元测试来说确会带来另一个问题:

由于层层依赖,导致在某个类的方法进行测试的时候,需要构造一大堆该类依赖的接口的实现,非常麻烦。

这个时候,我们脑子里会下意识想一个问题:为什么常用的 .Net 单元测试框架不支持依赖注入?

于是笔者带着这个问题在查阅了一些关于在单元测试中支持依赖注入的讨论Github Issue,以及其他的相关文档,突然明白一个之前一直忽视但实际却非常重要的问题:

在对于一个方法的单元测试中,我们应该关注的是这个方法内部的逻辑测试,而这个方法内部对于外部的依赖,则不在这个单元测试关注的范围内

换言之,单元测试永远都只关注需要测试的方法内部的逻辑实现,至于外部依赖方法的测试,则应该放在另一个专门针对这个方法的单元测试用例中。

弄清楚这个问题,我们才能更加理解另一个单元测试不可缺少的框架——Mock框架。在我们写的测试中,应该忽略外部依赖具体的实现,而是通过模拟该接口方法来显示的指定返回值,从而降低该返回值对于当前单元测试结果的影响,而 Mock 框架(例如最常用的Moq),刚好可以满足我们对于接口的模拟需求。

相信有同学跟我有同样的疑惑,并且当我尝试在 ASP.NET Core 单元测试中的一切外部依赖通过 Mock 的方式进行编写的时候,遇到了一些问题,下文会将这些问题一一道来,希望对有同样疑惑的同学有所帮助。

Mock 框架选择

在 .NET 中有几种 mock 框架可供选择,比如 NMock、PhinoMocks、FakeItEasy和Moq。尽管Moq相对较新,但是它非常易用。不需要像传统的 Record/Replay。并且使用 Moq 在 VS 中可以得到智能提示。学习成本也不高。

所以选择 Moq 作为 Mock 数据框架。Moq 有一个自动 Mock 库 Moq.AutoMock,建议安装该库。

Moq 基本使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var mock = new Mock<ILoveThisLibrary>();

// WOW! No record/replay weirdness?! :)
// 给 DownloadExists 传递一个参数,并使其返回 true
mock.Setup(library => library.DownloadExists("2.0.0.0"))
.Returns(true);

// Use the Object property on the mock to get a reference to the object
// implementing ILoveThisLibrary, and then exercise it by calling
// methods on it
ILoveThisLibrary lovable = mock.Object;
bool download = lovable.DownloadExists("2.0.0.0");

// Verify that the given method was indeed called with the expected value at most once
mock.Verify(library => library.DownloadExists("2.0.0.0"), Times.AtMostOnce());

上面的方式可以简化成:

1
2
3
4
5
6
7
8
9
10
11
12
ILoveThisLibrary lovable = Mock.Of<ILoveThisLibrary>(l =>
l.DownloadExists("2.0.0.0") == true);

// Exercise the instance returned by Mock.Of by calling methods on it...
bool download = lovable.DownloadExists("2.0.0.0");

// Simply assert the returned state:
Assert.True(download);

// If you really want to go beyond state testing and want to
// verify the mock interaction instead...
Mock.Get(lovable).Verify(library => library.DownloadExists("2.0.0.0"));

简而言之,Mock 数据的使用步骤可总结如下:

  1. 新建一个 Mock 实例 mock
  2. 通过 mock 设置方法的返回值
  3. 通过 mock.Object 获取 Mock 的对象来传递给目标方法使用

Moq.AutoMock 使用

基本使用方法

1
2
3
4
5
6
var mocker = new AutoMocker();
var car = mocker.CreateInstance<Car>();

car.DriveTrain.ShouldNotBeNull();
car.DriveTrain.ShouldImplement<IDriveTrain>();
Mock<IDriveTrain> mock = Mock.Get(car.DriveTrain);

注入现有实例

1
2
3
4
5
6
7
var mocker = new AutoMocker();

mocker.Use<IDriveTrain>(new DriveTrain());
// OR, setup a Mock
mocker.Use<IDriveTrain>(x => x.Shaft.Length == 5);

var car = mocker.CreateInstance<Car>();

结语

CI/CD 流程中应该包含单元测试

例如在编写 Repository 层进行单元测试时,经常有同学会编写依赖于数据库数据的单元测试,这样并不利于随时随地的进行单元测试检查。

如果将该流程放在 CI/CD 中,在代码的发布过程中通过单元测试可以检查代码逻辑的正确性,同时依赖于数据库的单元测试将不会通过(通常情况下,生产环境和开发环境隔离),变相迫使开发小伙伴通过 mock 方式模拟数据库返回结果。

这个原则同样适用于不能依赖三方API编写单元测试。

CI/CD 是一种通过在应用开发阶段引入自动化来频繁向客户交付应用的方法。CI/CD 的核心概念是持续集成、持续交付和持续部署。作为一个面向开发和运营团队的解决方案,CI/CD 主要针对在集成新代码时所引发的问题(亦称:"集成地狱")。

点击查看更多内容

单元测试覆盖率

通常很多开发 Leader 都会要求开发团队编写单元测试,但是很少检查单元测试的质量,即单元测试最重要的指标——单元测试代码覆盖率,如果不注重覆盖率的提升,那么很有可能会导致开发成员为了单元测试而写单元测试,预期就会与实际情况相差甚远。

保证单元测试代码覆盖率,将会大大降低代码变更带来的 Bug 率,从而节省整体开发成本。

新人问题:为何要写单元测试?

对于初次开始编写单元测试的开发人员,脑中经常会对此表示怀疑:我为什么要去验证一堆我自己写的正确的逻辑?

实际这个问题包含了区分一个一般开发人员和优秀开发人员很重要的一个条件:他是否会反向思考当前逻辑的正确性。有了这种思维,看待问题才会从多个角度入手分析,对问题的本质掌握更加全面。

不要怀疑,坚持写单元测试,因为这本身也是对反向思维的一种锻炼,以笔者的经验,只有当编写过一段时间之后,才会真正认识单元测试的魅力,并且开始非常习惯的在写一段逻辑之后,顺手写了对于它的单元测试。

即使笔者也算很早就开始写单元测试了,但直到写这篇文章,仍然不断在加深对单元测试的认识。

参考

  1. C#常用单元测试框架比较:XUnit, NUnit, 和 Visual Studio(MSTest)
  2. Testing in .NET
  3. Asp.Net Core 单元测试正确姿势
  4. C#单元测试:使用Moq框架Mock对象