XUnit单元测试(实用项目)——看完不会用你打我

news/2024/11/27 3:37:11/

一、简介

xUnit.net 是针对 .NET 的免费、开源单元测试框架,可并行测试、数据驱动测试。测试项目需要同时引用 xUnit和被测试项目,从而对其进行测试。测试编写完成后,用 Test Runner 来测试项目,Test Runner 可以读取测试代码,并且知道所会使用的测试框架,然后执行,并显示结果。

二、支持平台

xUnit.net 目前支持 .Net Framework、.Net Core、.Net Standard、UWP、Xamarin ,可以在这些平台使用 xUnit 进行测试。

三、核心思想

单元测试的核心思想:万物皆虚拟(mock data)、测试某个类时要假定其他类都正常、单元测试代码和被测试代码的目录结构最好一致。
配置项是虚拟的,配置项各属性的值要重新设置。
类实例是虚拟的,类实例中的不同方法返回什么结果要提前设置。
Http请求是虚拟的,不同http请求返回什么结果要提前设置。

四、XUnit具体用法详解

欲写单元测试首先得有要测试的功能/服务,写服务的过程就不在这里赘述了,写单元测试的时候我会把对应的服务关键代码截图过来让大家对照着看,下面正式开始:

1、准备环境

1.1新建项目

1.2安装依赖项及项目结构概览(FluentAssertions、Moq)

2、对AnalyzerService.cs进行单元测试

先来个简单点的对AnalyzerService.cs进行单元测试,具体可参照下述步骤:

  • 创建对应的测试类AnalyzerServiceUnitTest.cs
  • 声明测试对象IAnalyzerService _sut(System Under Test待测系统的缩写)
  • 在构造方法中创建AnalyzerService实例并赋值给测试对象,创建实例所需的参数都要用mock数据,缺哪些就声明哪些
  • 声明并用new对mock数据初始化后还要对其中的属性/方法进行设置才能使用(呼应第三点核心思想,这也是单元测试最重要的一步)
  • 测试对象创建完毕后,要根据IAnalyzerService创建测试方法(单元测试要包含其中的所有方法,甚至更多)
  • 单一方法业务较复杂时可以根据if条件将其拆分成多个测试方法,所以测试方法可能多于接口原有的方法
  • 若原方法有返回值可以根据返回值是否符合预期来进行断言,若原方法没返回值只要不报错即可,若需要判断异常我这也有处理方法
  • 下面直接上代码,我会尽可能的添加注释帮助大家理解

namespace LearnUnitTest.Test.Services
{public class AnalyzerServiceUnitTest{private readonly string _token;private readonly string _fileMetadataId;//声明测试对象private readonly IAnalyzerService _sut;//声明new AnalyzerService()所需的参数private readonly Mock<ISauthService> _sauthService;private readonly Mock<IFileMetadataService> _fileMetadataService;private readonly Mock<IProcessTimeService> _processTimeService;private readonly Mock<IErrorRecordService> _errorRecordService;private readonly Mock<IOptions<TimeSettings>> _timeSettings;private readonly Mock<IOptions<ReferencingServices>> _referencingServices;private readonly Mock<IOptions<ErrorRecordSetting>> _errorRecordSetting;private readonly Mock<IActivityService> _activityService;private readonly Mock<ILogger<AnalyzerService>> _logger;public AnalyzerServiceUnitTest(){_token = Guid.NewGuid().ToString();_fileMetadataId = "UT_FileMetadataId";_sauthService = new Mock<ISauthService>();_fileMetadataService = new Mock<IFileMetadataService>();_processTimeService = new Mock<IProcessTimeService>();_errorRecordService = new Mock<IErrorRecordService>();_timeSettings = new Mock<IOptions<TimeSettings>>();_referencingServices = new Mock<IOptions<ReferencingServices>>();_errorRecordSetting = new Mock<IOptions<ErrorRecordSetting>>();_activityService = new Mock<IActivityService>();_logger = new Mock<ILogger<AnalyzerService>>();//mock数据创建实例后还要根据需求对其中的属性/方法进行设置才能使用InitOptions();InitServices();//创建AnalyzerService实例并赋值给测试对象_sut = new AnalyzerService(_sauthService.Object, _fileMetadataService.Object,_processTimeService.Object, _errorRecordService.Object,_timeSettings.Object, _referencingServices.Object, _errorRecordSetting.Object,_activityService.Object, _logger.Object);}[Fact]public async Task Analyze_ShouldSuccess_WhenHasLatestProcessTime(){LatestProcessTimeRecord latestProcessTimeRecord = new LatestProcessTimeRecord(){//设置返回值非空LatestProcessTime = DateTime.UtcNow};//AnalyzerService中GetLatestProcessTimeAsync()返回空和非空会进行不同的逻辑处理,所以将其拆分成两个方法//通过重写GetLatestProcessTimeAsync()的返回值对不同的情况进行逻辑覆盖_processTimeService.Setup(x=>x.GetLatestProcessTimeAsync(It.IsAny<string>())).ReturnsAsync(latestProcessTimeRecord);await _sut.AnalyzeAsync();}[Fact]public async Task Analyze_ShouldSuccess_WhenNoLatestProcessTime(){LatestProcessTimeRecord latestProcessTimeRecord = new LatestProcessTimeRecord(){//设置返回值为空LatestProcessTime = null};//AnalyzerService中GetLatestProcessTimeAsync()返回空和非空会进行不同的逻辑处理,所以将其拆分成两个方法//通过重写GetLatestProcessTimeAsync()的返回值对不同的情况进行逻辑覆盖_processTimeService.Setup(x => x.GetLatestProcessTimeAsync(It.IsAny<string>())).ReturnsAsync(latestProcessTimeRecord);await _sut.AnalyzeAsync();}[Fact]public async Task NoImplement_ShouldThrowException_Method01(){Func<Task> result = async () => await _sut.NoImplementAsync(_token);//链式进行异常判断await result.Should().ThrowAsync<NotImplementedException>();}[Fact]public async Task NoImplement_ShouldThrowException_Method02(){try{//try...catch进行异常判断await _sut.NoImplementAsync(_token);Assert.True(false);}catch (NotImplementedException ex){Assert.True(true);}catch{Assert.True(false);}}private void InitOptions(){TimeSettings timeSettings = new TimeSettings(){DefaultUploadTime = "2022-06-01T00:00:00.000Z"};//IOptions<T>类型的变量通过Value属性来获取实际的参数值,所以这里要重写Value属性_timeSettings.Setup(x => x.Value).Returns(timeSettings);ReferencingServices referencingServices = new ReferencingServices(){DataPartitionId = "DataPartition-Id",FileServiceURL = "https://global.FileService.URL",ActivityServiceURL = "https://global.ActivityService.URL",ProcessStatusServiceURL = "https://global.ProcessStatusService.URL"};_referencingServices.Setup(x => x.Value).Returns(referencingServices);ErrorRecordSetting errorRecordSetting = new ErrorRecordSetting(){DefaultMaxRetryCount = 5};_errorRecordSetting.Setup(x => x.Value).Returns(errorRecordSetting);}private void InitServices(){//AnalyzerService中调用了ISauthService.GetToken(),不重写GetToken()则所有用到此方法的地方返回值都是null(返回值是string类型)_sauthService.Setup(x => x.GetToken()).ReturnsAsync(_token);List<FileMetadataGetResponse> fileMetadatas = new List<FileMetadataGetResponse>(){new FileMetadataGetResponse() { Id = _fileMetadataId }};//当需要重写的方法有参数时,根据参数类型用It.IsAny<T>()代替即可_fileMetadataService.Setup(x => x.QueryMetadatasAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>())).ReturnsAsync(fileMetadatas);WellTestingEmission wellTestingEmission = new WellTestingEmission(){Id = _fileMetadataId,CreatedTime = DateTime.UtcNow,Emissions = new List<EmissionInfo>(){new EmissionInfo(){Name = "CO2EstimatedEmissionForGas",Unit = "T",Value = 0.0f}}};_fileMetadataService.Setup(x => x.GetFileContentByMetadataIdAsync(It.IsAny<string>(), It.IsAny<string>())).ReturnsAsync(wellTestingEmission);_errorRecordService.Setup(x => x.InsertProcessErrorAsync(It.IsAny<ErrorBaseRecord>(), It.IsAny<string>(), It.IsAny<string>()));_errorRecordService.Setup(x => x.UpdateProcessErrorAsync(It.IsAny<ErrorBaseRecord>(), It.IsAny<string>(), It.IsAny<string>()));_activityService.Setup(x => x.InsertOrUpdateActivityAsync(It.IsAny<string>(), It.IsAny<WellTestingEmission>(), It.IsAny<OperationalActivity>(), It.IsAny<string>()));}}
}

3、对ActivityService.cs进行单元测试

本节在AnalyzerServiceUnitTest.cs单元测试的基础上添加了对http请求的处理(直接用System.Net.Http.HttpClient发送Post/Get请求),单元测试不能发送真实的http请求,所以我们要对不同的http请求分别设置对应的返回值(mock数据),具体可参照下述步骤:

  • 创建对应的测试类ActivityServiceUnitTest.cs
  • 声明测试对象IActivityService _sut(System Under Test待测系统的缩写)
  • 在构造方法中创建ActivityService实例并赋值给测试对象,创建实例所需的参数都要用mock数据,缺哪些就声明哪些
  • 声明并用new对mock数据初始化后还要对其中的属性/方法进行设置才能使用(呼应第三点核心思想,这也是单元测试最重要的一步)
  • 测试对象创建完毕后,要根据IActivityService创建测试方法(单元测试要包含其中的所有方法,甚至更多)
  • 单一方法业务较复杂时可以根据if条件将其拆分成多个测试方法,所以测试方法可能多于接口原有的方法
  • 若原方法有返回值可以根据返回值是否符合预期来进行断言,若原方法没返回值只要不报错即可,若需要判断异常我这也有处理方法(参考上一节
  • 下面直接上代码,我会尽可能的添加注释帮助大家理解

namespace LearnUnitTest.Test.Services
{public class ActivityServiceUnitTest{private readonly string _token;private readonly string _fileMetadataId;//多次用到的对象要定义成全局变量private readonly WellTestingEmission _wellTestingEmission;private readonly OperationalActivity _operationalActivity;private readonly JsonSerializerOptions _jsonSerializerOptions;//声明new ActivityService()所需的参数private readonly IActivityService _sut;private readonly Mock<ISauthService> _sauthService;private readonly Mock<IOptions<ReferencingServices>> _referencingServices;private readonly Mock<ILogger<ActivityService>> _logger;//重写http请求所需的变量private Mock<HttpMessageHandler> _httpMessageHandler;private Mock<IHttpClientFactory> _httpClientFactory;private IHttpClientWrapperService _httpClientWrapperSvc;private Mock<ILogger<HttpClientWrapperService>> _httpClientLogger;public ActivityServiceUnitTest(){_token = Guid.NewGuid().ToString();_fileMetadataId = "UT_FileMetadataId";_jsonSerializerOptions = new JsonSerializerOptions(){PropertyNamingPolicy = JsonNamingPolicy.CamelCase};_sauthService = new Mock<ISauthService>();_referencingServices = new Mock<IOptions<ReferencingServices>>();_logger = new Mock<ILogger<ActivityService>>();_wellTestingEmission = new WellTestingEmission(){JobId = "UT_JobId",JobName = "UT_JobName",CountryOfOrigin = "UT_CountryOfOrigin",CustomerName = "UT_CustomerName",FdpNumber = "UT_FdpNumber"};_operationalActivity = new OperationalActivity(){Wells = new[] { new Well() { Wellname = "UT_Wellname01", Wellfield = "UT_Wellfield01" } }};//mock数据创建实例后还要根据需求对其中的属性/方法进行设置才能使用InitOptions();InitServices();InitHttpClient();_sut = new ActivityService(_sauthService.Object, _httpClientWrapperSvc,_referencingServices.Object, _logger.Object);}[Fact]public async Task ExtractActivity_ShouldSuccess(){var result = _sut.ExtractActivity(_fileMetadataId, _wellTestingEmission, _operationalActivity);//判断响应结果是否正确时要逐层判断,避免空指针异常result.Data.Should().NotBeNull();result.Data.Execution.Should().NotBeNull();result.Data.Execution.Id.Should().NotBeNull();result.Data.Execution.Id.Should().Be(_wellTestingEmission.JobId);result.Data.Details.Should().NotBeNull();result.Data.Details.MetadataIds.Should().HaveCountGreaterThan(0);result.Data.Details.MetadataIds[0].Should().Be(_fileMetadataId);}[Fact]public async Task InsertOrUpdateActivity_ShouldSuccess_WhenInsert(){ResponseBase<ActivityBatchQueryResponse> activityBatchQueryResponse = new ResponseBase<ActivityBatchQueryResponse>() {Data = new ActivityBatchQueryResponse(){//TotalCount<=0TotalCount = 0}};ResponseBase<ActivityCreateResponse> activityCreateResponse = new ResponseBase<ActivityCreateResponse>(){Data = new ActivityCreateResponse() { Id= _wellTestingEmission.JobId }};//AnalyzerService中调用http://xxx/activities/query返回值response.Data.TotalCount是否大于0会进行不同的逻辑处理,所以将其拆分成两个方法//通过重写http请求的返回值对不同的情况进行逻辑覆盖SetHttpMessageHandlerSendAsyncMock(activityBatchQueryResponse, activityCreateResponse);await _sut.InsertOrUpdateActivityAsync(_fileMetadataId, _wellTestingEmission, _operationalActivity, _token);}[Fact]public async Task InsertOrUpdateActivity_ShouldSuccess_WhenUpdate(){ResponseBase<ActivityBatchQueryResponse> activityBatchQueryResponse = new ResponseBase<ActivityBatchQueryResponse>(){Data = new ActivityBatchQueryResponse(){//TotalCount>0且Results集合不为空TotalCount = 1,Results = new List<ActivityBatchQueryItem>() { new ActivityBatchQueryItem() { Id = _wellTestingEmission.JobId } }}};SetHttpMessageHandlerSendAsyncMock(activityBatchQueryResponse, null);await _sut.InsertOrUpdateActivityAsync(_fileMetadataId, _wellTestingEmission, _operationalActivity, _token);}private void InitOptions(){ReferencingServices referencingServices = new ReferencingServices() {DataPartitionId = "DataPartition-Id",FileServiceURL = "https://global.FileService.URL",ActivityServiceURL = "https://global.ActivityService.URL",ProcessStatusServiceURL = "https://global.ProcessStatusService.URL"};//IOptions<T>类型的变量通过Value属性来获取实际的参数值,所以这里要重写Value属性_referencingServices.Setup(x => x.Value).Returns(referencingServices);}private void InitServices(){//AnalyzerService中调用了ISauthService.GetToken(),不重写GetToken()则所有用到此方法的地方返回值都是null(返回值是string类型)_sauthService.Setup(x => x.GetToken()).ReturnsAsync(_token);}/// <summary>/// 这里只重写了IHttpClientFactory.CreateClient(string name),具体根据不同的请求返回不同的响应结果要在测试方法中设置/// </summary>private void InitHttpClient(){_httpClientFactory = new Mock<IHttpClientFactory>();_httpMessageHandler = new Mock<HttpMessageHandler>();_httpClientLogger = new Mock<ILogger<HttpClientWrapperService>>();_httpClientWrapperSvc = new HttpClientWrapperService(_httpClientFactory.Object, _httpClientLogger.Object);//IHttpClientFactory创建HttpClient有两个重载:CreateClient()和CreateClient(string name)//但CreateClient()最终也是调用了CreateClient(string name),所以应该重写CreateClient(string name)_httpClientFactory.Setup(x => x.CreateClient(It.IsAny<string>())).Returns(() => new HttpClient(_httpMessageHandler.Object));}private void SetHttpMessageHandlerSendAsyncMock(ResponseBase<ActivityBatchQueryResponse> activityBatchQueryResponse, ResponseBase<ActivityCreateResponse> activityCreateResponse){HttpResponseMessage httpResponseMsg = new(){StatusCode = HttpStatusCode.OK};_httpMessageHandler.Protected().Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>()).Returns(async (HttpRequestMessage request, CancellationToken token) =>{//根据request.RequestUri中的关键字来判断接口的作用,进而设置对应的响应结果if (request.RequestUri.AbsolutePath.Contains("activities/query", StringComparison.OrdinalIgnoreCase)){httpResponseMsg.Content = new StringContent(JsonSerializer.Serialize(activityBatchQueryResponse, _jsonSerializerOptions), Encoding.UTF8, "application/json");}else if ((request.RequestUri.AbsolutePath.Contains("activities/" + _wellTestingEmission.JobId, StringComparison.OrdinalIgnoreCase))){httpResponseMsg.Content = new StringContent(_wellTestingEmission.JobId, Encoding.UTF8, "application/json");}else{httpResponseMsg.Content = new StringContent(JsonSerializer.Serialize(activityCreateResponse, _jsonSerializerOptions), Encoding.UTF8, "application/json");}return httpResponseMsg;});}}
}

五、总结

完成上边两个单元测试之后其余Services的处理也都大同小异,这里就不完全展示了。

不同公司对单元测试的定义多多少少会有些不同,这里探讨的是xUnit+Moq数据的方式,在此之前我也看过许多博主关于单元测试的文章,但大多写的比较简单,比如:测试的方法就是个加减乘除,Assert比较一下结果是否正确就结束了,这样的文章很难应用到项目中,我也是后来接触到单元测试才有幸整理出这么一套东西,如果对大家有所帮助请点赞、关注、评论支持下,谢谢。


http://www.ppmy.cn/news/1192356.html

相关文章

redis1之安装redis,启动,常用数据结构

目录 redis安装与启动、常见数据结构 启动 Redis客户端 数据结构与常见的命令 redis的通用命令 String类型的用法 Hash命令的用法 List命令 Set命令 SortedSet类型用法 redis安装与启动、常见数据结构 1&#xff0c;在linux上安装上gcc的依赖&#xff0c;我这里是centos7.6…

【大数据】-- flink kubernetes operator 入门与实践

课程链接:https://edu.csdn.net/course/detail/38831 目录 课程链接:https://edu.csdn.net/course/detail/38831https://edu.csdn.net/course/detail/38831 一、你将收获

【Midjourney入门教程3】写好prompt常用的参数

文章目录 1、图片描述词&#xff08;图片链接&#xff09;文字描述词后缀参数2、权重划分3、后缀参数版本选择&#xff1a;--v版本风格&#xff1a;--style长宽比&#xff1a;--ar多样性: --c二次元化&#xff1a;--niji排除内容&#xff1a;--no--stylize--seed--tile、--q 4、…

测试老鸟,Python接口自动化测试框架搭建-全过程,看这篇就够了...

目录&#xff1a;导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09; 前言 1、接口测试自动化…

pytorch笔记 GRUCELL

1 介绍 GRU的一个单元 2 基本使用方法 torch.nn.GRUCell(input_size, hidden_size, biasTrue, deviceNone, dtypeNone) 输入&#xff1a;&#xff08;batch&#xff0c;input_size&#xff09; 输出和隐藏层&#xff1a;&#xff08;batch&#xff0c;hidden_size&#xf…

干了3年“点点点”,我废了...

简单概括一下 先说一下自己的情况&#xff0c;普通本科&#xff0c;18年通过校招进入深圳某软件公司&#xff0c;干了3年多的功能测试&#xff0c;21年的那会&#xff0c;因为大环境不好&#xff0c;我整个人心惊胆战的&#xff0c;怕自己卷铺盖走人了&#xff0c;我感觉自己不…

excel利用正则匹配和替换指定内容

上班中, 突然接到电话, 屋里的上司大人发来个excel, 说要替换里面x-x-xxx列的内容为x栋x单元xxx. 大致表格如下, 原表格我就不发了 身为程序猿的我, 肯定第一就想到了 正则! 打开excel-开始-查找和替换, 我擦, 只能完全匹配和替换 比如一次只能替换1-1- -> 为1栋1单元 1-2…

【代码数据】2023粤港澳大湾区金融数学建模B题分享

基于中国特色估值体系的股票模型分析和投资策略 首先非常建议大家仔细的阅读这个题的题目介绍&#xff0c;还有附赠的就是那个附件里的那几篇材料&#xff0c;我觉得你把这些内容读透理解了&#xff0c;就可以完成大部分内容。然后对于题目里它主要第一部分给出了常用的估值模…