Mark Stott
May 26, 2022
  2218
(1 votes)

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

Background

If you've never heard of it, the Dynamic Data Store (DDS) which is a component that offers an API and infrastructure for CRUD operations of custom data objects.  It allows developers to access and store data without needing to provision custom tables or databases using either classes or property bags which are stored within shared tables contained in the CMS database.

Optimizely also use the same functionality for some of their own features.  For example, Optimizely Search & Navigation comes with a feature known as Best Bets which allows content editors to provide search suggestions for given search terms.  Each Best Bet has it's own entry inside of the Dynamic Data Store.

This article is the first of two articles showing developers how they can unit test with the Dynamic Data Store, each with it's own separate technical approach.  Both approaches are valid, but the choice in approach may come down to how many different operations you are under taking with the Dynamic Data Store.  Part 1 deals with the direct mocking of the Dynamic Data Store objects while Part 2 deals with abstraction of the Dynamic Data Store behind multiple interfaces.

The Repository Under Test

Lets assume we have a custom data object that we want to store in the Dynamic Data Store as follows:

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

The object MyCustomDataObject has an Identity property in order to meet the IDynamicData interface which is the minimum requirement for storage within the Dynamic Data Store.  The UniqueText property should be unique to that instance of the MyCustomDataObject, the SomeOtherText is just additional information to be stored with that record.

A repository that handles the saving of MyCustomDataObject records may look something like this:

public class MyCustomDataObjectRepository
{
   private readonly DynamicDataStore _dataStore;

   public MyCustomDataObjectRepository(DynamicDataStoreFactory dataStoreFactory)
   {
       _dataStore = dataStoreFactory.CreateStore(typeof(MyCustomDataObject));
   }

   public void Save(Guid id, string uniqueText, string someOtherText)
   {
       var matchingRecord = _dataStore.Find<MyCustomDataObject>(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() : _dataStore.Load<MyCustomDataObject>(Identity.NewIdentity(id));
       recordToSave.UniqueText = uniqueText;
       recordToSave.SomeOtherText = someOtherText;
       _dataStore.Save(recordToSave);
   }

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

Unit Testing

In order to unit test the repository behaviour, we need to mock the Dynamic Data Store.  Optimizely does not provide any interfaces to assist with the writing of unit tests and we have to mock implementations instead. 

The mocking of implementations can come with it's own complications.  With a mock behaviour of 'strict', the mocked object will behave like the true implementation and will throw exceptions as expected.  With a mock behaviour of 'loose', no exceptions will be thrown and default values will be returned as necessary.  The default mock behavior of any mocked object is actually the strict behaviour.

The strict mock behavior makes unit testing of the Dynamic Data Store impossible with exceptions being thrown during the set up of the test.  So in the following setup method, I have mocked the StoreDefinition, the DynamicDataStore and the DynamicDataStoreFactory with a loose mock behavior.  This is done by supplying the behavior in the mock constructor. The additional parameters applied to the mock constructors are default types which fulfil the constructor of the implementation being mocked.

[TestFixture]
public class MyCustomDataObjectRepositoryTests
{
   private Mock<DynamicDataStoreFactory> _mockDynamicDataStoreFactory;

    private Mock<DynamicDataStore> _mockDynamicDataStore;

    private Mock<StoreDefinition> _mockStoreDefinition;

    private MyCustomDataObjectRepository _repository;

   [SetUp]
   public void SetUp()
   {
       _mockStoreDefinition = new Mock<StoreDefinition>(
           MockBehavior.Loose,
           string.Empty,
           new List<PropertyMap>(0),
           null);

        _mockDynamicDataStore = new Mock<DynamicDataStore>(
           MockBehavior.Loose,
           _mockStoreDefinition.Object);

       _mockDynamicDataStoreFactory = new Mock<DynamicDataStoreFactory>();
       _mockDynamicDataStoreFactory.Setup(x => x.CreateStore(typeof(MyCustomDataObject)))
                                   .Returns(_mockDynamicDataStore.Object);

        _repository = new MyCustomDataObjectRepository(_mockDynamicDataStoreFactory.Object);
   }
}

At this point we are now free to write unit tests as easily as we would do when mocking interfaces.  In the following test example, an existing record is created and is set to be the result of the mocked Find method against the DynamicDataStore. When we try to save a record with a matching uniqueText, the desired outcome is that an EntityExistsException is thrown and our assertion is constructed to prove that.

[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"
   };

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

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

Summary

In order to unit test against the Dynamic Data Store, you must do the following in order to write unit tests as normal:

  • Mock the StoreDefinition with MockBehavior.Loose.
  • Mock the DynamicDataStore with MockBehavior.Loose and pass in the mock of the StoreDefinition.
  • Mock the DynamicDataStoreFactory with either mock behavior and set up the CreateStore method to return the mocked DynamicDataStore.

To be continued in Part 2...

May 26, 2022

Comments

Please login to comment.
Latest blogs
Creating an Optimizely CMS Addon - Adding an Editor Interface Gadget

In   Part One   of this series, I covered getting started with creating your own AddOn for Optimizely CMS 12. This covered what I consider to be an...

Mark Stott | Aug 30, 2024

Configure your own Search & Navigation timeouts

The main blog Configure your own Search & Navigation timeouts was posted for years but you need copy the code to your application. We now bring tho...

Manh Nguyen | Aug 30, 2024

Joining the Optimizely MVP Program

Andy Blyth has been honoured as an Optimizely MVP, recognising his contributions to the Optimizely community. Learn how this achievement will enhan...

Andy Blyth | Aug 29, 2024 | Syndicated blog

Welcome 2024 Summer OMVPs

Hello, Optimizely community! We are thrilled to announce and welcome the newest members to the Optimizely Most Valuable Professionals (OMVP) progra...

Patrick Lam | Aug 29, 2024

Create custom folder type in Edit Mode

Content Folders, which are located in assets pane gadget or multichannel content gadget, allow you to add any type of block or folders. But...

Grzegorz Wiecheć | Aug 28, 2024 | Syndicated blog

Creating an Optimizely AddOn - Getting Started

When Optimizely CMS 12 was launched in the summer of 2021, I created my first AddOn for Optimizely CMS and talked about some of the lessons I learn...

Mark Stott | Aug 28, 2024