umbraco / Umbraco-CMS

Umbraco is a free and open source .NET content management system helping you deliver delightful digital experiences.
https://umbraco.com
Other
4.49k stars 2.69k forks source link

Use Test Environment Settings in Unit and Integration Tests. #8654

Closed JimBobSquarePants closed 2 years ago

JimBobSquarePants commented 4 years ago

In ASP.NET Core environmental settings are loaded without transforms.

The environment itself is determined by the present of the environmental setting ASPNETCORE_ENVIRONMENT. By default three values are supported. Development, Staging, and Production.

It is often a requirement though to use custom setting during testing. This is possible via IWebHostBuilder.ConfigureAppConfiguration used currently in the source...

https://github.com/umbraco/Umbraco-CMS/blob/43efee9647ed8f2eaab3ec2b71d3dd3eb46b9017/src/Umbraco.Tests.Integration/TestServerTest/UmbracoWebApplicationFactory.cs#L52-L59

...but there's a better way which allows test settings to sit in an environmental specific appsettings.Testing.json file in the same manner as other environments.

This is massively beneficial for value discovery for developers contributing to the library since values are easy to find and compare.

Implementing it is trivial. First define an extension environment.

/// <summary>
/// Extension methods for <see cref="IHostEnvironment"/>.
/// </summary>
public static class HostEnvironmentExtensions
{
    /// <summary>
    /// The environment name for the "Testing" environment.
    /// </summary>
    public const string Testing = nameof(Testing);

    /// <summary>
    /// Checks if the current host environment name is <see cref="Testing"/>.
    /// </summary>
    /// <param name="hostEnvironment">An instance of <see cref="IHostEnvironment"/>.</param>
    /// <returns>
    /// True if the environment name is <see cref="Testing"/>, otherwise false.
    /// </returns>
    public static bool IsTesting(this IHostEnvironment hostEnvironment)
        => hostEnvironment.IsEnvironment(Testing);
}

For integration tests wiring it up is as simple as follows.

/// <summary>
/// Gives a fixture an opportunity to configure the application before it gets built.
/// </summary>
/// <param name="builder">The <see cref="IWebHostBuilder" /> for the application.</param>
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
    // Use environment settings
    builder.UseEnvironment(HostEnvironmentExtensions.Testing);

    // Use the same startup class as your application.
    builder.UseStartup<Startup>();

    // TODO: Add any other setting like URLs.

    base.ConfigureWebHost(builder);
}

For unit tests you can create your own IConfiguration instance which can then be used for populating IOptions<T> instances for testing.

    /// <summary>
    /// A test configuration that can be used to read configuration for testing.
    /// </summary>
    public class TestConfiguration
    {
        public TestConfiguration(string environmentName)
        {
            this.EnvironmentName = environmentName;
        }

        public static TestConfiguration Default { get; } = new TestConfiguration(HostEnvironmentExtensions.Testing);

        public string EnvironmentName { get; }

        public string CurrentDirectory { get; set; }

        public IConfiguration GetConfiguration()
        {
            this.CurrentDirectory ??= this.GetCurrentDirectory();

            return new ConfigurationBuilder()
                .SetBasePath(this.CurrentDirectory)
                .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
                .AddJsonFile($"appsettings.{this.EnvironmentName}.json", optional: true)
                .AddEnvironmentVariables()
                .Build();
        }

        private string GetCurrentDirectory()
        {
            string current = Directory.GetCurrentDirectory();
            string absolute = Path.Combine(current, @"RELATIVE_PATH_TO_APPLICATION_SRC");
            return Path.GetFullPath(new Uri(absolute).LocalPath);
        }
    }

That class can now be used to get the full configuration settings.

IConfiguration configuration = TestConfiguration.Default.GetConfiguration();
bergmania commented 4 years ago

Seems like a good idea. The only concern is the connection string as it is created on the fly by the test. But this part could still be handled like today.

JimBobSquarePants commented 4 years ago

The only concern is the connection string as it is created on the fly by the test.

That's um... Odd. Most integration tests will use an SQLite implementation or suchlike.

Shazwazza commented 4 years ago

We require localdb to run the tests which requires creating the connection string in the code for various reasons.

umbrabot commented 2 years ago

Hiya @JimBobSquarePants,

Just wanted to let you know that we noticed that this issue got a bit stale and might not be relevant any more.

We will close this issue for now but we're happy to open it up again if you think it's still relevant (for example: it's a feature request that's not yet implemented, or it's a bug that's not yet been fixed).

To open it this issue up again, you can write @umbrabot still relevant in a new comment as the first line. It would be super helpful for us if on the next line you could let us know why you think it's still relevant.

For example:

@umbrabot still relevant This bug can still be reproduced in version x.y.z

This will reopen the issue in the next few hours.

Thanks, from your friendly Umbraco GitHub bot :robot: :slightly_smiling_face: