Home > Mobile >  .NET 6 E2E tests with test server fail on TeamCity CI, but work run manually
.NET 6 E2E tests with test server fail on TeamCity CI, but work run manually

Time:06-01

I have a .NET 6 app (with ASP.NET Core web part). I use cypress for E2E testing. After we migrated to .NET 6 from .NET Framework, I wanted to run E2E cypress tests on the web application.

My process is that I actually run nUnit tests in C# that initialize the in-memory SQLite database and then use this database connection for my app. I use such an approach to create data needed for E2E tests before the actual tests are run. Then I run cypress from C# unit test. This all worked fine in .NET Framework, where the web application was actually run during the tests, so it was available on a real URL within IIS.

With .NET 6, we have WebApplicationFactory class that allows running the app in-memory. However, I wanted my web application to be available under a real URL with a real port, so I can launch cypress tests against it.

After exploring various ideas from this GitHub discussion, I created a factory of real test apps (with real IP and port):

public abstract class RealTestServerWebAppFactory<TStartup> : WebApplicationFactory<TStartup> where TStartup : class
{
    private bool _disposed;
    private IHost _host;

    public RealTestServerWebAppFactory()
    {
        ClientOptions.AllowAutoRedirect = false;
        ClientOptions.BaseAddress = new Uri("http://localhost");
    }
    
    public string ServerAddress
    {
        get
        {
            EnsureServer();
            return ClientOptions.BaseAddress.ToString();
        }
    }
    
    public override IServiceProvider Services
    {
        get
        {
            EnsureServer();
            return _host!.Services!;
        }
    }
    
    // Code created with advices from https://github.com/dotnet/aspnetcore/issues/4892
    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {
        base.ConfigureWebHost(builder);
        
        // Final port will be dynamically assigned
        builder.UseUrls("http://127.0.0.1:0");
        
        builder.ConfigureServices(ConfigureServices);
    }
    
    protected override IHost CreateHost(IHostBuilder builder)
    {
        // Create the host for TestServer now before we
        // modify the builder to use Kestrel instead.
        var testHost = builder.Build();

        // Modify the host builder to use Kestrel instead
        // of TestServer so we can listen on a real address.
        builder.ConfigureWebHost(webHostBuilder => webHostBuilder.UseKestrel());

        // Create and start the Kestrel server before the test server,
        // otherwise due to the way the deferred host builder works
        // for minimal hosting, the server will not get "initialized
        // enough" for the address it is listening on to be available.
        // See https://github.com/dotnet/aspnetcore/issues/33846.
        _host = builder.Build();
        _host.Start();

        // Extract the selected dynamic port out of the Kestrel server
        // and assign it onto the client options for convenience so it
        // "just works" as otherwise it'll be the default http://localhost
        // URL, which won't route to the Kestrel-hosted HTTP server.
        var server = _host.Services.GetRequiredService<IServer>();
        var addresses = server.Features.Get<IServerAddressesFeature>();

        ClientOptions.BaseAddress = addresses!.Addresses
            .Select(x => new Uri(x))
            .Last();

        // Return the host that uses TestServer, rather than the real one.
        // Otherwise the internals will complain about the host's server
        // not being an instance of the concrete type TestServer.
        // See https://github.com/dotnet/aspnetcore/pull/34702.
        testHost.Start();
        return testHost;
    }

    protected override void Dispose(bool disposing)
    {
        base.Dispose(disposing);

        if (!_disposed)
        {
            if (disposing)
            {
                _host?.Dispose();
            }

            _disposed = true;
        }
    }

    protected abstract void ConfigureServices(IServiceCollection services);

    /// <summary>
    /// Makes sure that the server has actually been run.
    /// It must be executed at lest once to have the address and port assigned and the app fully working
    /// </summary>
    private void EnsureServer()
    {
        if (_host is null)
        {
            using var _ = CreateDefaultClient();
        }
    }
}

Then I create a class deriving from RealTestServerWebAppFactory, where I set up some test dependency injection, but this nothing important for our example. Let's call this child class MyServerTestApp.

An instance creation in the tests looks as follows:

var serverTestApp = new MyServerTestApp();
serverTestApp.ServerAddress;

At the second line, EnsureServer() method is called which creates a default client for the app. It also calls CreateHost(IHostBuilder builder) from RealTestServerWebAppFactory class which does all the magic. Locally, after this line I have the URL for the app and I can access it via a web browser (or E2E tests).

Now, the problem is that the tests work fine locally. However, I also execute them on CI - TeamCity to be more precise. The dotnet test command used to run those tests by TeamCity looks as follows:

"C:\Program Files\dotnet\dotnet.exe" test C:\TeamCity\buildAgent\work\c9a5fb5bca7f6666\Tests\E2ETests\bin\Release\net6.0-windows\MyApp.E2ETests.dll /logger:logger://teamcity /TestAdapterPath:C:\TeamCity\buildAgent\plugins\dotnet\tools\vstest15 /logger:console;verbosity=normal

which is in fact quite straightforward use of dotnet test command. However, these tests fail executed by TeamCity with the following exception:

[12:47:54][dotnet test] A total of 1 test files matched the specified pattern.
[12:47:56][dotnet test] NUnit Adapter 4.2.0.0: Test execution started
[12:47:56][dotnet test] Running all tests in C:\TeamCity\buildAgent\work\c9a5fb5bca7f6666\Tests\E2ETests\bin\Release\net6.0-windows\MyApp.E2ETests.dll
[12:47:58][dotnet test]    NUnit3TestExecutor discovered 10 of 10 NUnit test cases using Current Discovery mode, Non-Explicit run
[12:48:07][dotnet test] Setup failed for test fixture MyApp.E2ETests.SomeTestsClass
[12:48:07][dotnet test] System.InvalidOperationException : Sequence contains no elements
[12:48:07][dotnet test] StackTrace:    at System.Linq.ThrowHelper.ThrowNoElementsException()
[12:48:07][dotnet test]    at System.Linq.Enumerable.Last[TSource](IEnumerable`1 source)
[12:48:07][dotnet test]    at MyApp.E2ETests.Infrastructure.RealTestServerWebAppFactory`1.CreateHost(IHostBuilder builder) in C:\TeamCity\buildAgent\work\c9a5fb5bca7f6666\Tests\MyApp.E2ETests\Infrastructure\RealTestServerWebAppFactory.cs:line 85
[12:48:07][dotnet test]    at Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactory`1.ConfigureHostBuilder(IHostBuilder hostBuilder)
[12:48:07][dotnet test]    at Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactory`1.EnsureServer()
[12:48:07][dotnet test]    at Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactory`1.CreateDefaultClient(DelegatingHandler[] handlers)
[12:48:07][dotnet test]    at MyApp.E2ETests.Infrastructure.RealTestServerWebAppFactory`1.EnsureServer() in C:\TeamCity\buildAgent\work\c9a5fb5bca7f6666\Tests\MyApp.E2ETests\Infrastructure\RealTestServerWebAppFactory.cs:line 122
[12:48:07][dotnet test]    at MyApp.E2ETests.Infrastructure.RealTestServerWebAppFactory`1.get_ServerAddress() in C:\TeamCity\buildAgent\work\c9a5fb5bca7f6666\Tests\MyApp.E2ETests\Infrastructure\RealTestServerWebAppFactory.cs:line 35
System.InvalidOperationException : Sequence contains no elements

It seems that this code:

var addresses = server.Features.Get<IServerAddressesFeature>();

returns an empty connection of addresses being run by TeamCity. As a consequence, this code:

ClientOptions.BaseAddress = addresses!.Addresses
            .Select(x => new Uri(x))
            .Last();

fails with System.InvalidOperationException : Sequence contains no elements.

What's weird is that if I run those tests using the same dotnet test command as TeamCity does manually using cmd, it works. It works on the same CI machine, but executed manually from cmd, not as TeamCity's build step. The CI machine runs Windows Server 2019.

I've been thinking it might be something related to the rights/permissions of the user used for running TeamCity. However, the Windows Service for TeamCity uses "Local System" account, so it should have all permissions as far as I know.

Any help/ideas are appreciated :)

CodePudding user response:

According to this answer, you have to set the default address on IServerAddressesFeature using the following code:

var serverFeatures = featureCollection.Get<IServerAddressesFeature>();
if (serverFeatures.Addresses.Count == 0)
{
    ListenOn(DefaultAddress); // Start the server on the default address
    serverFeatures.Addresses.Add(DefaultAddress) // Add the default address to the IServerAddressesFeature
}
  • Related