為WebApi加上整合測試
每次都要翻翻舊專案回憶,乾脆寫一篇筆記
從發出一個request開始,開始測試API的各個部份,
並且驗證各個功能整合起來是正確的,直接針對需求做驗證
1.從一個新建的WebApi範本專案開始,額外用到了幾個nuget package在測試專案上,輔助測試
dotnet package add Microsoft.AspNet.WebApi.Client
dotnet package add FluentAssertions
dotnet package add NSubstitute
2.為測試專案加上nuget package
dotnet package add Microsoft.AspNetCore.Mvc.Testing
3.新增一個TestBase
- 必須實作 IClassFixture<WebApplicationFactory<Startup>>
- Startup為待測試專案的Startup
- 並且從ctor注入一個 WebApplicationFactory<Startup>
public class TestBase
: IClassFixture<WebApplicationFactory<Startup>>
{
private readonly WebApplicationFactory<Startup> _applicationFactory;
public TestBase(WebApplicationFactory<Startup> applicationFactory)
{
_applicationFactory = applicationFactory;
}
}
4.接著完成第一個測試(這時的Test Class繼承了TestBase
- 從Base的 _applicationFactory 可以 Create 一個 HttpClient,後面的使用就跟一般的HttpClient一樣了
- 發出request,目標是待測試的Api
- 拿回response,並驗證結果
[Fact]
public async Task Test1()
{
var httpClient = _applicationFactory.CreateClient();
var message = await httpClient.GetAsync("/WeatherForecast");
var list = await message.Content.ReadAsAsync<List<WeatherForecast>>();
message.StatusCode.Should().Be(HttpStatusCode.OK);
list.Count.Should().Be(5);
}
5.接著讓程式碼稍微複雜一點,抽出一個Service
//Controller
[HttpGet]
public IEnumerable<WeatherForecast> Get()
{
return _testService.WeatherForecasts();
}
//Service
public class TestService
{
private static readonly string[] Summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};
public IEnumerable<WeatherForecast> WeatherForecasts()
{
var rng = new Random();
return Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
Date = DateTime.Now.AddDays(index),
TemperatureC = rng.Next(-20, 55),
Summary = Summaries[rng.Next(Summaries.Length)]
})
.ToArray();
}
}
6.接著跑個測試,仍然會是通過的
7.接著假設這個Service是我們無法控制的服務,需要模擬掉他
8.在TestBase上加上一個方法,讓我們可以在建立HttpClient時一併替換掉一些在DI註冊的服務
- 把create出來的webHost保存起來,後續會使用到
protected HttpClient CreateHttpClient(Action<IServiceCollection> configureServices)
{
_webHost = _factory.WithWebHostBuilder(builder =>
{
if (configureServices != null)
{
builder.ConfigureServices(configureServices);
}
});
return _webHost.CreateClient();
}
9.回到測試,現在可以把Service給模擬掉了
[Fact]
public async Task Test1()
{
var testService = Substitute.For<ITestService>();
testService.WeatherForecasts().Returns(new List<WeatherForecast>
{
new WeatherForecast(),
});
var httpClient = CreateHttpClient(collection =>
{
collection.AddScoped(r => testService);
});
var message = await httpClient.GetAsync("/WeatherForecast");
var list = await message.Content.ReadAsAsync<List<WeatherForecast>>();
message.StatusCode.Should().Be(HttpStatusCode.OK);
list.Count.Should().Be(1);
}
10.現在已經可以針對API做測試,也可以模擬掉無法控制的服務,接著再加上DB的存取
11.調整一下WebApi
- 在WebApi加上nuget package,為了方便用Sqlite
dotnet add package Microsoft.EntityFrameworkCore dotnet add package Microsoft.EntityFrameworkCore.Sqlite
-
新增DbContext
public class MyDbContext : DbContext { public MyDbContext(DbContextOptions<MyDbContext> options) : base(options) { } public DbSet<Member> Member { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Member>().HasKey(r => r.Name); } }
-
Model
public class Member { public string Name { get; set; } }
-
Startup
services.AddDbContext<MyDbContext>(builder => { builder.UseSqlite("Data Source=my.db"); });
-
在Controller新增一個Action,對Db操作
[HttpGet] public int GetDb() { _dbContext.Member.Add(new Member() { Name = "Controller" }); _dbContext.SaveChanges(); return _dbContext.Member.Count(); }
12.在測試專案上,先把DataBase替換成Memory方便測試(也可以成其他資料庫或是Sqlite)
protected HttpClient CreateHttpClient(Action<IServiceCollection> configureServices)
{
_webHost = _factory.WithWebHostBuilder(builder =>
{
builder.UseEnvironment("Test");
if (configureServices != null)
{
builder.ConfigureTestServices(configureServices);
}
var descriptor = services.SingleOrDefault(d => d.ServiceType == typeof(DbContextOptions<MyDbContext>));
if (descriptor != null)
{
services.Remove(descriptor);
}
//---------------------------------
builder.ConfigureServices(services =>
{
services.AddDbContext<MyDbContext>(options =>
{
options.UseInMemoryDatabase("Test");
});
var serviceProvider = services.BuildServiceProvider();
using (var scope = serviceProvider.CreateScope())
{
var db = scope.ServiceProvider.GetRequiredService<MyDbContext>();
db.Database.EnsureDeleted();
db.Database.EnsureCreated();
InitDb(db);
}
});
//---------------------------------
});
return _webHost.CreateClient();
}
13.並且在TestBase加上一個方法,好讓我們針對DataBase的資料做事後的驗證
- 這邊也可以改成從DI容器拿出特定的服務做其他的驗證,視個人需求調整
public void DbOperator(Action<MyDbContext> action)
{
using (var serviceScope = _webHost.Services.CreateScope())
{
var myDbContext = serviceScope.ServiceProvider.GetRequiredService<MyDbContext>();
action(myDbContext);
}
}
14.最後調整下測試
- 發出request測試方法GetDb
- 執行完畢後僅驗證回應的HttpStatusCode.OK
- 從WebHost內拿出DB並驗證有Add一筆資料進去,且Name相符
[Fact]
public async Task TestDb()
{
var httpClient = CreateHttpClient(null);
var message = await httpClient.GetAsync("/WeatherForecast/GetDb");
message.StatusCode.Should().Be(HttpStatusCode.OK);
DbOperator(context =>
{
var member = context.Member.FirstOrDefault(r => r.Name == "Controller");
member.Should().NotBeNull();
});
}
平常開發上都會加上E2E測試和不少的單元測試,不得已的情況下才會選擇把E2E下的某些服務模擬掉(有些服務要模擬掉也不是太容易的事情阿......)
後續會再寫一篇針對MVC的整合測試,在實務上試了一陣子,還是WebApi的測試親切多了......
Microsoft Docs https://docs.microsoft.com/zh-tw/aspnet/core/test/integration-tests?view=aspnetcore-3.1
Sample Code https://github.com/ianChen806/IntegrationTestSample