1

I am trying to write an integration test for a controller with a view. I do it as part of migration to .Net Core 3.1 from 2.2. There is a lot of configuration in ConfigureServices that we need to mock or disable in the tests, so, we derive from existing Startup class and override the parts needed.

Now, I can make it working in .Net Core 3.1 using WebApplicationFactory and overriding ConfigureWebHost. However, I rather hoped to not rewrite the existing class that derives from Startup.

I tried to use the approach from https://gunnarpeipman.com/aspnet-core-integration-test-startup/ where I specify the derived Startup for WebApplicationFactory and call UseSolutionRelativeContentRoot (which has UseContentRoot inside which I also tried). However, the views cannot be found. Part of the exception returned is:

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

How could I fix the tests?

I have a "mock" project where I reproduce the issue.

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 (goes in the same folder as the controller)

@{
    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>

The test part:

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();
    }
}

The working fix I am trying to avoid:

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();
}
Mykola
  • 197
  • 1
  • 10

1 Answers1

3

To fix the problem into WithWebHostBuilder I added

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

However, there were other fixes suggested, for example:

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

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

Mykola
  • 197
  • 1
  • 10
  • This works. Also note that, in my case, the test projects are in the same parent folder with the projects under test (PFWP). As of now, and it's v5.0, you'll end up with a different unsolvable error, if you'll have the test projects outside of the PFWP. – lnaie Jan 24 '21 at 13:59
  • You have no idea how happy I am for this answer... I was scratching my head about this for hours to no avail. THANK YOU!! – Andreas Forslöw Jan 12 '22 at 14:30