浅谈.Net Core后端单元测试的实现
1.前言
单元测试一直都是"好处大家都知道很多,但是因为种种原因没有实施起来"的一个老大难问题。具体是否应该落地单元测试,以及落地的程度,每个项目都有自己的情况。
本篇为个人认为"如何更好地写单元测试",即更加偏向实践向中夹杂一些理论的分享。
下列示例的单元测试框架为xUnit,Mock库为Moq
2.为什么需要单元测试
优点有很多,这里提两点我个人认为的很明显的好处
2.1防止回归
通常在进行新功能/模块的开发或者是重构的时候,测试会进行回归测试原有的已存在的功能,以验证以前实现的功能是否仍能按预期运行。
使用单元测试,可在每次生成后,甚至在更改一行代码后重新运行整套测试,从而可以很大程度减少回归缺陷。
2.2减少代码耦合
当代码紧密耦合或者一个方法过长的时候,编写单元测试会变得很困难。当不去做单元测试的时候,可能代码的耦合不会给人感觉那么明显。为代码编写测试会自然地解耦代码,变相提高代码质量和可维护性。
3.基本原则和规范
3.13A原则
3A分别是"arrange、act、assert",分别代表一个合格的单元测试方法的三个阶段
- 事先的准备
- 测试方法的实际调用
- 针对返回值的断言
一个单元测试方法可读性是编写测试时最重要的方面之一。在测试中分离这些操作会明确地突出显示调用代码所需的依赖项、调用代码的方式以及尝试断言的内容.
所以在进行单元测试的编写的时候,请使用注释标记出3A的各个阶段的,如下示例
[Fact]
publicasyncTaskVisitDataCompressExport_ShouldReturnEmptyResult_WhenFileTokenDoesNotExist()
{
//arrange
varmockFiletokenStore=newMock();
mockFiletokenStore
.Setup(it=>it.Get(It.IsAny()))
.Returns(string.Empty);
varcontroller=newStatController(
mockFiletokenStore.Object,
null);
//act
varactual=awaitcontroller.VisitDataCompressExport("faketoken");
//assert
Assert.IsType(actual);
}
3.2尽量避免直接测试私有方法
尽管私有方法可以通过反射进行直接测试,但是在大多数情况下,不需要直接测试私有的private方法,而是通过测试公共public方法来验证私有的private方法。
可以这样认为:private方法永远不会孤立存在。更应该关心的是调用private方法的public方法的最终结果。
3.3重构原则
如果一个类/方法,有很多的外部依赖,造成单元测试的编写困难。那么应该考虑当前的设计和依赖项是否合理。是否有部分可以存在解耦的可能性。选择性重构原有的方法,而不是硬着头皮写下去.
3.4避免多个断言
如果一个测试方法存在多个断言,可能会出现某一个或几个断言失败导致整个方法失败。这样不能从根本上知道是了解测试失败的原因。
所以一般有两种解决方案
- 拆分成多个测试方法
- 使用参数化测试,如下示例
[Theory]
[InlineData(null)]
[InlineData("a")]
publicvoidAdd_InputNullOrAlphabetic_ThrowsArgumentException(stringinput)
{
//arrange
varstringCalculator=newStringCalculator();
//act
Actionactual=()=>stringCalculator.Add(input);
//assert
Assert.Throws(actual);
}
当然如果是对对象进行断言,可能会对对象的多个属性都有断言。此为例外。
3.5文件和方法命名规范文件名规范
一般有两种。比如针对UserController下方法的单元测试应该统一放在UserControllerTest或者UserController_Test下
单元测试方法名
单元测试的方法名应该具有可读性,让整个测试方法在不需要注释说明的情况下可以被读懂。格式应该类似遵守如下
<被测试方法全名>_<期望的结果>_<给予的条件>
//例子
[Fact]
publicvoidAdd_InputNullOrAlphabetic_ThrowsArgumentException()
{
...
}
4.常用类库介绍
4.1xUnit/MsTest/NUnit
编写.NetCore的单元测试绕不过要选择一个单元测试的框架,三大单元测试框架中
- MsTest是微软官方出品的一个测试框架
- NUnit没用过
- xUnit是.NetFoundation下的一个开源项目,并且被dotnetgithub上很多仓库(包括runtime)使用的单元测试框架
三大测试框架发展至今已是大差不差,很多时候选择只是靠个人的喜好。
个人偏好xUnit简洁的断言
//xUnit Assert.True() Assert.Equal() //MsTest Assert.IsTrue() Assert.AreEqual()
客观地功能性地分析三大框架地差异可以参考如下
https://anarsolutions.com/automated-unit-testing-tools-comparison
4.2Moq
官方仓库
https://github.com/moq/moq4
Moq是一个非常流行的模拟库,只要有一个接口它就可以动态生成一个对象,底层使用的是Castle的动态代理功能.
基本用法
在实际使用中可能会有如下场景
publicclassUserController
{
privatereadonlyIUserService_userService;
publicUserController(IUserServiceuserService)
{
_userService=userService;
}
[HttpGet("{id}")]
publicIActionResultGetUser(intid)
{
varuser=_userService.GetUser(id);
if(user==null)
{
returnNotFound();
}
else
{
...
}
}
}
在进行单元测试的时候,可以使用Moq对_userService.GetUser进行模拟返回值
[Fact]
publicvoidGetUser_ShouldReturnNotFound_WhenCannotFoundUser()
{
//arrange
//新建一个IUserService的mock对象
varmockUserService=newMock();
//使用moq对IUserService的GetUs方法进行mock:当入参为233时返回null
mockUserService
.Setup(it=>it.GetUser(233))
.Return((User)null);
varcontroller=newUserController(mockUserService.Object);
//act
varactual=controller.GetUser(233)asNotFoundResult;
//assert
//验证调用过userService的GetUser方法一次,且入参为233
mockUserService.Verify(it=>it.GetUser(233),Times.AtMostOnce());
}
4.3AutoFixture
官方仓库
https://github.com/AutoFixture/AutoFixture
AutoFixture是一个假数据填充库,旨在最小化3A中的arrange阶段,使开发人员更容易创建包含测试数据的对象,从而可以更专注与测试用例的设计本身。
基本用法
直接使用如下的方式创建强类型的假数据
[Fact]
publicvoidIntroductoryTest()
{
//arrange
Fixturefixture=newFixture();
intexpectedNumber=fixture.Create();
MyClasssut=fixture.Create();
//act
intresult=sut.Echo(expectedNumber);
//assert
Assert.Equal(expectedNumber,result);
}
上述示例也可以和测试框架本身结合,比如xUnit
[Theory,AutoData]
publicvoidIntroductoryTest(
intexpectedNumber,MyClasssut)
{
//act
intresult=sut.Echo(expectedNumber);
//assert
Assert.Equal(expectedNumber,result);
}
5.实践中结合VisualStudio的使用
VisualStudio提供了完备的单元测试的支持,包括运行.编写.调试单元测试。以及查看单元测试覆盖率等。
5.1如何在VisualStudio中运行单元测试
5.2如何在VisualStudio中查看单元测试覆盖率
如下功能需要VisualStudio2019Enterprise版本,社区版不带这个功能。
如何查看覆盖率
- 在测试窗口下,右键相应的测试组点
- 点击如下的"分析代码覆盖率"
6.实践中常见场景的Mock
主要
6.1DbSet
使用EFCore过程中,如何mockDbSet是一个绕不过的坎。
方法一
参考如下链接的回答进行自行封装
https://stackoverflow.com/questions/31349351/how-to-add-an-item-to-a-mock-dbset-using-moq
方法二(推荐)
使用现成的库(也是基于上面的方式封装好的)
仓库地址:
https://github.com/romantitov/MockQueryable
使用范例
//1.测试时创建一个模拟的Listvarusers=newList () { newUserEntity{LastName="ExistLastName",DateOfBirth=DateTime.Parse("01/20/2012")}, ... }; //2.通过扩展方法转换成DbSet varmockUsers=users.AsQueryable().BuildMock(); //3.赋值给给mock的DbContext中的Users属性 varmockDbContext=newMock (); mockDbContext .Setup(it=>it.Users) .Return(mockUsers);
6.2HttpClient
使用RestEase/Refit的场景
如果使用的是RestEase或者Refit等第三方库,具体接口的定义本质上就是一个interface,所以直接使用moq进行方法mock即可。
并且建议使用这种方式。
IHttpClientFactory
如果使用的是.NetCore自带的IHttpClientFactory方式来请求外部接口的话,可以参考如下的方式对IHttpClientFactory进行mock
https://www.thecodebuzz.com/unit-test-mock-httpclientfactory-moq-net-core/
6.3ILogger
由于ILogger的LogError等方法都是属于扩展方法,所以不需要特别的进行方法级别的mock。
针对平时的一些使用场景封装了一个帮助类,可以使用如下的帮助类进行Mock和Verify
publicstaticclassLoggerHelper
{
publicstaticMock>LoggerMock()whereT:class
{
returnnewMock>();
}
publicstaticvoidVerifyLog(thisMock>loggerMock,LogLevellevel,stringcontainMessage,Timestimes)
{
loggerMock.Verify(
x=>x.Log(
level,
It.IsAny(),
It.Is((o,t)=>o.ToString().Contains(containMessage)),
It.IsAny(),
(Func)It.IsAny
使用方法
[Fact]
publicvoidEcho_ShouldLogInformation()
{
//arrange
varmockLogger=LoggerHelpe.LoggerMock();
varcontroller=newUserController(mockLogger.Object);
//act
controller.Echo();
//assert
mockLogger.VerifyLog(LogLevel.Information,"hello",Times.Once());
}
7.拓展
7.1TDD介绍
TDD是测试驱动开发(Test-DrivenDevelopment)的英文简称.一般是先提前设计好单元测试的各种场景再进行真实业务代码的编写,编织安全网以便将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
到此这篇关于浅谈.NetCore后端单元测试的实现的文章就介绍到这了,更多相关.NetCore单元测试内容请搜索毛票票以前的文章或继续浏览下面的相关文章希望大家以后多多支持毛票票!
声明:本文内容来源于网络,版权归原作者所有,内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎发送邮件至:czq8825#qq.com(发邮件时,请将#更换为@)进行举报,并提供相关证据,一经查实,本站将立刻删除涉嫌侵权内容。