Take the community feedback survey now.

Mark Stott
May 26, 2022
  67
(0 votes)

Unit Testing with Dynamic Data Store in CMS 12 - Part 2

Background

In the previous article of this series, Unit Testing Optimizely CMS 12 Dynamic Data Store - Part 1, an introduction was given to what the Dynamic Data Store is, and an approach was given for unit testing by mocking the DynamicDataStore object.

In this article we are covering a different approach where we abstract away the usage of the Dynamic Data Store behind an interface with minimal logic.

Approach

Developers who are familiar with Entity Framework (EF) or Unit of Work (UOW) pattern may see some familiarity with this approach, and EF was indeed my inspiration here.

For this example, I am using a custom data object which implements the IDynamicData interface required for use with Dynamic Data Store:

public class MyCustomDataObject : IDynamicData
{
   public Identity Id { get; set; }

    public string UniqueText { get; set; }

    public string SomeOtherText { get; set; }
}

Next we add an interface for data context, and since this will potentially expose multiple data collections, we also add an interface for operations on a data collection:

public interface IDdsContext
{
   IDdsEntityCollection<MyCustomDataObject> MyCustomDataObjects { get; }
}

public interface IDdsEntityCollection<TModel>
   where TModel : IDynamicData
{
   IOrderedQueryable<TModel> Items();

    IEnumerable<TModel> AllItems();

    TModel Get(Identity identity);

    IEnumerable<TModel> Find(string propertyName, object propertyValue);

    Identity Save(TModel entity);
}

Next we need to add a implementation of the IDdsEntityCollection<TModel> to facilitate data operations against a singular data type.  As you will see below, each of the implemented methods is as light weight as possible.  If you are using additional methods within the Dynamic Data Store, then you will need to extend this interface and implementation.

public class DdsEtityCollection<TModel> : IDdsEntityCollection<TModel>
   where TModel : IDynamicData
{
   private DynamicDataStore _dynamicDataStore;

   public DdsEtityCollection(DynamicDataStoreFactory dynamicDataStoreFactory)
   {
       _dynamicDataStore = dynamicDataStoreFactory.CreateStore(typeof(TModel));
   }

   public IOrderedQueryable<TModel> Items()
   {
       return _dynamicDataStore.Items<TModel>();
   }

   public IEnumerable<TModel> AllItems()
   {
       return _dynamicDataStore.LoadAll<TModel>() ?? Enumerable.Empty<TModel>();
   }

   public TModel Get(Identity identity)
   {
       return _dynamicDataStore.Load<TModel>(identity);
   }

   public IEnumerable<TModel> Find(string propertyName, object propertyValue)
   {
       return _dynamicDataStore.Find<TModel>(propertyName, propertyValue);
   }

   public Identity Save(TModel entity)
   {
       return _dynamicDataStore.Save(entity);
   }
}

Now we have an implementation of IDdsEntityCollection<TModel>, we can now implement IDdsContext. In this example, the implementation of IDdsEntityCollection<TModel> is not instantiated until it is actually required by the consuming code.

public class DdsContext : IDdsContext
{
   private DynamicDataStoreFactory _dataStoreFactory;

   public DdsContext(DynamicDataStoreFactory dataStoreFactory)
   {
       _dataStoreFactory = dataStoreFactory;
   }

   private IDdsEntityCollection<MyCustomDataObject> _myCustomDataObjects = null;

   public IDdsEntityCollection<MyCustomDataObject> MyCustomDataObjects
   {
       get
       {
           if (_myCustomDataObjects == null)
           {
               _myCustomDataObjects = new DdsEtityCollection<MyCustomDataObject>(_dataStoreFactory);
           }

           return _myCustomDataObjects;
       }
   }
}

Now we can update the repository we created in Part 1 of this series, stripping out the Dynamic Data Store objects and injecting the IDdsContext instead.

public class MyCustomDataObjectRepository
{
   private readonly IDdsContext _context;

   public MyCustomDataObjectRepository(IDdsContext context)
   {
       _context = context;
   }

   public void Save(Guid id, string uniqueText, string someOtherText)
   {
       var matchingRecord = _context.MyCustomDataObjects.Find(nameof(MyCustomDataObject.UniqueText), uniqueText).FirstOrDefault();

       if (matchingRecord != null && !matchingRecord.Id.ExternalId.Equals(id))
       {
           throw new EntityExistsException($"An entry already exists for the unique value of '{uniqueText}'.");
       }

       var recordToSave = Guid.Empty.Equals(id) ? CreateNewRecord() : _context.MyCustomDataObjects.Get(Identity.NewIdentity(id));
       recordToSave.UniqueText = uniqueText;
       recordToSave.SomeOtherText = someOtherText;
       _context.MyCustomDataObjects.Save(recordToSave);
   }

   private static MyCustomDataObject CreateNewRecord()
   {
       return new MyCustomDataObject { Id = Identity.NewIdentity() };
   }
}

Finally, we can write the same unit tests again, however we are now mocking interfaces inside of our repositories instead of mocking the implementations of the Dynamic Data Store.

[TestFixture]
public class MyCustomDataObjectRepositoryTests
{
   private Mock<IDdsEntityCollection<MyCustomDataObject>> _mockDataCollection;
   private Mock<IDdsContext> _mockContext;
   private MyCustomDataObjectRepository _repository;

   [SetUp]
   public void SetUp()
   {
       _mockDataCollection = new Mock<IDdsEntityCollection<MyCustomDataObject>>();
       _mockContext = new Mock<IDdsContext>();
       _mockContext.Setup(x => x.MyCustomDataObjects).Returns(_mockDataCollection.Object);
       _repository = new MyCustomDataObjectRepository(_mockContext.Object);
   }

   [Test]
   public void GivenUniqueTextExistsAgainstAnotherEntity_ThenAnEntityExistsExceptionShouldBeThrown()
   {
       // Arrange
       var uniqueText = "i-am-unique";
       var someOtherText = "some-other-text";

       var existingRecord = new MyCustomDataObject
       {
           Id = Guid.NewGuid(),
           UniqueText = uniqueText,
           SomeOtherText = "original-other-text"
       };

        _mockDataCollection.Setup(x => x.Find(It.IsAny<string>(), It.IsAny<object>()))
                           .Returns(new List<MyCustomDataObject> { existingRecord });

        // Assert
       Assert.Throws<EntityExistsException>(() => _repository.Save(Guid.Empty, uniqueText, someOtherText));
   }
}

Summary

This approach to unit testing with the Dynamic Data Store did the following.

  • Created a data collection interface and implementation using generics for operations on the DynamicDataStore object for a given type.
  • Created an interface and implementation of a data context which exposed only interfaces for the data collections.
  • Injected the interface data context into repositories instead of the Dynamic Data Store.

Which Approach

When deciding which approach to take, consider just how many operations and data types you will be using with the Dynamic Data Store.  Consider whether all of the scaffolding of the data context approach in this article out weighs your usage of the Dynamic Data Store.  Remember the KISS and DRY principals and I'm sure you'll get the right solution for your project.

Finally, if you are finding you are heavily using the Dynamic Data Store, consider using your own tables using Entity Framework or any other Object Relational Mapper (ORM).  You will have better performance with data tables which are designed to suit your data needs.

May 26, 2022

Comments

Please login to comment.
Latest blogs
A day in the life of an Optimizely OMVP - Opticon London 2025

This installment of a day in the life of an Optimizely OMVP gives an in-depth coverage of my trip down to London to attend Opticon London 2025 held...

Graham Carr | Oct 2, 2025

Optimizely Web Experimentation Using Real-Time Segments: A Step-by-Step Guide

  Introduction Personalization has become de facto standard for any digital channel to improve the user's engagement KPI’s.  Personalization uses...

Ratish | Oct 1, 2025 |

Trigger DXP Warmup Locally to Catch Bugs & Performance Issues Early

Here’s our documentation on warmup in DXP : 🔗 https://docs.developers.optimizely.com/digital-experience-platform/docs/warming-up-sites What I didn...

dada | Sep 29, 2025

Creating Opal Tools for Stott Robots Handler

This summer, the Netcel Development team and I took part in Optimizely’s Opal Hackathon. The challenge from Optimizely was to extend Opal’s abiliti...

Mark Stott | Sep 28, 2025

Integrating Commerce Search v3 (Vertex AI) with Optimizely Configured Commerce

Introduction This blog provides a technical guide for integrating Commerce Search v3, which leverages Google Cloud's Vertex AI Search, into an...

Vaibhav | Sep 27, 2025

A day in the life of an Optimizely MVP - Opti Graph Extensions add-on v1.0.0 released

I am pleased to announce that the official v1.0.0 of the Opti Graph Extensions add-on has now been released and is generally available. Refer to my...

Graham Carr | Sep 25, 2025