如何使用 TestHost 達到 Controller/Service 測試目的

在測試 controller 時,在傳統三層式架構上,不但要注入 service 之外,還要注入許多的 repositories,還有許多的 ILogger<T>,若不透過測試工具處理,光是注入這些東西將會痛苦不堪

不管是使用 MSTest 或是 Nunit,若是你想做的是比較高層級的 service test,或是貼近 integration test

通常在注入這些依賴的物件上,都需要花上不少時間

不是不寫測試,而是真的太麻煩了!!!

在測試專案裡使用 Microsoft.AspNetCore.TestHost

使用 Microsoft.AspNetCore.TestHost 這套件可以幫你解決掉很大一部份的問題

在測試專案的使用上,你可以像 api 站台一樣,也可以使用讀取到 appsetting.json 的內容

注入的方式也跟 api 站台裡的完全一樣

  1. 建立假的 startup






public class TestStartup
{
    private IConfiguration _configuration { set; get; }
    public TestStartup(IConfiguration configuration) => _configuration = configuration;


    public void ConfigureServices(IServiceCollection services)
    {
        services.Configure<ApiSetting>(_configuration.GetSection("ApiSetting"));
    
        //add services
        services.AddScoped<IMemberService, MemberService>();
        services.AddScoped(p => Substitute.For<IReportService>());
        //fake repoes
        services.AddScoped(p => Substitute.For<IMemberRepository>());

        services.AddScoped<MemberController>();
        services.AddMemoryCache();
    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        //add middleware here
    }
}

這個類別,你就可以像 Controller 一樣注入你需要的所有東西

或是你想透過 NSubstitute 去假造不需要測試的介面

2. 建立測試基底

這邊你可以發現甚至你可以使用環境變數去決定你要綁到什麼環境給它使用

public class BaseHostTest
{
    protected IServiceProvider _serviceProvider;
    protected ApiSetting _options;
    
    public void MockService()
    {
        var config = new ConfigurationBuilder().Build();
        var builder = new WebHostBuilder()
            .UseConfiguration(config)
            .UseEnvironment("Development")
            .ConfigureAppConfiguration((ctx, cfg) =>
            {
                cfg.AddJsonFile("appsettings.json", optional: false)
                   .AddJsonFile($"appsettings.{ctx.HostingEnvironment.EnvironmentName}.json", optional: true);
            })
            .UseStartup<TestStartup>();

        var host = builder.Build();

        _serviceProvider = host.Services;
        _options = _serviceProvider.GetRequiredService<IOptions<ApiSetting>>().Value;
    }
}

3. 開始測試

假設 Controller 只有很單純的行為

namespace Mg.Supplier.AG.Api.Controllers
{
    [ApiController]
    public class MemberController : ControllerBase
    {
        private readonly IMemberService _service;
        private readonly ILogger _logger;
        private readonly IOptionsMonitor<ApiSetting> _options;
        public SeamlessWalletController(IMemberService service, ILogger<MemberController> logger, IOptionsMonitor<ApiSetting> options)
        {
            _service = service;
            _logger = logger;
            _options = options;
        }

        [HttpGet("test")]
        public IActionResult Test(string name)
        {
            return Ok($"Test {name}");
        }
    }
}

    public class ControllerTest : BaseHostTest
    {

        [SetUp]
        public void Setup()
        {
            MockService();
        }

        [Test]
        public async Task WhenControllerCalledThenReturnOK()
        {
            //arrange
            var con = _serviceProvider.GetRequiredService<MemberController>();

            //act
            var response = (OkObjectResult)con.Test("Ace");

            //assert
            response.StatusCode.Should().Be(HttpStatusCode.OK.GetHashCode());
            response.Value.Should().Be("Test Ace");
        }
    }

如此一來就能很快速的測試到比較高層級的 Service/Controller 部份