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