anon_dcs3spp
anon_dcs3spp

Reputation: 3012

WebApplicationFactory - InvalidOperationException: Solution root could not be located using application root - When Testing From Docker Container

I am learning ASP.NET Core and have subclassed WebApplicationFactory class with the ConfigureWebHost method overriden as shown in the listing below. I have also created a small GitHub repository to highlight the issues that I am encountering.

/// <summary>
/// Set the content root to path relative to sln, e.g. Src/WebApp
/// Configure the webhost using appsettings.json from the path where the assembly is located for 
/// the startup class. The Autofac container is configured here also.
/// </summary>
/// <seealso cref="Microsoft.AspNetCore.Mvc.Testing.ConfigureWebHost(IWebHostBuilder)"/></seealso>
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
    string path = Assembly.GetAssembly(typeof(WebApiTestFactory<TStartupClass>)).Location;

    builder.UseSolutionRelativeContentRoot(_contentRoot)
        .ConfigureAppConfiguration(cb =>
        {
            cb.AddJsonFile($"{Path.GetDirectoryName(path)}/appsettings.json", optional: false)
                .AddEnvironmentVariables();
        })
        .ConfigureServices(services => services.AddAutofac());

    base.ConfigureWebHost(builder);
}

This uses UseSolutionRelativeContentRoot to set the path for content root that is relative to the directory containing *.sln files.

My integration test runs successfully within local development environment.

However, when I run the test within a docker container an InvalidOperationException is thrown: Solution root could not be located using application root

Any ideas how I can solve this?

Exception

A total of 1 test files matched the specified pattern.
/Tests/FunctionalTests/WebApp.FunctionalTests/bin/Debug/netcoreapp3.1/WebApp.FunctionalTests.dll
[xUnit.net 00:00:00.00] xUnit.net VSTest Adapter v2.4.1 (64-bit .NET Core 3.1.9)
[xUnit.net 00:00:00.88]   Discovering: WebApp.FunctionalTests
[xUnit.net 00:00:00.94]   Discovered:  WebApp.FunctionalTests
[xUnit.net 00:00:00.95]   Starting:    WebApp.FunctionalTests
[xUnit.net 00:00:01.17]     WebApp.FunctionalTests.ApiTest.WebApp_ApiController_DownloadImage [FAIL]
[xUnit.net 00:00:01.17]       System.InvalidOperationException : Solution root could not be located using application root /Tests/FunctionalTests/WebApp.FunctionalTests/bin/Debug/netcoreapp3.1/.
[xUnit.net 00:00:01.18]       Stack Trace:
[xUnit.net 00:00:01.18]            at Microsoft.AspNetCore.TestHost.WebHostBuilderExtensions.UseSolutionRelativeContentRoot(IWebHostBuilder builder, String solutionRelativePath, String applicationBasePath, String solutionName)
[xUnit.net 00:00:01.18]            at Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactory`1.SetContentRoot(IWebHostBuilder builder)
[xUnit.net 00:00:01.18]            at Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactory`1.EnsureServer()
[xUnit.net 00:00:01.18]            at Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactory`1.CreateDefaultClient(DelegatingHandler[] handlers)
[xUnit.net 00:00:01.18]            at Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactory`1.CreateDefaultClient(Uri baseAddress, DelegatingHandler[] handlers)
[xUnit.net 00:00:01.18]            at Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactory`1.CreateClient(WebApplicationFactoryClientOptions options)
[xUnit.net 00:00:01.18]            at Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactory`1.CreateClient()
[xUnit.net 00:00:01.18]         /Tests/FunctionalTests/WebApp.FunctionalTests/ApiTest.cs(45,0): at WebApp.FunctionalTests.ApiTest.WebApp_ApiController_DownloadImage()
[xUnit.net 00:00:01.18]         --- End of stack trace from previous location where exception was thrown ---
[xUnit.net 00:00:01.19]   Finished:    WebApp.FunctionalTests
  X WebApp.FunctionalTests.ApiTest.WebApp_ApiController_DownloadImage [93ms]
  Error Message:
   System.InvalidOperationException : Solution root could not be located using application root /Tests/FunctionalTests/WebApp.FunctionalTests/bin/Debug/netcoreapp3.1/.
  Stack Trace:
     at Microsoft.AspNetCore.TestHost.WebHostBuilderExtensions.UseSolutionRelativeContentRoot(IWebHostBuilder builder, String solutionRelativePath, String applicationBasePath, String solutionName)
   at Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactory`1.SetContentRoot(IWebHostBuilder builder)
   at Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactory`1.EnsureServer()
   at Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactory`1.CreateDefaultClient(DelegatingHandler[] handlers)
   at Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactory`1.CreateDefaultClient(Uri baseAddress, DelegatingHandler[] handlers)
   at Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactory`1.CreateClient(WebApplicationFactoryClientOptions options)
   at Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactory`1.CreateClient()
   at WebApp.FunctionalTests.ApiTest.WebApp_ApiController_DownloadImage() in /Tests/FunctionalTests/WebApp.FunctionalTests/ApiTest.cs:line 45
--- End of stack trace from previous location where exception was thrown ---

Test Run Failed.
Total tests: 1
     Failed: 1
 Total time: 2.0426 Seconds
/usr/share/dotnet/sdk/3.1.403/Microsoft.TestPlatform.targets(32,5): error MSB4181: The "Microsoft.TestPlatform.Build.Tasks.VSTestTask" task returned false but did not log an error. [/Tests/FunctionalTests/WebApp.FunctionalTests/WebApp.FunctionalTests.csproj]

Test Project

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

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

    <IsPackable>false</IsPackable>
  </PropertyGroup>

  <ItemGroup>
    <Content Update="xunit.runner.json">
      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
    </Content>
  </ItemGroup>

  <ItemGroup>
    <PackageReference Include="Autofac" Version="6.0.0" />
    <PackageReference Include="Microsoft.AspNetCore" Version="2.2.0" />
    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.2.0" />
    <PackageReference Include="xunit" Version="2.4.1" />
    <PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" />
    <PackageReference Include="coverlet.collector" Version="1.0.1" />
  </ItemGroup>

  <ItemGroup>
    <ProjectReference Include="..\..\..\Src\WebApp\WebApp.csproj" />
    <ProjectReference Include="..\..\Functional\Utilities\WebApp.Functional.Utilities.csproj" />
    <ProjectReference Include="..\..\..\Src\WebApp.S3.Contracts\WebApp.S3.Contracts.csproj" />
  </ItemGroup>

</Project>

Also added Microsoft.AspNetCore.Mvc.Testing dependency to the test project as detailed here and still get the same result:

 <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="3.1.9" />

Also tried adding WebApplicationFactoryContentRoot attribute in Test Project:

using System;
using System.IO;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;

using Microsoft.AspNetCore.Hosting;
using Autofac.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.AspNetCore.Mvc.Testing;
using Xunit;
using Xunit.Abstractions;

using WebApp.S3.Contracts;
using WebApp.Functional.Utilities;


/// key should match FullName assembly attributed of TStartup : WebApplicationFactory<TStartup> 
/// https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.mvc.testing.webapplicationfactorycontentrootattribute?view=aspnetcore-3.0
[assembly: WebApplicationFactoryContentRoot(
    key: "WebApp.FunctionalTests, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null",
    contentRootPath: "../../../../../../Src/WebApp",
    contentRootTest: "Program.cs",
    priority: "1")]

namespace WebApp.FunctionalTests
{
    public class ApiTest
    {
        private TestingBase<WebAppTestStartup> _testing;
        private string _settingsFile;
        
        
        private string GetSettingsFile() 
        {
            const string AspNetCoreEnvironment="ASPNETCORE_ENVIRONMENT";
            const string DefaultSettingsFile = "appsettings.Local.json";
            
            var envVar = System.Environment.GetEnvironmentVariable(AspNetCoreEnvironment);

            if(envVar != null)
                return $"appsettings.{envVar}.json";
            else
                return DefaultSettingsFile;
        }

        /// <summary>
        /// Create Web Application Factory with content root at Src/WebApp using
        /// appsettings file derived from ASPNETCORE_ENVIRONMENT variable. If
        /// the variable is unset then defaults to appsettings.Local.json
        /// </summary>
        /// <param name="output">xUnit output stream</param>
        public ApiTest(ITestOutputHelper output)
        {
            const string contentRoot = "Src/WebApp";
            
            _settingsFile = GetSettingsFile();
            
            string startupPath = Assembly.GetAssembly(typeof(WebApiTestFactory<WebAppTestStartup>)).Location;
            string settingsFilePath = $"{Path.GetDirectoryName(startupPath)}/{_settingsFile}";

            _testing = new TestingBase<WebAppTestStartup>(output, contentRoot, settingsFilePath);
        }

        [Fact]
        public async Task WebApp_ApiController_DownloadImage()
        {
            const string TestData = "test";
            const string ObjectName = "objectName";
            byte[] Payload = Encoding.UTF8.GetBytes(TestData);

            // upload a sample image
            using (var client = _testing.Factory.CreateClient())
            using (var scope = _testing.Factory.Server.Host.Services.CreateScope())
            using (var byteStream = new MemoryStream(Payload))
            using (var stream = new BufferedStream(byteStream))
            {
                var s3Client = scope.ServiceProvider.GetRequiredService<IS3Service>();
                await s3Client.UploadAsync(stream, ObjectName);

                var response = await client.GetAsync(ApiRoutes.Get.Image(ObjectName));
                response.EnsureSuccessStatusCode();

                string result = await response.Content.ReadAsStringAsync();

                Assert.Equal(TestData, result);
            }
        }
    }
}

Also tried adding this to my csproj file:

<Target Name="AddGitMetadaAssemblyAttributes" BeforeTargets="CoreGenerateAssemblyInfo">
    <ItemGroup>
        <AssemblyAttribute Include="Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactoryContentRoot">
            <_Parameter1>WebApp.FunctionalTests, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null</_Parameter1>
            <_Parameter2>../../../Src/WebApp</_Parameter2>
            <_Parameter3>appsettings.Development.json</_Parameter3>
            <_Parameter4>1</_Parameter4>
        </AssemblyAttribute>
    </ItemGroup>
  </Target>

WebApp.FunctionalTests is the assembly that contains my Startup class. This is derived from the live Startup class that exists in a different assembly.

Resorted to creating a smaller project and debug from there when have time. Also raised an issue on Github.

Upvotes: 3

Views: 1752

Answers (1)

Max
Max

Reputation: 1715

Microsoft.AspNetCore.Mvc.Testing already has a class for creating TestsServers for integration tests with .net core and ways to replace configuration, not sure why you need to create your own. I would recommend adding your configuration to .net core´s DI and replacing your configuration in ConfigureServices.

I´ve written integration tests for a WebAPI like this. You can enclose this for your specific needs into an abstract class that you inherit from in each of your test classes.

var appFactory = new WebApplicationFactory<Startup>()
   .WithWebHostBuilder(builder => 
      {
         builder.ConfigureServices(services => {
             // Remove/Add Services from .net CORE DI
             services.Remove(services.SingleOrDefault(
                  d => d.ServiceType == typeof(IExampleDbSettings)));
             services.AddSingleton<IExampleDbSettings>(new TestDbSettings { ... });
         }
      }

// Test HTTP Client
var httpClient = appFactory.CreateClient(); //Http Client to connect to TestServer
// Request a Token and add it to client if needed
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("bearer", "<jwt_token>");

Upvotes: 1

Related Questions