.Net Core 3.1 WebApplicationFactory TestServer:覆盖启动时找不到 Razor 视图

.Net Core 3.1 WebApplicationFactory TestServer: cannot find Razor view when override Startup

我正在尝试为带有视图的控制器编写集成测试。我这样做是从 2.2 迁移到 .Net Core 3.1 的一部分。 ConfigureServices 中有很多配置我们需要在测试中模拟或禁用,因此,我们从现有的 Startup class 派生并覆盖所需的部分。

现在,我可以使用 WebApplicationFactory 并覆盖 ConfigureWebHost 使其在 .Net Core 3.1 中工作。但是,我更希望不要重写现有的 class 派生自 Startup.

我尝试使用 https://gunnarpeipman.com/aspnet-core-integration-test-startup/ 中的方法,其中我为 WebApplicationFactory 指定派生的 Startup 并调用 UseSolutionRelativeContentRoot(其中有 UseContentRoot我也试过了)。但是,找不到视图。返回的部分异常是:

System.InvalidOperationException: The view 'Index' was not found. The following locations were searched:
Features\Dummy\Index.cshtml
Features\Shared\Index.cshtml
\Features\Index\Dummy.cshtml

如何修复测试?

我有一个 "mock" 项目重现了这个问题。

public class Program
{
    public static void Main(string[] args)
    {
        var host = BuildHost(args);
        host.Run();
    }

    public static IHost BuildHost(string[] args) =>
        Host.CreateDefaultBuilder(args)
        .ConfigureWebHostDefaults(webBuilder => webBuilder
            .UseStartup<Startup>())
        .Build();
}

public class Startup
{
    protected virtual void AddTestService(IServiceCollection services)
    {
        services.TryAddSingleton<IServiceToMock, ServiceToMock>();
    }

    public void ConfigureServices(IServiceCollection services)
    {
        AddTestService(services);

        services.AddMvc()
            .AddFeatureFolders();
    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        app.UseRouting();

        app.UseEndpoints(endpoints => endpoints.MapControllerRoute("default", "{controller=Home}/{action=Index}"));
    }
}

public interface IServiceToMock
{
    Task DoThing();
}

public class ServiceToMock : IServiceToMock
{
    public async Task DoThing() =>
        throw new Exception(await Task.FromResult("service exception"));
}

[Route("candidates/[controller]/[action]")]
public class DummyController : Controller
{
    private readonly IServiceToMock serviceToMock;

    public DummyController(IServiceToMock serviceToMock)
    {
        this.serviceToMock = serviceToMock;
    }

    [HttpGet]
    public IActionResult Index()
    {
        return View();
    }

    [HttpGet]
    public async Task<bool> IsExternal(string email)
    {
        await serviceToMock.DoThing();
        return await Task.FromResult(!string.IsNullOrWhiteSpace(email));
    }
}

Index.cshtml(与控制器在同一文件夹中)

@{
    ViewData["Title"] = "Title";
}

<p>Hello.</p>

csproj:

<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>netcoreapp3.1</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="OdeToCode.AddFeatureFolders" Version="2.0.3" />
  </ItemGroup>

</Project>

测试部分:

public class TestServerFixture : TestServerFixtureBase<Startup, TestStartup>
{
}

public class TestServerFixtureBase<TSUTStartus, TTestStartup> : WebApplicationFactory<TTestStartup>
    where TTestStartup : class where TSUTStartus : class
{
    private readonly Lazy<HttpClient> m_AuthClient;

    public TestServerFixtureBase()
    {
        m_AuthClient = new Lazy<HttpClient>(() => CreateAuthClient());
    }

    protected override IWebHostBuilder CreateWebHostBuilder()
    {
        return WebHost.CreateDefaultBuilder()
            .UseStartup<TTestStartup>();
    }

    public HttpClient AuthClient => m_AuthClient.Value;
    protected virtual HttpClient CreateAuthClient() => WithWebHostBuilder(builder =>
    {
        builder.UseSolutionRelativeContentRoot("NetCore31IntegrationTests3");

        builder.ConfigureTestServices(services =>
        {
            services.AddMvc().AddApplicationPart(typeof(TSUTStartus).Assembly);
        });

    }).CreateClient();
}

public class TestStartup : Startup
{
    protected override void AddTestService(IServiceCollection services)
    {
        services.AddSingleton<IServiceToMock, TestServiceToMock>();
    }
}

public class TestServiceToMock : IServiceToMock
{
    public async Task DoThing() => await Task.CompletedTask;
}

public class HomeControllerTests : IClassFixture<TestServerFixture>
{
    private readonly TestServerFixture _factory;

    public HomeControllerTests(TestServerFixture factory)
    {
        _factory = factory;
    }

    [Theory]
    [InlineData("/candidates/dummy/IsExternal?email=aaa")]
    [InlineData("/candidates/dummy/index")]
    [InlineData("candidates/dummy/index")]
    public async Task Get_EndpointsReturnSuccessAndCorrectContentType(string url)
    {
        // Arrange
        var client = _factory.AuthClient;

        // Act
        var response = await client.GetAsync(url);

        // Assert
        response.EnsureSuccessStatusCode();
    }
}

我试图避免的工作修复:

public class TestServerFixtureBase<TSUTStartus, TTestStartup> : WebApplicationFactory<TSUTStartus>
    where TTestStartup : class where TSUTStartus : class
{
    private readonly Lazy<HttpClient> m_AuthClient;

    public TestServerFixtureBase()
    {
        m_AuthClient = new Lazy<HttpClient>(() => CreateAuthClient());
    }

    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {
        builder.ConfigureServices(services =>
        {
            var descriptor = services.SingleOrDefault(
            d => d.ServiceType ==
                typeof(IServiceToMock));

            if (descriptor != null)
                services.Remove(descriptor);

            services.AddSingleton<IServiceToMock, TestServiceToMock>();
        });
    }
    protected override IWebHostBuilder CreateWebHostBuilder()
    {
        return WebHost.CreateDefaultBuilder()
            .UseStartup<TSUTStartus>();
    }

    public HttpClient AuthClient => m_AuthClient.Value;
    protected virtual HttpClient CreateAuthClient() => WithWebHostBuilder(builder => { }).CreateClient();
}

为了解决问题 WithWebHostBuilder 我添加了

services
    .AddMvc()
    .AddRazorRuntimeCompilation()
    .AddApplicationPart(typeof(TTestStartup).Assembly);

但是,还有其他修复建议,例如:

var builder = new WebHostBuilder();
        builder.ConfigureAppConfiguration((context, b) => {
            context.HostingEnvironment.ApplicationName = typeof(HomeController).Assembly.GetName().Name;
        });

来自 https://github.com/dotnet/aspnetcore/issues/17655#issuecomment-581418168