Create unit tests

When creating automated tests, use unit tests whenever possible. Unit tests run without external resources such as a database. They execute faster than other types of automated tests and are the easiest to integrate into your development process.

A common problem with unit tests is that code needs to connect to a database, either directly or indirectly through external dependencies. Unit tests where this problem occurs throw the following exception upon execution:

“System.InvalidOperationException: The ConnectionString property has not been initialized”

In many cases, you can solve the problem by preparing faked data for your unit tests.

If you cannot fake the data required for a test, create an integration or isolated integration test instead. Integration tests can work with a database, but have significantly slower execution. See:

Fake Info and Provider objects

The Xperience API uses Info and Provider classes to manage data stored in database tables. See Database table API for more information.

To create fake data for your unit tests, call the Fake method from the UnitTests base class (must be inherited by all unit test classes using the CMS.Tests library). The method prepares a faked instance of the provider with in-memory data. You can then use the provider’s methods to get, create, or update data in your unit tests.

Code that works with Info objects typically receives the corresponding provider as IInfoProvider<TInfo> through dependency injection. Register the faked provider using Service.Use to make it available to such code.

Faked providers do not allow you to use methods that delete data. If you need to test methods that delete data, use isolated integration tests.

C#
Example

                     using CMS.Core;
                     
                     using CMS.DataEngine;
                     
                     using CMS.Membership;
                     
                     using CMS.Tests;
                     
                     using NUnit.Framework;
                     
                     [TestFixture]
                     public class MyUnitTests : UnitTests
                     {
                         protected override void RegisterTestServices()
                         {
                             // Always call the base implementation to keep the default test service registrations
                             base.RegisterTestServices();
                     
                             // Prepares faked data for the member provider
                             // The tested code depends on IInfoProvider<MemberInfo>, so the generic provider is faked
                             IInfoProviderFake<MemberInfo, IInfoProvider<MemberInfo>> memberFake = Fake<MemberInfo, IInfoProvider<MemberInfo>>().WithData(
                                 new MemberInfo
                                 {
                                     MemberID = 1,
                                     MemberName = "FakeMember"
                                 });
                     
                             // Registers the fake so that it is injected into code that depends on IInfoProvider<MemberInfo>
                             Service.Use<IInfoProvider<MemberInfo>>(memberFake.ProviderObject);
                         }
                     
                         [Test]
                         public void MyTest()
                         {
                             IInfoProvider<MemberInfo> memberProvider = Service.Resolve<IInfoProvider<MemberInfo>>();
                     
                             // Returns the faked data instead of accessing the database
                             MemberInfo? member = memberProvider.Get(1);
                     
                             Assert.That(member?.MemberName, Is.EqualTo("FakeMember"));
                         }
                     }

In the example, the IInfoProvider<MemberInfo> provider is faked with specific data. Code that resolves IInfoProvider<MemberInfo> then returns the faked data instead of accessing the database. The code behaves as if there were a single member named FakeMember in the database.

The second type parameter of the Fake method must match the provider type that the tested code depends on:

  • For code that uses the generic provider, pass IInfoProvider<TInfo>, for example Fake<MemberInfo, IInfoProvider<MemberInfo>>().
  • For code that uses a dedicated provider class, pass the provider class, for example Fake<MemberInfo, MemberInfoProvider>().

You do not need to clean up the faked data before or after running tests. Tests inherited from the UnitTests base class automatically reset all faked data upon initialization and cleanup.

Every WithData call resets the faked data and replaces it with the provided items. To add items to a provider that is already faked, for example in an individual test that needs extra data, call IncludeData on the fake instead.

Fake Info metadata only

Calling the constructor of an Info class requires access to metadata stored in the database. If your unit tests do not require faked Providers with data prepared in advance, but directly create new instances of Info classes, you can fake only the Info metadata.

To prepare faked metadata for an Info class, call the Fake method with a single generic type parameter matching the given Info class.

C#
Faking metadata only

                     using CMS.Membership;
                     
                     using CMS.Tests;
                     
                     using NUnit.Framework;
                     
                     [Test]
                     public void FakeMetadata_AllowsInfoInstantiation()
                     {
                         Fake<MemberInfo>();
                     
                         MemberInfo member = new();
                         member.MemberName = "TestMember";
                     
                         Assert.That(member.MemberName, Is.EqualTo("TestMember"));
                     }

Substitute other services

Faking covers Info objects and their providers. To replace any other service that your code resolves from the Xperience service container, override the RegisterTestServices method and call Service.Use. Always call the base implementation first to keep the default test registrations.

C#
Registering a test double for a service

                     using CMS.Core;
                     
                     using CMS.Tests;
                     
                     using NUnit.Framework;
                     
                     [TestFixture]
                     public class RegisterTestServicesUnitTests : UnitTests
                     {
                         private IMyCustomService myCustomService = null!;
                     
                         protected override void RegisterTestServices()
                         {
                             // Always call the base implementation to keep the default test service registrations
                             base.RegisterTestServices();
                     
                             myCustomService = new DeterministicCustomService();
                             Service.Use<IMyCustomService>(myCustomService);
                         }
                     
                         [Test]
                         public void RegisterTestServices_ResolvesCustomService()
                         {
                             IMyCustomService resolvedService = Service.Resolve<IMyCustomService>();
                     
                             Assert.That(resolvedService, Is.SameAs(myCustomService));
                         }
                     }

If your service only depends on your own interfaces and never calls the Xperience API, you can instantiate it directly in a plain NUnit test class without any CMS.Tests base class. Such tests do not need the Xperience service container at all.

Test code that retrieves content

Code that retrieves content items and website channel pages through the Content item API requires a database. Faking covers Info objects and their providers only, so you cannot fake content items or pages the way you fake other Xperience data.

Choose an approach based on what you need to verify:

Approach

Tests

When to use

Substitute the retrieval services

Your code’s handling of retrieved content

You need to verify how your code processes the results (mapping, filtering, formatting).

Isolated integration tests

The real query, including content type definitions, language fallbacks, and workspace or channel filtering

You need to verify that a query returns the expected content. This is the only approach that covers the query itself.

Substituting the retrieval services replaces content retrieval with prepared data, so it does not verify that your query returns the correct content. Combine it with a small number of isolated integration tests that cover the queries themselves.

Substitute the retrieval services

Content retrieval in Xperience goes through the following services:

  • IContentRetriever – the recommended service for retrieving content in application code. See Content retriever API.
  • IContentQueryExecutor – the lower-level service used to execute content item queries.

Register a test double for the service that your code uses in the RegisterTestServices override, and return prepared content from it.

With this approach, the tested code still builds the real query and calls the Xperience API. The query never reaches the database, so the tests verify only what your code does with the returned content.

Isolated integration tests

Content queries are one of the cases that require a real database. See Create isolated integration tests.

Test the service container itself

For tests that only need the service container (registration and resolution) without the faking infrastructure, inherit from IsolatedContainerUnitTests. This class provides a lightweight alternative to UnitTests:

  • Manages the container lifecycle (reset, register, build, dispose) per test fixture
  • Does not reset application state between individual tests
  • Does not provide Fake<>() methods
  • Executes faster than UnitTests
C#
Using IsolatedContainerUnitTests

                     using CMS.Core;
                     
                     using CMS.Tests;
                     
                     using NUnit.Framework;
                     
                     [TestFixture]
                     public class IsolatedContainerUnitTestsExample : IsolatedContainerUnitTests
                     {
                         protected override void RegisterTestServices()
                         {
                             // Registers services required by tests in this fixture
                             Service.Use<IMyCustomService, DeterministicCustomService>();
                         }
                     
                         [Test]
                         public void IsolatedContainer_ResolvesRegisteredService()
                         {
                             IMyCustomService service = Service.Resolve<IMyCustomService>();
                     
                             Assert.That(service.GetData(), Is.EqualTo("test-data"));
                         }
                     }

Test fixtures that call Service.InitializeContainer() without inheriting from one of the CMS.Tests base classes produce a compile-time warning, because container registrations can leak between fixtures.