一、简介
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<=0
TotalCount = 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比较一下结果是否正确就结束了,这样的文章很难应用到项目中,我也是后来接触到单元测试才有幸整理出这么一套东西,如果对大家有所帮助请点赞、关注、评论支持下,谢谢。