一、简介
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;
private const string ExceptionMsg = "UT_Exception_Message";
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);
var exception = await Record.ExceptionAsync(async () => await _sut.AnalyzeAsync());
exception.Should().BeNull();
}
[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);
Exception exception = await Record.ExceptionAsync(async () => await _sut.AnalyzeAsync());
exception.Should().BeNull();
}
[Fact]
public async Task Analyze_ShouldRecordErrorLog_WhenThrowException()
{
LatestProcessTimeRecord latestProcessTimeRecord = new LatestProcessTimeRecord()
{
LatestProcessTime = DateTime.UtcNow
};
_processTimeService.Setup(x => x.GetLatestProcessTimeAsync(It.IsAny<string>()))
.ReturnsAsync(latestProcessTimeRecord);
_fileMetadataService.Setup(x => x.QueryMetadatasAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>()))
.Callback(() =>
{
throw new Exception(ExceptionMsg);
});
await Record.ExceptionAsync(async () => await _sut.AnalyzeAsync());
//LogLevel参数写法1
_logger.Verify(x => x.Log(
It.Is<LogLevel>(logLevel => logLevel == LogLevel.Error),
It.IsAny<EventId>(),
It.IsAny<It.IsAnyType>(),
It.IsAny<Exception>(),
It.IsAny<Func<It.IsAnyType, Exception?, string>>()),
Times.Once);
//LogLevel参数写法2
_logger.Verify(x => x.Log(
LogLevel.Error,
It.IsAny<EventId>(),
It.IsAny<It.IsAnyType>(),
It.IsAny<Exception>(),
It.IsAny<Func<It.IsAnyType, Exception?, string>>()),
Times.Once);
}
[Fact]
public async Task NoImplement_ShouldThrowException_Method01()
{
Func<Task> result = async () => await _sut.NoImplementAsync(_token);
//链式进行异常判断写法1
await result.Should().ThrowAsync<NotImplementedException>();
}
[Fact]
public async Task NoImplement_ShouldThrowException_Method02()
{
Exception exception = await Record.ExceptionAsync(async () => await _sut.NoImplementAsync(_token));
//链式进行异常判断写法2
exception.Should().BeOfType<NotImplementedException>();
}
[Fact]
public async Task NoImplement_ShouldThrowException_Method03()
{
try
{
//try...catch进行异常判断
await _sut.NoImplementAsync(_token);
Assert.True(false);
}
catch (NotImplementedException ex)
{
Assert.True(true);
}
catch
{
Assert.True(false);
}
}
[Theory]
[InlineData(NamingFileType.MaxwellSMLSummary, "EVQ", "maxwellsmlsummary-evq")]
[InlineData(NamingFileType.MaxwellSMLSummary, "evq", "maxwellsmlsummary-evq")]
[InlineData(NamingFileType.MaxwellSMLSummary, "EVT", "maxwellsmlsummary-evt")]
[InlineData(NamingFileType.MaxwellSMLSummary, "evt", "maxwellsmlsummary-evt")]
[InlineData(NamingFileType.MaxwellSMLSummary, "PROD", "maxwellsmlsummary")]
[InlineData(NamingFileType.MaxwellSMLSummary, "prod", "maxwellsmlsummary")]
[InlineData(NamingFileType.MaxwellSMLSummary, "", "maxwellsmlsummary")]
[InlineData(NamingFileType.General, "evQ", "general-evq")]
[InlineData(NamingFileType.General, "EvT", "general-evt")]
[InlineData(NamingFileType.General, "PRod", "general")]
[InlineData(NamingFileType.General, "", "general")]
public void GetContainerName_ShouldReturnCorrectContainer(NamingFileType fileType, string envionment, string expect)
{
string containerName = fileType.GetContainerName(envionment);
containerName.Should().BeEquivalentTo(expect);
}
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;
//声明new ActivityService()所需的参数
private readonly IActivityService _sut;
private readonly Mock<ISauthService> _sauthService;
private readonly Mock<IOptions<ReferencingServices>> _referencingServices;
private readonly Mock<ILogger<ActivityService>> _logger;
private readonly Mock<IHttpClientWrapperService> _httpClientWrapperSvc;
public ActivityServiceUnitTest()
{
_token = Guid.NewGuid().ToString();
_fileMetadataId = "UT_FileMetadataId";
_sauthService = new Mock<ISauthService>();
_referencingServices = new Mock<IOptions<ReferencingServices>>();
_logger = new Mock<ILogger<ActivityService>>();
_httpClientWrapperSvc = new Mock<IHttpClientWrapperService>();
_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();
_sut = new ActivityService(_sauthService.Object, _httpClientWrapperSvc.Object,
_referencingServices.Object, _logger.Object);
}
[Fact]
public async Task ExtractActivity_ShouldSuccess()
{
var excepted = new ActivityCreateUpdateRequest
{
WellInfo = new WellData
{
WellName = _operationalActivity?.Wells[0]?.Wellname,
FieldName = _operationalActivity?.Wells[0]?.Wellfield,
CountryCode = _wellTestingEmission.CountryOfOrigin,
ClientName = _wellTestingEmission.CustomerName
},
Execution = new ExecutionData
{
Id = _wellTestingEmission.JobId,
Name = _wellTestingEmission.JobName,
Time = Convert.ToDateTime(_wellTestingEmission.CreationDate),
System = ExecutionSystem.Tallix,
Status = ExecutionStatus.Completed
},
BusinessContext = new BusinessContextData
{
FDPNumber = _wellTestingEmission.FdpNumber
},
Details = new ActivityDetails
{
MetadataIds = new List<string>() { _fileMetadataId }
}
};
RequestBase<ActivityCreateUpdateRequest> result = _sut.ExtractActivity(_fileMetadataId, _wellTestingEmission, _operationalActivity);
//逐个属性对比属性值是否相同
excepted.Should().BeEquivalentTo(result.Data);
}
[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 }
};
//一个方法中调用多个PostAsync/GetAsync时,根据不同的参数类型来进行设置
_httpClientWrapperSvc.Setup(x => x.PostAsync<RequestBase<ActivityBatchQueryRequest>, ResponseBase<ActivityBatchQueryResponse>>(It.IsAny<string>(), It.IsAny<RequestBase<ActivityBatchQueryRequest>>(), It.IsAny<string>(), It.IsAny<string>()))
.Returns(Task.FromResult(activityBatchQueryResponse));
_httpClientWrapperSvc.Setup(x => x.PostAsync<RequestBase<ActivityCreateUpdateRequest>, ResponseBase<ActivityCreateResponse>>(It.IsAny<string>(), It.IsAny<RequestBase<ActivityCreateUpdateRequest>>(), It.IsAny<string>(), It.IsAny<string>()))
.Returns(Task.FromResult(activityCreateResponse));
//方法没有返回值时,检查执行过程中不能有异常
var exception = await Record.ExceptionAsync(async () => await _sut.InsertOrUpdateActivityAsync(_fileMetadataId, _wellTestingEmission, _operationalActivity, _token));
exception.Should().BeNull();
}
[Fact]
public async Task InsertOrUpdateActivity_ShouldSuccess_WhenInsert_Met401()
{
int calls = 0;
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 }
};
_httpClientWrapperSvc.Setup(x => x.PostAsync<RequestBase<ActivityBatchQueryRequest>, ResponseBase<ActivityBatchQueryResponse>>(It.IsAny<string>(), It.IsAny<RequestBase<ActivityBatchQueryRequest>>(), It.IsAny<string>(), It.IsAny<string>()))
.Returns(Task.FromResult(activityBatchQueryResponse));
//方法执行过程中主动抛出异常,且仅第一次执行时抛异常,之后就正常执行
_httpClientWrapperSvc.Setup(x => x.PostAsync<RequestBase<ActivityCreateUpdateRequest>, ResponseBase<ActivityCreateResponse>>(It.IsAny<string>(), It.IsAny<RequestBase<ActivityCreateUpdateRequest>>(), It.IsAny<string>(), It.IsAny<string>()))
.Returns(Task.FromResult(activityCreateResponse))
.Callback(() =>
{
calls++;
if (calls == 1)
{
throw new HttpRequestException("", new Exception(), HttpStatusCode.Unauthorized);
}
});
var exception = await Record.ExceptionAsync(async () => await _sut.InsertOrUpdateActivityAsync(_fileMetadataId, _wellTestingEmission, _operationalActivity, _token));
exception.Should().BeNull();
}
[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 } }
}
};
_httpClientWrapperSvc.Setup(x => x.PostAsync<RequestBase<ActivityBatchQueryRequest>, ResponseBase<ActivityBatchQueryResponse>>(It.IsAny<string>(), It.IsAny<RequestBase<ActivityBatchQueryRequest>>(), It.IsAny<string>(), It.IsAny<string>()))
.ReturnsAsync(activityBatchQueryResponse);
_httpClientWrapperSvc.Setup(x => x.PutAsync<RequestBase<ActivityCreateUpdateRequest>>(It.IsAny<string>(), It.IsAny<RequestBase<ActivityCreateUpdateRequest>>(), It.IsAny<string>(), It.IsAny<string>()))
.ReturnsAsync(_wellTestingEmission.JobId);
var exception = await Record.ExceptionAsync(async () => await _sut.InsertOrUpdateActivityAsync(_fileMetadataId, _wellTestingEmission, _operationalActivity, _token));
exception.Should().BeNull();
}
[Fact]
public async Task InsertOrUpdateActivity_ShouldSuccess_WhenUpdate_Met401()
{
int calls = 0;
ResponseBase<ActivityBatchQueryResponse> activityBatchQueryResponse = new ResponseBase<ActivityBatchQueryResponse>()
{
Data = new ActivityBatchQueryResponse()
{
//TotalCount>0且Results集合不为空
TotalCount = 1,
Results = new List<ActivityBatchQueryItem>() { new ActivityBatchQueryItem() { Id = _wellTestingEmission.JobId } }
}
};
_httpClientWrapperSvc.Setup(x => x.PostAsync<RequestBase<ActivityBatchQueryRequest>, ResponseBase<ActivityBatchQueryResponse>>(It.IsAny<string>(), It.IsAny<RequestBase<ActivityBatchQueryRequest>>(), It.IsAny<string>(), It.IsAny<string>()))
.ReturnsAsync(activityBatchQueryResponse);
_httpClientWrapperSvc.Setup(x => x.PutAsync<RequestBase<ActivityCreateUpdateRequest>>(It.IsAny<string>(), It.IsAny<RequestBase<ActivityCreateUpdateRequest>>(), It.IsAny<string>(), It.IsAny<string>()))
.ReturnsAsync(_wellTestingEmission.JobId)
.Callback(() =>
{
calls++;
if (calls == 1)
{
throw new HttpRequestException("", new Exception(), HttpStatusCode.Unauthorized);
}
});
var exception = await Record.ExceptionAsync(async () => await _sut.InsertOrUpdateActivityAsync(_fileMetadataId, _wellTestingEmission, _operationalActivity, _token));
exception.Should().BeNull();
}
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);
}
}
}
五、总结
完成上边两个单元测试之后其余Services的处理也都大同小异,这里就不完全展示了。
不同公司对单元测试的定义多多少少会有些不同,这里探讨的是xUnit+Moq数据的方式,在此之前我也看过许多博主关于单元测试的文章,但大多写的比较简单,比如:测试的方法就是个加减乘除,Assert比较一下结果是否正确就结束了,这样的文章很难应用到项目中,我也是后来接触到单元测试才有幸整理出这么一套东西,如果对大家有所帮助请点赞、关注、评论支持下,谢谢。
版权归原作者 changuncle 所有, 如有侵权,请联系我们删除。