2020/02/01, ASP.NET Core 3.1, VS2019, xunit 2.4.1, Microsoft.AspNetCore.TestHost 3.1.1
摘要:基于ASP.NET Core 3.1 WebApi搭建后端多层网站架构【12-xUnit单元测试之集成测试】
使用xUnit借助TestServer进行集成测试,在单元测试中对WebApi的每个接口进行测试
本章节介绍了使用xUnit借助TestServer进行集成测试,在单元测试中对WebApi的每个接口进行测试
新建单元测试
在tests解决方案文件夹下新建xUnit单元测试,记得存放在实际tests路径下,取名WebApiTests
添加包引用
向WebApiTests
单元测试添加Microsoft.AspNetCore.TestHost
包引用:
<ItemGroup> <PackageReference Include="Microsoft.AspNetCore.TestHost" Version="3.1.1" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.4.0" /> <PackageReference Include="xunit" Version="2.4.1" /> <PackageReference Include="xunit.runner.visualstudio" Version="2.4.1"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> </PackageReference> <PackageReference Include="coverlet.collector" Version="1.2.0"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> </PackageReference> </ItemGroup>
添加Microsoft.AspNetCore.TestHost
包(3.1.1)引用,其他默认的包升级到最新,具体版本参考上面
向WebApiTests
单元测试添加对MS.WebApi
的引用
接口测试
建立TestServerHost
在WebApiTests
单元测试中添加TestHostBuild.cs
类,这是整个集成测试的核心部分:
using Autofac.Extensions.DependencyInjection; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.TestHost; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Options; using MS.Component.Jwt; using MS.Component.Jwt.UserClaim; using MS.WebApi; using System.Net.Http; namespace WebApiTests { public static class TestHostBuild { public static readonly JwtService jwtService = new JwtService(Options.Create(new JwtSetting { Audience = "MS.Audience", Issuer = "MS.WebHost", LifeTime = 1440, SecurityKey = "MS.WebHost SecurityKey"//此处内容需和服务器appsettings.json中保持一致 })); public static readonly UserData userData = new UserData { Account = "test", Email = "test@qq.com", Id = 1, Name = "测试用户", Phone = "123456789111", RoleDisplayName = "testuserRole", RoleName = "testuser" };//测试用户的数据,也可以改成真实的数据,看需求 public static IHostBuilder GetTestHost() { //代码和网站Program中CreateHostBuilder代码很类似,去除了AddNlogService以免跑测试生成很多日志 //如果网站并没有使用autofac替换原生DI容器,UseServiceProviderFactory这句话可以去除 //关键是webBuilder中的UseTestServer,建立TestServer用于集成测试 return new HostBuilder() .UseServiceProviderFactory(new AutofacServiceProviderFactory())//替换autofac作为DI容器 .ConfigureWebHostDefaults(webBuilder => { webBuilder .UseTestServer()//关键时多了这一行建立TestServer .UseStartup<Startup>(); }); } /// <summary> /// 生成带token的httpclient /// </summary> /// <param name="host"></param> /// <returns></returns> public static HttpClient GetTestClientWithToken(this IHost host) { var client = host.GetTestClient(); client.DefaultRequestHeaders.Add("Authorization", $"Bearer {GenerateToken()}");//把token加到Header中 return client; } /// <summary> /// 生成jwt令牌 /// </summary> /// <returns></returns> public static string GenerateToken() { return jwtService.BuildToken(jwtService.BuildClaims(userData)); } } }
- new了一个JwtService,用于生成token,其中JwtSetting的设置要和服务器保持一致(也可以直接从appsettings.json中读取)
- new了一个UserData,用于测试的伪造数据,如果有需求也可以改成数据库中的真实数据
- 其中GetTestHost方法中的代码和网站Program中CreateHostBuilder代码很类似
- 去除了AddNlogService以免跑测试生成很多日志
- 如果网站并没有使用autofac替换原生DI容器,UseServiceProviderFactory这句话可以去除
- 关键是webBuilder中的UseTestServer,建立TestServer用于集成测试
- 获得Host该怎么写,可以看官方的测试用例,以上的代码和之后的测试用例就是我参考官方的写出来的
- GetTestClientWithToken方法就是获得一个带token的httpclient,基于GetTestClient方法而来的
- 如有不需要带token的请求,也可以直接用GetTestClient方法
构建接口返回值对象
在WebApiTests
单元测试中添加ApiResult.cs
类:
namespace WebApiTests { public class ApiResult<T> { public int status { get; set; } public T data { get; set; } } }
由于我们的接口返回值统一包装了一层,所以构建了ApiResult用于反序列化接口返回值对象
编写接口的测试用例
在WebApiTests
单元测试中添加RoleControllerTest.cs
类,这是Role接口的测试用例:
using Microsoft.AspNetCore.TestHost; using Microsoft.Extensions.Hosting; using MS.Common.Extensions; using MS.Entities; using MS.Models.ViewModel; using MS.WebCore.Core; using System.Net.Http; using System.Text; using System.Threading.Tasks; using Xunit; namespace WebApiTests { public class RoleControllerTest { const string _testUrl = "/role/"; const string _mediaType = "application/json"; readonly Encoding _encoding = Encoding.UTF8; [Theory] [InlineData(1222538617050763264)] public async Task Delete_Id_ReturnResult(long id) { //arrange string url = $"{_testUrl}?id={id.ToString()}";// url: /role/?id=11111111 using var host = await TestHostBuild.GetTestHost().StartAsync();//启动TestServer //act var response = await host.GetTestClientWithToken().DeleteAsync(url);//调用Delete接口 var result = (await response.Content.ReadAsStringAsync()).GetDeserializeObject<ApiResult<ExecuteResult>>();//获得返回结果并反序列化 //assert Assert.Equal(result.data.IsSucceed, string.IsNullOrWhiteSpace(result.data.Message)); } [Fact] public async Task Post_CreateRole_ReturnTrue() { //arrange RoleViewModel viewModel = new RoleViewModel { Name = "RoleForPostTest", DisplayName = "RoleForPostTest" }; StringContent content = new StringContent(viewModel.ToJsonString(), _encoding, _mediaType);//定义post传递的参数、编码和类型 using var host = await TestHostBuild.GetTestHost().StartAsync();//启动TestServer //act var response = await host.GetTestClientWithToken().PostAsync(_testUrl, content); //调用Post接口 var result = (await response.Content.ReadAsStringAsync()).GetDeserializeObject<ApiResult<ExecuteResult<Role>>>();//获得返回结果并反序列化 //assert Assert.True(result.data.IsSucceed); //测完把添加的删除 await Delete_Id_ReturnResult(result.data.Result.Id); } [Fact] public async Task Put_UpdateRole_ReturnTrue() { //arrange RoleViewModel viewModel = new RoleViewModel { Name = "RoleForPutTest", DisplayName = "RoleForPutTest" }; StringContent content = new StringContent(viewModel.ToJsonString(), _encoding, _mediaType);//定义put传递的参数、编码和类型 using var host = await TestHostBuild.GetTestHost().StartAsync();//启动TestServer var response = await host.GetTestClientWithToken().PostAsync(_testUrl, content);//先添加一个用于更新测试 viewModel.Id = (await response.Content.ReadAsStringAsync()).GetDeserializeObject<ApiResult<ExecuteResult<Role>>>().data.Result.Id; content = new StringContent(viewModel.ToJsonString(), _encoding, _mediaType); //act response = await host.GetTestClientWithToken().PutAsync(_testUrl, content); var result = (await response.Content.ReadAsStringAsync()).GetDeserializeObject<ApiResult<ExecuteResult>>(); //assert Assert.True(result.data.IsSucceed); //测完把添加的删除 await Delete_Id_ReturnResult(viewModel.Id); } } }
- 用于测试的参数准备好后,启动TestServer
- 从TestServer中获取我们自定义带Token的HttpClient用于接口测试请求
- 有个名为Delete_Id_ReturnResult的测试,使用了参数,所以改为Theory特性而不是Fact,继而给出了InlineData用作默认参数测试
打开测试管理器,运行测试,测试都通过:
项目完成后,如下图所示
说明
- 以上便是模拟服务端和客户端通信,从而集成测试整个网站的接口
- 如果不想对整个网站集成测试,而只是测试某个服务、组件,可以考虑使用moq
- 说一个不太正规的偏门办法,可以在单元测试中自己new一个依赖注入容器,自己注册服务,然后在测试用例里自己解析,也一样可以做到测试组件的目的
来源:https://www.cnblogs.com/kasnti/p/12246180.html