赞
踩
xUnit.net 是针对 .NET 的免费、开源单元测试框架,可并行测试、数据驱动测试。测试项目需要同时引用 xUnit和被测试项目,从而对其进行测试。测试编写完成后,用 Test Runner 来测试项目,Test Runner 可以读取测试代码,并且知道所会使用的测试框架,然后执行,并显示结果。
xUnit.net 目前支持 .Net Framework、.Net Core、.Net Standard、UWP、Xamarin ,可以在这些平台使用 xUnit 进行测试。
单元测试的核心思想:万物皆虚拟(mock data)、测试某个类时要假定其他类都正常、单元测试代码和被测试代码的目录结构最好一致。
配置项是虚拟的,配置项各属性的值要重新设置。
类实例是虚拟的,类实例中的不同方法返回什么结果要提前设置。
Http请求是虚拟的,不同http请求返回什么结果要提前设置。
欲写单元测试首先得有要测试的功能/服务,写服务的过程就不在这里赘述了,写单元测试的时候我会把对应的服务关键代码截图过来让大家对照着看,下面正式开始:
先来个简单点的对AnalyzerService.cs进行单元测试,具体可参照下述步骤:
- 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>()));
- }
- }
- }
本节在AnalyzerServiceUnitTest.cs单元测试的基础上添加了对http请求的处理(直接用System.Net.Http.HttpClient发送Post/Get请求),单元测试不能发送真实的http请求,所以我们要对不同的http请求分别设置对应的返回值(mock数据),具体可参照下述步骤:
- 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比较一下结果是否正确就结束了,这样的文章很难应用到项目中,我也是后来接触到单元测试才有幸整理出这么一套东西,如果对大家有所帮助请点赞、关注、评论支持下,谢谢。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。