DEV Community

Dennis
Dennis

Posted on • Edited on

Integration testing in Umbraco 10+ in 4 easy steps

Automated testing is one of those things: Everyone should do it, but not everyone does it. When you find yourself amongst developers who are only used to manual testing, it can be difficult to get the ball rolling. One of the factors that I think plays a big role is the difficulty to get started.

A good way to introduce automated testing is by automating the manual testing process. With that I mean: automate the process of requesting URLs on your website and checking what result is returned. In this post, I introduce an easy method to create integration tests with Umbraco 10+ websites that might get the ball rolling for you.

Step 1: Running your Umbraco website in-memory

Microsoft has introduced the tools to make it surprisingly easy to start your website in-memory. It's called: WebApplicationFactory. You will need to install a NuGet package: Microsoft.AspNetCore.Mvc.Testing
To prepare your code, create a class like this:

MyCustomWebApplicationFactory.cs

public class MyCustomWebApplicationFactory
    : WebApplicationFactory<Program>
// 'Program' is the class that contains your Main method.
{ }
Enter fullscreen mode Exit fullscreen mode

That's all! That is your whole website running in-memory.
Now here's a small test class to illustrate how you would use this (using NUnit):

MyIntegrationTests.cs

public class MyIntegrationTests
{
    private MyCustomWebApplicationFactory _websiteFactory;

    private MyCustomWebApplicationFactory CreateApplicationFactory()
    {
        return new MyCustomWebApplicationFactory();
    }

    [SetUp]
    public virtual void Setup()
    {
        _websiteFactory = CreateApplicationFactory();
    }

    [TearDown]
    public virtual void TearDown()
    {
        _websiteFactory.Dispose();
    }

    [TestCase(TestName = "Root page returns 200 OK")]
    public async Task GetRootPage_HappyFlow_ReturnsOK()
    {
        // arrange
        var client = _websiteFactory.CreateClient();

        // act
        var response = await client.GetAsync("/");

        // assert
        Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK))
    }
}
Enter fullscreen mode Exit fullscreen mode

Step 2: Connecting to a temporary database

You will need this or suffer the consequences 👿. A fresh database ensures that you don't accidentally create dependencies between your tests or between your manual testing environment and your automated testing environment. My example will show how to connect your Umbraco 10+ website to an in-memory sqlite database.

To do this, we will make some changes to the web application factory

MyCustomWebApplicationFactory.cs

public class MyCustomWebApplicationFactory
    : WebApplicationFactory<Program>
{
    private const string _inMemoryConnectionString = "Data Source=IntegrationTests;Mode=Memory;Cache=Shared";
    private readonly SqliteConnection _imConnection;

    public MyCustomWebApplicationFactory()
    {
        // Shared in-memory databases get destroyed when the last connection is closed.
        // Keeping a connection open while this web application is used, ensures that the database does not get destroyed in the middle of a test.
        _imConnection = new SqliteConnection(_inMemoryConnectionString);
        _imConnection.Open();
    }

    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {
        Environment.SetEnvironmentVariable("ASPNETCORE_ENVIRONMENT", "Development");
        builder.ConfigureAppConfiguration(conf =>
        {
            conf.AddInMemoryCollection(new KeyValuePair<string, string>[]
            {
                new KeyValuePair<string, string>("ConnectionStrings:umbracoDbDSN", _inMemoryConnectionString),
                new KeyValuePair<string, string>("ConnectionStrings:umbracoDbDSN_ProviderName", "Microsoft.Data.Sqlite")
            });
        });
    }

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

        // When this application factory is disposed, close the connection to the in-memory database
        //    This will destroy the in-memory database
        _imConnection.Close();
        _imConnection.Dispose();
    }
}
Enter fullscreen mode Exit fullscreen mode

NOTE: You can also setup temporary databases with SqlServer and/or LocalDb, but the in-memory sqlite database is significantly faster. A test that takes 10 - 20 seconds with LocalDb can take between 1 - 6 seconds using sqlite in-memory.

At this point, your in-memory web application will likely not be able to boot, so let's fix that:

Step 3: Setting up testing configurations

You'll need to override some settings in your appsettings to make sure your application works. Most importantly: you need to set up automatic installation of your Umbraco. I find it's most convenient to introduce an additional appsettings file:

integration.settings.json

{
  "Umbraco": {
    "CMS": {
      "Unattended": {
        "UpgradeUnattended": true,
        "InstallUnattended": true,
        "UnattendedUserName": "Test",
        "UnattendedUserEmail": "test@test.nl",
        "UnattendedUserPassword": "1234567890"
      },
      "Hosting": {
        "Debug": true
      },
      "ModelsBuilder": {
        "ModelsMode": "Nothing"
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Now you need to make a small adjustment to the application factory like so:

protected override void ConfigureWebHost(IWebHostBuilder builder)
{
    Environment.SetEnvironmentVariable("ASPNETCORE_ENVIRONMENT", "Development");

    // I placed the config in the root directory of my integration test project. Change the path to wherever you store your config file
    var projectDir = Directory.GetCurrentDirectory();
    var configPath = Path.Combine(projectDir, "integration.settings.json");
    builder.ConfigureAppConfiguration(conf =>
    {
        conf.AddJsonFile(configPath);
        conf.AddInMemoryCollection(new KeyValuePair<string, string>[]
        {
            new KeyValuePair<string, string>("ConnectionStrings:umbracoDbDSN", _inMemoryConnectionString),
            new KeyValuePair<string, string>("ConnectionStrings:umbracoDbDSN_ProviderName", "Microsoft.Data.Sqlite")
        });
    });
}
Enter fullscreen mode Exit fullscreen mode

If you happen to use uSync (I strongly recommend that you do, it's a lifechanger!), then you'll find it's incredibly easy to build up your website in your in-memory database. Just enable ImportOnFirstBoot! If you only have a testwebsite to test your plugin on, then you could also install a starterkit, which also seeds your website with some basic content on first boot.

Step 4: Get your coworkers excited

To really push your coworkers over the edge, you'll likely want to make their life a little bit easier still. Let's make a base class to make testing a little bit more convenient:

IntegrationTestingBase.cs

public abstract class IntegrationTestBase
{
    protected MyCustomWebApplicationFactory WebsiteFactory { get; private set; }
    protected AsyncServiceScope Scope { get; private set; }
    protected IServiceProvider ServiceProvider => Scope.ServiceProvider;

    protected virtual MyCustomWebApplicationFactory CreateApplicationFactory()
    {
        return new MyCustomWebApplicationFactory();
    }

    [SetUp]
    public virtual void Setup()
    {
        WebsiteFactory = CreateApplicationFactory();
        Scope = WebsiteFactory.Services.GetRequiredService<IServiceScopeFactory>().CreateAsyncScope();
    }

    [TearDown]
    public virtual void TearDown()
    {
        Scope.Dispose();
        WebsiteFactory.Dispose();
    }

    protected virtual HttpClient Client
        => WebsiteFactory.CreateClient();

    protected virtual GetService<TType>()
        => ServiceProvider.GetService<TType>();

    protected virtual async Task<T> GetAsync<T>(string url)
    {
        var response = await Client.GetAsync(url);
        return JsonConvert.DeserializeObject<T>(await response.Content.ReadAsStringAsync());
    }
}
Enter fullscreen mode Exit fullscreen mode

Now your integration tests could look like this:

MyIntegrationTests.cs

public class MyIntegrationTests : IntegrationTestBase
{
    [TestCase(TestName = "My api endpoint returns model from database")]
    public async Task MyBasicTest()
    {
        // arrange
        var expected = new MyModel();
        GetService<IMyModelService>().Create(expected);

        // act
        var result = await GetAsync<MyModel>($"/umbraco/api/mymodels/get/{expected.Id}");

        // assert
        Assert.That(result, Is.EqualTo(expected));
    }
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

It's surprisingly easy to get started with integration testing with Umbraco 10+ and I've had a lot of fun figuring out how to make this work.

What especially sells this concept to me is that it doesn't introduce a whole new testing concept, but rather adds a layer of automation over the manual testing process. It makes it so much more convincing to coworkers who have doubts about automated testing and it doesn't require them to immediately adopt a new coding style.

Even if your Umbraco 10+ code doesn't lend itself well for unit testing, this approach allows you to automatically test the bigger chunks of your application, so it's never too late to introduce this in your projects.

Top comments (0)