MiniAPIのユニットテストはasp.net web apiのユニットテストと大体同じです(どちらもasp.net coreですから)。しかし、細かな違いがいくつかあり、この記事で詳しく説明します。ここで使っているテストフレームワークはXUnit、モックフレームワークはMoqです。これら二つのフレームワークとライブラリについては、ここでは省略します。
まず、二つのプロジェクトを作成します。APIプロジェクトはMiniAPI19UnitTest、UnitTestプロジェクトはMiniAPI19UnitTestUTです。以下のようになります。
MiniAPI19UnitTest
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddScoped<IOrderService, OrderService>();
var app = builder.Build();
app.MapGet("/order", (IOrderService orderService) => {
return "Result:" + orderService.GetOrder("123");
});
app.MapPost("/order", (Order order, IOrderService orderService) => {
return "Result:" + orderService.AddOrder(order);
});
app.Run();
public interface IOrderService {
bool AddOrder(Order order);
string GetOrder(string orderNo);
}
public class OrderService : IOrderService {
private readonly ILogger<OrderService> _logger;
public OrderService(ILogger<OrderService> logger) {
_logger = logger;
}
public string GetOrder(string orderNo) {
return "this is my order,orderno:" + orderNo;
}
public bool AddOrder(Order order) {
_logger.LogInformation(order.ToString());
return true;
}
}
public record Order {
public string OrderNo { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
}
MiniAPI19UnitTestUT:このプロジェクトにMiniAPI19UnitTestプロジェクトの参照を追加します。
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Moq;
using System;
using System.Net.Http;
using System.Threading.Tasks;
using Xunit;
namespace MiniAPI19UnitTestUT {
public class MiniAPI19Test {
[Fact]// 引数なしのテスト
public async Task GetOrderTest() {
var orderNo = "abcd";
// Moqを使ってserverのインターフェースをモックし、層の隔離を実現
var mock = new Mock<IOrderService>();
mock.Setup(x => x.GetOrder(It.IsAny<string>())).Returns(orderNo);
var myapp = new MyAppHostTest(services => services.AddSingleton(mock.Object));
var client = myapp.CreateClient();
var result = await client.GetStringAsync("/order");
Assert.Equal($"Result:{orderNo}", result);
}
[Theory] // 引数ありのテスト
[InlineData(true)]
[InlineData(false)]
public async Task PostOrderTest(bool backResult) {
var mock = new Mock<IOrderService>();
mock.Setup(x => x.AddOrder(It.IsAny<Order>())).Returns(backResult);
var myapp = new MyAppHostTest(services => services.AddSingleton(mock.Object));
var client = myapp.CreateClient();
var content = new StringContent(System.Text.Json.JsonSerializer.Serialize(new Order { OrderNo = "abcd", Name = "Surface Pro 8", Price = 10000 }), System.Text.Encoding.UTF8, "application/json");
var response = await client.PostAsync("/order", content);
var result = await response.Content.ReadAsStringAsync();
Assert.Equal($"Result:{backResult}", result);
}
}
// MiniAPI webホストのクラスをカプセル化し、テストプログラムで使用する
class MyAppHostTest : WebApplicationFactory<Program> {
private readonly Action<IServiceCollection> _services;
public MyAppHostTest(Action<IServiceCollection> services) {
_services = services;
}
protected override IHost CreateHost(IHostBuilder builder) {
builder.ConfigureServices(_services);
return base.CreateHost(builder);
}
}
}
上記のコードでは、Programクラスが見つからないというエラーが発生します。これはAPIプロジェクトがトップレベルの方式で開発されており、Programのアクセス修飾子がinternalであるためです。MiniAPI19UnitTestプロジェクトへの参照を追加しても、Programにはアクセスできません。解決策は二つあります。トップレベルを使用しない方法は以下のようになります:
public class Program {
static void Main(string[] args) {
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddScoped<IOrderService, OrderService>();
var app = builder.Build();
app.MapGet("/test", (IOrderService orderService) => {
return "Result:" + orderService.GetOrder("123");
});
app.Run();
}
}
または、MiniAPI19UnitTest.csprojファイルに以下の設定を追加し、テストプロジェクトがProgramにアクセスできるようにします。
<ItemGroup>
<InternalsVisibleTo Include="MiniAPI19UnitTestUT"/>
</ItemGroup>
反射ツールを使ってAPIプロジェクトを確認すると、Main関数がトップレベルエントリポイント方式であり、Programが見えなくなっています。これで、自分のユニットテストを楽しく書くことができます。
(Translated by GPT)