Skip to main content

Unit Testing Asp.net MVC and the Cache

If you haven’t worked with Stephen Walthers MVC Tip #12, you’re missing out on a great way to test your MVC controllers. He provides fakes for the ControllerContext, HttpContext, HttpRequest, SessionState and also mechanisms for testing authorization. You can download his code at the end of the post.

I created a new project called MvcFakesForTesting and copied all of the classes. I did this so I can extend and grow the project. Stephen wrote his library with pre-release code, so I needed to change a couple of IController references to ControllerBase. I also created added Headers so that I can fake the request headers. Just implement this the same as QueryString and FormParams. Now I have the DLL that I will use on all of our projects for testing. I made sure to include a reference to Stephen so that credit is given where credit is due.

One thing that is not included is a fake for the Cache. Of course, I ran into this on my first test! First of all, the Cache object is sealed / notinheritable and doesn’t have an interface so mocking or faking it is not going to work. We’re going to need a wrapper.

Step one is to create an interface so I can create use dependency injection for testing. ICacheWrapper is simply all of the public methods from Cache.

public interface ICacheWrapper
{
int Count { get; }

object this[string key] { get; set; }

object Add(string key,
object value,
CacheDependency dependencies,
DateTime absoluteExpiration,
TimeSpan slidingExpiration,
CacheItemPriority priority,
CacheItemRemovedCallback onRemoveCallback);

object Get(string key);

void Insert(string key, object value);

void Insert(string key, object value, CacheDependency dependencies);

void Insert(string key, object value, CacheDependency dependencies, DateTime absoluteExpiration, TimeSpan slidingExpiration);

void Insert(string key, object value, CacheDependency dependencies, DateTime absoluteExpiration, TimeSpan slidingExpiration, CacheItemUpdateCallback onUpdateCallback);

void Insert(string key, object value, CacheDependency dependencies, DateTime absoluteExpiration, TimeSpan slidingExpiration, CacheItemPriority priority, CacheItemRemovedCallback onRemoveCallback);

object Remove(string key);
}

Now I can create  CacheWrapper that implements ICacheWrapper and defers the call to the actual cache.

public class CacheWrapper : ICacheWrapper
{
private Cache cache;

public CacheWrapper()
{
this.cache = HttpContext.Current.Cache;
}

public CacheWrapper(Cache cache)
{
this.cache = cache;
}

#region ICacheWrapper Members

public int Count
{
get { return this.cache.Count; }
}

public object this[string key]
{
get
{
return this.cache[key];
}
set
{
this.cache.Insert(key, value);
}
}

public object Add(string key, object value, CacheDependency dependencies, DateTime absoluteExpiration, TimeSpan slidingExpiration, CacheItemPriority priority, CacheItemRemovedCallback onRemoveCallback)
{
return this.cache.Add(key, value, dependencies, absoluteExpiration, slidingExpiration, priority, onRemoveCallback);
}

public object Get(string key)
{
return this.cache[key];
}

public void Insert(string key, object value)
{
this.cache.Insert(key, value);
}

public void Insert(string key, object value, CacheDependency dependencies)
{
this.cache.Insert(key, value, dependencies);
}

public void Insert(string key, object value, CacheDependency dependencies, DateTime absoluteExpiration, TimeSpan slidingExpiration)
{
this.cache.Insert(key, value, dependencies, absoluteExpiration, slidingExpiration);
}

public void Insert(string key, object value, CacheDependency dependencies, DateTime absoluteExpiration, TimeSpan slidingExpiration, CacheItemUpdateCallback onUpdateCallback)
{
this.cache.Insert(key, value, dependencies, absoluteExpiration, slidingExpiration, onUpdateCallback);
}

public void Insert(string key, object value, CacheDependency dependencies, DateTime absoluteExpiration, TimeSpan slidingExpiration, CacheItemPriority priority, CacheItemRemovedCallback onRemoveCallback)
{
this.cache.Insert(key, value, dependencies, absoluteExpiration, slidingExpiration, priority, onRemoveCallback);
}

public object Remove(string key)
{
return this.cache.Remove(key);
}

#endregion
}

Now, anywhere I want to access the cache, I create a link to ICacheWrapper and provide a mechanism for injecting the wrapper. This makes the code the same whether I’m testing for production.

public class CustomersProxy
{
private ICacheWrapper cache;
private ICustomersRepository repository;

public CustomersProxy(ICustomersRepository repository, ICacheWrapper cache)
{
this.repository = repository;
this.cache = cache;
}

public List<Customer> ListCustomers()
{
List<Customer> customers = (List<Customer>)this.cache[CustomersCacheKey];
if (customers == null)
{
customers = repository.FindAll();
this.cache.Add(
CustomersCacheKey,
customers ,
null,
Cache.NoAbsoluteExpiration,
new TimeSpan(1, 0, 0),
CacheItemPriority.AboveNormal,
null);
}
return customers;
}
}

FakeCacheWrapper uses a Dictionary to act as the cache. It implements ICacheWrapper so we can inject it for testing anywhere we need to call the cache.

public class FakeCacheWrapper : ICacheWrapper
{
private readonly Dictionary<string, object> cacheItems;

public static readonly DateTime NoAbsoluteExpiration;

public static readonly TimeSpan NoSlidingExpiration;

public FakeCacheWrapper()
{
this.cacheItems = new Dictionary<string, object>();
}

public int Count { get { return this.cacheItems.Count; } }

public object this[string key]
{
get
{
return Get(key);
}
set
{
cacheItems.Add(key, value);
}
}

public object Add(string key,
object value,
CacheDependency dependencies,
DateTime absoluteExpiration,
TimeSpan slidingExpiration,
CacheItemPriority priority,
CacheItemRemovedCallback onRemoveCallback)
{
this.cacheItems.Add(key, value);
return value;
}

public object Get(string key)
{
if (this.cacheItems.ContainsKey(key))
{ return this.cacheItems[key]; }
return null;
}

public IDictionaryEnumerator GetEnumerator()
{ return this.cacheItems.GetEnumerator(); }

public void Insert(string key, object value)
{
this.cacheItems.Add(key, value);
}

public void Insert(string key, object value, CacheDependency dependencies)
{
this.cacheItems.Add(key, value);
}

public void Insert(string key, object value, CacheDependency dependencies, DateTime absoluteExpiration, TimeSpan slidingExpiration)
{
this.cacheItems.Add(key, value);
}

public void Insert(string key, object value, CacheDependency dependencies, DateTime absoluteExpiration, TimeSpan slidingExpiration, CacheItemUpdateCallback onUpdateCallback)
{
this.cacheItems.Add(key, value);
}

public void Insert(string key, object value, CacheDependency dependencies, DateTime absoluteExpiration, TimeSpan slidingExpiration, CacheItemPriority priority, CacheItemRemovedCallback onRemoveCallback)
{
this.cacheItems.Add(key, value);
}

public object Remove(string key)
{
object obj = null;
if (this.cacheItems.ContainsKey(key))
{
obj = this.cacheItems[key];
}
this.cacheItems.Remove(key);
return obj;
}
}
Now my controller has an two constructors, one for production and one for testing.
public class CustomersController : Controller
{
private CustomersProxy proxy;

public CustomersController()
{
CacheWrapper wrapper = new CacheWrapper(HttpContext.Current.Cache);
CustomersRepository repository = new CustomersRepository();
this.proxy = new CustomersProxy(repository, wrapper);
}

public CustomersController(CustomersProxy proxy)
{
this.proxy = proxy;
}

.....
}

With Stephen’s fakes and ICacheWrapper/FakeCacheWrapper, I can now test just about everything I need to.

Comments

Tony Chevis said…
Thanks for posting this and saving us some time!
Glad you found it helpful!!
Anonymous said…
Thx alot for your code! It put me on the right track again.
Stephen Cawood said…
Hey, the download link on http://stephenwalther.com/archive/2008/07/01/asp-net-mvc-tip-12-faking-the-controller-context.aspx is broken. Can you share a download?
Anonymous said…
Thanks for putting this together. It is exactly what I need. Too bad the MVC framework doesn't already have something like this in place. -John

Popular posts from this blog

Migrating Legacy Apps to the New SimpleMembership Provider

Asp.Net MVC4 uses the new SimpleMembership provider, changes the table structure and adds a new hashing algorithm. The reasons for the changes can be found in this article by Jon Galloway. This article shows how to migrate your existing apps to the new provider.I’m assuming that you stored your passwords in the unrecoverable SHA-1 format. If you didn’t, then you’ll have to change a couple of things. All of my apps are done this way so… I’m also assuming that you have created the basic skeleton of the new app and ran it once so the correct tables will be created.First, we’ll look at the new tables. Previously, we had all of those aspnet_xxxxxx tables. Here’s the new ones.UserProfileContains all of the elements relevant to the user. This is a combination of the aspnet_Users table and the aspnet_Profiles table.webpages_MembershipStores the password info when not using OAuth, Live, Facebook, etc. This table is somewhat of a match to the aspnet_Membership table.webpages_OAuthMembershipStor…

JavaScript function to automatically add slashes to date

In converting an old Windows app to a browser app, the user wanted to be able to enter dates without the slashes. Here's a simple jscript: 1:// Function to convert short date string (MMddyy) 2:// or (MMddyyyy) to a date string (mm/dd/yyyy). 3:// txtBox is the actual textbox control 4:// with the value to be processed. 5:function FixShortDate(txtBox) { 6:if (txtBox == null) { 7:return'' } 8: 9:var re = new RegExp(/(\d{6})(\d{2})?/); 10: 11:if (re.test(txtBox.value)) 12: { 13:if (txtBox.value.length == 8) { 14: txtBox.value = txtBox.value.substring(0, 2) + '/' + txtBox.value.substring(2, 4) + '/' + txtBox.value.substring(4, 8) 15: } 16: 17:if (txtBox.value.length == 6) { 18:if (txtBox.value.substring(4, 6) < 20)

Get Asp.Net Profile properties from Sql

Ever wanted to include the profile information from an Asp.Net profile in a query? It’s not that hard once you understand the structure. I’ve written a little function that does all the work. Note: I’m using Sql Server as my repository.

First we need to understand how the profile data is stored. Looking at the aspnet_Profile table, we can see that it stores the information in two columns: PropertyNames and PropertyValuesString.

Looking at PropertyNames we can see that it has a basic structure of Property Name, Data Type, Starting Position and Length. For example, in the string “FirstName:S:0:4:Phone:S:4:10:LastName:S:14:5:” we can see that FirstName is of type string, starts at position 0 and has a length of 4. Notice the zero base for the starting position, we need to correct for that in our function. This means in the PropertyValuesString “John2175551212Smith”, we would start with the first position and proceed 4 characters to get the name.

You might be thinking …