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一個依賴注入容器,自己注冊服務,然后在測試用例里自己解析,也一樣可以做到測試組件的目的