Published on:May 26, 2022
Views: 279
Number of votes: 1
Average rating:

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

Please login to comment.