次の方法で共有


Using Moq with Windows Server AppFabric Caching

Last week I tried doing TDD with Windows Server AppFabric.  In my previous post I shared with you my conclusions.  To sum it up, doing TDD with AppFabric caching is painful; even more painful than using a database in your unit tests.

Over the weekend I decided to see if I could use Moq to mock my calls to AppFabric.  Because of the way the AppFabric API is written it turned out to be a fair bit of work.   However, I did get it working.

<rant>

How I wish that teams who design APIs for the .NET Framework would consider testability and “mockability” as an attribute of their design.  Over and over again I run into classes without interfaces, internal constructors, etc.  All of this makes it very difficult to work with these APIs in unit testing.  I wonder, has anybody written some API design guidelines to insure testability?  If so, please let me know…

</rant>

Step 1 – Create Interfaces that duplicate the AppFabric Caching interfaces

In order to mock the AppFabric Cache Client API I needed to create interfaces that would stand in for the classes in the API they are

  • IDataCache
  • IDataCacheFactory
  • IDataCacheItem
  • IDataCacheItemVersion
  • IDataCacheTags
  • IDataCacheLockHandle

To create these interfaces I simply copy from the metadata in Visual Studio and create an interface.  Here is the IDataCacheInterface

 public interface IDataCache
{
    object this[string key] { get; set; }
    IDataCacheItemVersion Add(string key, object value);
    IDataCacheItemVersion Add(string key, object value, IEnumerable<IDataCacheTag> tags);
    IDataCacheItemVersion Add(string key, object value, string region);
    IDataCacheItemVersion Add(string key, object value, TimeSpan timeout);
    IDataCacheItemVersion Add(string key, object value, IEnumerable<IDataCacheTag> tags, string region);
    IDataCacheItemVersion Add(string key, object value, TimeSpan timeout, IEnumerable<IDataCacheTag> tags);
    IDataCacheItemVersion Add(string key, object value, TimeSpan timeout, string region);
    IDataCacheItemVersion Add(string key, object value, TimeSpan timeout, IEnumerable<IDataCacheTag> tags, string region);
    void ClearRegion(string region);
    bool CreateRegion(string region);
    object Get(string key);
    object Get(string key, out IDataCacheItemVersion version);
    object Get(string key, string region);
    object Get(string key, out IDataCacheItemVersion version, string region);
    object GetAndLock(string key, TimeSpan timeout, out IDataCacheLockHandle lockHandle);
    object GetAndLock(string key, TimeSpan timeout, out IDataCacheLockHandle lockHandle, bool forceLock);
    object GetAndLock(string key, TimeSpan timeout, out IDataCacheLockHandle lockHandle, string region);
    object GetAndLock(string key, TimeSpan timeout, out IDataCacheLockHandle lockHandle, string region, bool forceLock);
    IDataCacheItem GetCacheItem(string key);
    IDataCacheItem GetCacheItem(string key, string region);
    object GetIfNewer(string key, ref IDataCacheItemVersion version);
    object GetIfNewer(string key, ref IDataCacheItemVersion version, string region);
    IEnumerable<KeyValuePair<string, object>> GetObjectsByAllTags(IEnumerable<IDataCacheTag> tags, string region);
    IEnumerable<KeyValuePair<string, object>> GetObjectsByAnyTag(IEnumerable<IDataCacheTag> tags, string region);
    IEnumerable<KeyValuePair<string, object>> GetObjectsByTag(IDataCacheTag tag, string region);
    IEnumerable<KeyValuePair<string, object>> GetObjectsInRegion(string region);
    string GetSystemRegionName(string key);
    IEnumerable<string> GetSystemRegions();
    IDataCacheItemVersion Put(string key, object value);
    IDataCacheItemVersion Put(string key, object value, IDataCacheItemVersion oldVersion);
    IDataCacheItemVersion Put(string key, object value, IEnumerable<IDataCacheTag> tags);
    IDataCacheItemVersion Put(string key, object value, string region);
    IDataCacheItemVersion Put(string key, object value, TimeSpan timeout);
    IDataCacheItemVersion Put(string key, object value, IDataCacheItemVersion oldVersion, IEnumerable<IDataCacheTag> tags);
    IDataCacheItemVersion Put(string key, object value, IDataCacheItemVersion oldVersion, string region);
    IDataCacheItemVersion Put(string key, object value, IDataCacheItemVersion oldVersion, TimeSpan timeout);
    IDataCacheItemVersion Put(string key, object value, IEnumerable<IDataCacheTag> tags, string region);
    IDataCacheItemVersion Put(string key, object value, TimeSpan timeout, IEnumerable<IDataCacheTag> tags);
    IDataCacheItemVersion Put(string key, object value, TimeSpan timeout, string region);
    IDataCacheItemVersion Put(string key, object value, IDataCacheItemVersion oldVersion, IEnumerable<IDataCacheTag> tags, string region);
    IDataCacheItemVersion Put(string key, object value, IDataCacheItemVersion oldVersion, TimeSpan timeout, IEnumerable<IDataCacheTag> tags);
    IDataCacheItemVersion Put(string key, object value, IDataCacheItemVersion oldVersion, TimeSpan timeout, string region);
    IDataCacheItemVersion Put(string key, object value, TimeSpan timeout, IEnumerable<IDataCacheTag> tags, string region);
    IDataCacheItemVersion Put(string key, object value, IDataCacheItemVersion oldVersion, TimeSpan timeout, IEnumerable<IDataCacheTag> tags, string region);
    IDataCacheItemVersion PutAndUnlock(string key, object value, IDataCacheLockHandle lockHandle);
    IDataCacheItemVersion PutAndUnlock(string key, object value, IDataCacheLockHandle lockHandle, IEnumerable<IDataCacheTag> tags);
    IDataCacheItemVersion PutAndUnlock(string key, object value, IDataCacheLockHandle lockHandle, string region);
    IDataCacheItemVersion PutAndUnlock(string key, object value, IDataCacheLockHandle lockHandle, TimeSpan timeout);
    IDataCacheItemVersion PutAndUnlock(string key, object value, IDataCacheLockHandle lockHandle, IEnumerable<IDataCacheTag> tags, string region);
    IDataCacheItemVersion PutAndUnlock(string key, object value, IDataCacheLockHandle lockHandle, TimeSpan timeout, IEnumerable<IDataCacheTag> tags);
    IDataCacheItemVersion PutAndUnlock(string key, object value, IDataCacheLockHandle lockHandle, TimeSpan timeout, string region);
    IDataCacheItemVersion PutAndUnlock(string key, object value, IDataCacheLockHandle lockHandle, TimeSpan timeout, IEnumerable<IDataCacheTag> tags, string region);
    bool Remove(string key);
    bool Remove(string key, IDataCacheItemVersion version);
    bool Remove(string key, IDataCacheLockHandle lockHandle);
    bool Remove(string key, string region);
    bool Remove(string key, IDataCacheItemVersion version, string region);
    bool Remove(string key, IDataCacheLockHandle lockHandle, string region);
    bool RemoveRegion(string region);
    void ResetObjectTimeout(string key, TimeSpan newTimeout);
    void ResetObjectTimeout(string key, TimeSpan newTimeout, string region);
    void Unlock(string key, IDataCacheLockHandle lockHandle);
    void Unlock(string key, IDataCacheLockHandle lockHandle, string region);
    void Unlock(string key, IDataCacheLockHandle lockHandle, TimeSpan timeout);
    void Unlock(string key, IDataCacheLockHandle lockHandle, TimeSpan timeout, string region);
}

Step 2 – Write Test Code with Moq

Now I can write my unit test using Moq with the help of these interfaces

 [TestMethod()]
public void ShouldAddToCache()
{
    // Arrange - Setup mocks (using MOQ)
    var moqDcf = new Mock<IDataCacheFactory>();
    var moqDc = new Mock<IDataCache>();
    moqDcf.Setup(d => d.GetDefaultCache()).Returns(moqDc.Object);
    moqDc.Setup(dcm => dcm.Get("key")).Returns("value");

    // Get DataCacheFactory and DataCache
    IDataCacheFactory dcf = moqDcf.Object;
    IDataCache dc = dcf.GetDefaultCache();

    // Act
    dc.Add("key", "value");

    // Assert
    Assert.IsNotNull(dc.Get("key"));
}

Summary

So where does this leave us?  I made it work through a great deal of effort but this solution is not sufficient.

  1. I’m going to need a library that hides the real AppFabricCache behind a provider model.  Then I can write my code against that library.

  2. Using Moq like this would require me to record every detail of how I want it to behave.  For small tests like the example above that is fine, but what about when I’m testing real business logic which might have hundreds of calls to the cache?  What about code that reads / writes to the cache?  Setting up the mocks would be very ticky.

  3. The DataCache class includes a number of abstract methods which cannot be in the interface.  These methods deal with the callbacks for cache notifications.  This solution doesn’t include a way to mock callback notifications.

Next Steps

I’ve started working on a cache API that solves these problems.  The API is fully mockable and supports generics i.e. IDataCache<T> so we won’t have to cast things that we get out of the cache.

This API will be implemented by providers and support MEF allowing me to plug in AppFabric Caching or a MemoryCache backed implementation when testing.  When doing TDD you can use the MemoryCache implementation and then for integration testing switch to the AppFabric DataCache. 

The API will be the same and I’ll work towards getting the behavior to be the same as well.  That is… assuming that I have time to do this.  It is a fair bit of work.

Comments

  • Anonymous
    May 03, 2010
    For libraries that don't provide an interfaces for testing , you can use TypeMock.