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.
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 exampleFake<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.
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.
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 |
|
Your code’s handling of retrieved content |
You need to verify how your code processes the results (mapping, filtering, formatting). |
|
|
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
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.