Integration testing with Episerver CMS 11

December 5, 2017

What do you need to do to set up an Episerver CMS environment in a test environment so that you can run integration tests through the whole stack? It’s probably easier than you might believe, especially with some of the enhancements that were released in CMS 11.

In this post, I will walk you through an example of how you can setup the environment ready to run an API level integration test. With this, I refer to tests that verify the functionality from the service API level down to the database avoiding the complexity of the requests/response layer with the need for setting up a web server and making HTTP requests as part of the tests.

We will create a temporary database, start the Episerver CMS instance, apply an export package with a default structure, run the tests and finally tear down the instance and clear the database. While some of these parts might not apply to your environment or need changes to fit they can hopefully provide you with some ideas and guidance.

I will use xUnit.net as the testing framework in this example, but it should not be difficult to modify it to fit with whichever test framework that you are using.

In this example, we will only run the setup and teardown once for the whole test assembly, but it’s up to you if you want to run it more frequently.

The full source code used in this example can be found at https://github.com/hennys/integration-test-sample/.

Creating a temporary database

There are many ways to solve this problem and as I cannot claim to be an expert database administrator in any capacity I won’t attempt to claim that this is the best way to archive this.

The sample repository contains a simple helper class that can provide an example of how it can be done in a way that has worked successfully for us. Each time the test setup is run, a new database name is generated and then that database is created on the database server. This requires that the user who’s running the test has enough access rights to the database server, but this is usually not a problem in a development environment. The helper class wraps this all up in a disposable object that will remove the database when the instance is disposed. Usage is dead simple.

_database = DatabaseHelper.Temporary(“[connection string]”);

The database will be created empty and we will let the Episerver instance create the database schema itself during the initialization by using a configuration setting. Exactly how this is done is described in the section below.

Configuring the application

Just as your web application is configured using the web.config you can normally use a standard app.config file to configure the Episerver instance for your integration tests. Some test frameworks have a few quirks in this area so you may be happy to learn that we have made it much easier to configure Episerver from code in CMS 11. The easiest way to do this is by creating an initialization module and do your configuration in the ConfigureContainer method.

[InitializableModule]
[ModuleDependency(typeof(EPiServer.Web.InitializationModule))]
public class IntegrationTestInitialization : IConfigurableModule
{
    public void ConfigureContainer(ServiceConfigurationContext context)
    {
        context.Services.Configure<DataAccessOptions>(o =>
        {
            o.SetConnectionString("...");
            o.CreateDatabaseSchema = true;
        });
    }

    public void Initialize(InitializationEngine context) { }
    public void Uninitialize(InitializationEngine context) { }
}

While your application may require additional configuration, these two are the only ones required in our basic example. The first method, SetConnectionString will set the default connection string to our freshly created database and set CreateDatabaseSchema to true will ensure that the database schema is deployed when started.

Starting the Episerver instance

The next step is to start the Episerver instance. This is done by creating a new instance of the EPiServer.Framework.Initialization.InitializationEngine class and calling the Initialize method.

_engine = new InitializationEngine((IEnumerable)null, HostType.TestFramework, new AssemblyList(true).AllowedAssemblies);
_engine.Initialize();

The InitializationEngine class comes with several different constructors and it’s unfortunately quite important which one you use. This has historical reasons, but it’s something that we hope to be able to fix in a future release.

The one we will use takes three parameters, a module list, a host type and an assembly list. The module list allows the user to provide a list of which initialization modules that should be used and while it may be tempting to try to optimize this list I would strongly recommend passing in null here, which will signal the engine to run all modules it can find.

The next parameter is a HostType enumeration value, which in this case should be TestFramework. Providing this value will disable certain parts of the solution such as Scheduler and Remote events that can give you issues if run inside a test framework.

The final parameter is a list of assemblies that should be considered when scanning for types during the initialization. By using the AssemblyList class here we will get a behaviour that is identical to one used when initialization is started from a web application.

Setting up a test site

The next step is to ensure that a site is defined and that it contains the required content. This may or may not be required in all cases.

The first thing we want to do is to make sure that the correct language branches are enabled on the site. This can be done by going through all branches in the ILanguageBranchRepository and either enable or disable them. Just make sure that you do not disable all languages at some point as this will result in an exception.

private static void EnabledLanguages(ILanguageBranchRepository languageRepository, IEnumerable<string> languages)
{
    if (languages == null || !languages.Any()) return;

    var shouldDisable = languageRepository.ListEnabled()
                                    .Where(l => !languages.Contains(l.LanguageID, StringComparer.OrdinalIgnoreCase));

    foreach (var language in languages.Select(x => CultureInfo.GetCultureInfo(x)))
    {
        languageRepository.Enable(language);
    }

    // Disable language secondly to avoid exception thrown if no languages are enabled.
    foreach (var language in shouldDisable)
    {
        languageRepository.Disable(language.Culture);
    }
}

As a second step, we will create content for the test site. In our example, we will use the import system for creating content from an embedded .episerverdata file and this is probably the easiest way unless your required site is very small.

private static ContentReference ImportEmbeddedPackage(IDataImporter importer, string embeddedResourceName)
{
    // Load content package from embedded resources
    var resources = Assembly.GetExecutingAssembly().GetManifestResourceStream(embeddedResourceName);

    var options = new ImportOptions
    {
        AutoCloseStream = true,
        KeepIdentity = true,
        TransferType = TypeOfTransfer.Importing,
        ValidateDestination = false
    };

    importer.Import(resources, ContentReference.RootPage, options);

    // Root of Imported pages will be our start page
    return importer.Status.ImportedRoot;
}

Finally, we will take the root of the imported content structure and define a Site with this page as the start page. This is done by creating a SiteDefinition object and saving it to the ISiteDefinitionRepository.

private static void DefineSite(ISiteDefinitionRepository siteDefinitionRepository, string siteName, ContentReference startPage, string siteUrl)
{
     var existingSite = siteDefinitionRepository.Get(siteName);
     if (existingSite != null) return;

     // Define our site
     var siteDefinition = new SiteDefinition
     {
            Name = siteName,
            SiteUrl = new Uri($"http://{siteUrl}/", UriKind.Absolute),
            StartPage = startPage
     };
     siteDefinitionRepository.Save(siteDefinition);
}

Running the test

Now everything should be ready for you to run your tests. Simply request the service you want to test from the ServiceLocator and call your method.

[Fact]
public void RootPageChildren_ShouldContainTestPage()
{
     const string pageName = "TestPage";

     var repository = ServiceLocator.Current.GetInstance<IContentRepository>();
     var children = repository.GetChildren<IContent>(ContentReference.RootPage);

     Assert.Contains(children, x => x.Name == pageName);
}

Cleaning up

Once the test run has completed we simply have to call Uninitialize on our Initialization engine instance and drop our temporary database to clean up after ourselves.

public void Dispose()
{
     // Take down the CMS instance
     if (_engine.InitializationState == InitializationState.Initialized)
     {
          _engine.Uninitialize();
     }
     // Remove the temporary database
     _database.Dispose();
}

Wrap up

It doesn’t have to be hard to set up an Episerver CMS instance for you to run integration tests against. And while we have not covered the rendering layer in our test it can provide us with a simple and reasonably fast and robust set of tests to validate how our business services work together with the rest of the service stack.

Advertisements

5 Responses to “Integration testing with Episerver CMS 11”


  1. Im so going to steal this! :)
    Nice work!

  2. Steve Celius Says:

    Could we use this approach to run Episerver in a Windows Service for integration purposes? Especially interesting for Commerce sites, as we’d have pricing and inventory APIs easily available.


    • self-hosted Episerver? :) btw, nice that you are making progress in that direction


    • It’s definitely within reach now. There are some limitations in this setup when the AspNet packages aren’t in place which is why we wanted to start off with pushing the integration test use case first. The obvious areas are of course all the functionality around rendering, but there are also other perhaps not as obvious areas that are dependent on the web stack for full functionality. My best suggestion at this point is to try it out for your use case.


Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s