1. Goals and Requirements
The Surveys application
uses Windows Azure table and BLOB storage, and the developers at
Tailspin were concerned about how this would affect their unit testing
strategy. From a testing perspective, a unit test should focus on the
behavior of a specific class and not on the interaction of that class
with other components in the application. From the perspective of
Windows Azure, any test that depends on Windows
Azure storage requires complex setup and tear-down logic to make sure
that the correct data is available for the test to run. For both of
these reasons, the developers at Tailspin designed the data access
functionality in the Surveys application with testability in mind, and
specifically to make it possible to run unit tests on their data store classes without a dependency on Windows Azure storage.
2. The Solution
The solution adopted by the
developers at Tailspin was to wrap the Windows Azure storage components
in such a way as to facilitate replacing them with mock objects during
unit tests and to use the Unity Application Block (Unity). A unit test
should be able to instantiate a suitable mock storage component, use it
for the duration of the test, and then discard it. Any integration tests
can continue to use the original data access components to test the
functionality of the application.
Note:
The Surveys application uses Unity to decouple its components and facilitate testing.
Note:
Unity
is a lightweight, extensible dependency injection container that
supports interception, constructor injection, property injection, and
method call injection. You can use Unity in a variety of ways to help
decouple the components of your applications, to maximize coherence in
components, and to simplify design, implementation, testing, and
administration of these applications.
You can learn more about Unity and download the application block at http://unity.codeplex.com/.
3. Inside the Implementation
Now is a good time to walk
through some code that illustrates testing the store classes in more
detail. As you go through this section, you may want to download the
Microsoft® Visual Studio® development system solution for the Tailspin
Surveys application from http://wag.codeplex.com/.
This section describes how the design of the Surveys application supports unit testing of the SurveyStore
class that provides access to the table storage. This description
focuses on one specific set of tests, but the application uses the same
approach with other store classes.
The following code example shows the IAzureTable interface and the AzureTable class that are at the heart of the implementation.
public interface IAzureTable<T> where T : TableServiceEntity
{
IQueryable<T> Query { get; }
void EnsureExist();
void Add(T obj);
void Add(IEnumerable<T> objs);
void AddOrUpdate(T obj);
void AddOrUpdate(IEnumerable<T> objs);
void Delete(T obj);
void Delete(IEnumerable<T> objs);
}
public class AzureTable<T>
: IAzureTable<T> where T : TableServiceEntity
{
private readonly string tableName;
private readonly CloudStorageAccount account;
...
public IQueryable<T> Query
{
get
{
TableServiceContext context = this.CreateContext();
return context.CreateQuery<T>(this.tableName)
.AsTableServiceQuery();
}
}
public void Add(T obj)
{
this.Add(new[] { obj });
}
public void Add(IEnumerable<T> objs)
{
TableServiceContext context = this.CreateContext();
foreach (var obj in objs)
{
context.AddObject(this.tableName, obj);
}
var saveChangesOptions = SaveChangesOptions.None;
if (objs.Distinct(new PartitionKeyComparer())
.Count() == 1)
{
saveChangesOptions = SaveChangesOptions.Batch;
}
context.SaveChanges(saveChangesOptions);
}
...
private TableServiceContext CreateContext()
{
return new TableServiceContext(
this.account.TableEndpoint.ToString(),
this.account.Credentials);
}
private class PartitionKeyComparer :
IEqualityComparer<TableServiceEntity>
{
public bool Equals(TableServiceEntity x,
TableServiceEntity y)
{
return string.Compare(x.PartitionKey, y.PartitionKey,
true,
System.Globalization.CultureInfo
.InvariantCulture) == 0;
}
public int GetHashCode(TableServiceEntity obj)
{
return obj.PartitionKey.GetHashCode();
}
}
}
The generic interface and class have a type parameter T that derives from the Windows Azure TableServiceEntity type that you use to create your own table types. For example, in the Surveys application, the SurveyRow and QuestionRow types derive from the TableServiceEntity class. The interface defines several operations: the Query method returns an IQueryable collection of the type T, and the Add, AddOrUpdate, and Delete methods each take a parameter of type T. In the AzureTable class, the Query method returns a TableServiceQuery object, the Add and AddOrUpdate methods save the object to table storage, and the Delete method deletes the object from table storage. To create a mock object for unit testing, you must instantiate an object of type IAzureTable.
The following code example from the SurveyStore class shows the constructor.
public SurveyStore(IAzureTable<SurveyRow> surveyTable,
IAzureTable<QuestionRow> questionTable)
{
this.surveyTable = surveyTable;
this.questionTable = questionTable;
}
The constructor takes parameters of type IAzureTable that enable you to pass in either real or mock objects that implement the interface.
This parameterized
constructor is invoked in two different scenarios. The Surveys
application invokes the constructor indirectly when the application uses
the SurveysController
MVC class. The application uses the Unity dependency injection
framework to instantiate MVC controllers. The Surveys application
replaces the standard MVC controller factory with the UnityControllerFactory class in the OnStart
method in both web roles, so when the application requires a new MVC
controller instance, Unity is responsible for instantiating that
controller. The following code example shows part of the ContainerBootstrapper class from the TailSpin.Web project that the Unity container uses to determine how to instantiate objects.
public static class ContainerBootstraper
{
public static void RegisterTypes(IUnityContainer container)
{
var account = CloudConfiguration
.GetStorageAccount("DataConnectionString");
container.RegisterInstance(account);
container.RegisterType<ISurveyStore, SurveyStore>();
container.RegisterType<IAzureTable<SurveyRow>,
AzureTable<SurveyRow>>(
new InjectionConstructor(typeof
(Microsoft.WindowsAzure.CloudStorageAccount),
AzureConstants.Tables.Surveys));
container.RegisterType<IAzureTable<QuestionRow>,
AzureTable<QuestionRow>>(
new InjectionConstructor(typeof
(Microsoft.WindowsAzure.CloudStorageAccount),
AzureConstants.Tables.Questions));
...
}
}
The last two calls to the RegisterType method define the rules that tell the Unity container how to instantiate the AzureTable instances that it must pass to the SurveyStore constructor.
When the application requires a new MVC controller instance, Unity is responsible for creating the controller, and in the case of the SurveysController class, Unity instantiates a SurveyStore object using the parameterized constructor shown earlier, and passes the SurveyStore object to the SurveysController constructor.
In the second usage scenario for the parameterized SurveyStore constructor, you create unit tests for the SurveyStore
class by directly invoking the constructor and passing in mock objects.
The following code example shows a unit test method that uses the
constructor in this way.
[TestMethod]
public void GetSurveyByTenantAndSlugNameReturnsTenantNameFrom
PartitionKey()
{
string expectedRowKey = string.Format(
CultureInfo.InvariantCulture, "{0}_{1}", "tenant",
"slug-name");
var surveyRow = new SurveyRow { RowKey = expectedRowKey,
PartitionKey = "tenant" };
var surveyRowsForTheQuery = new[] { surveyRow };
var mock = new Mock<IAzureTable<SurveyRow>>();
mock.SetupGet(t => t.Query)
.Returns(surveyRowsForTheQuery.AsQueryable());
var store = new SurveyStore(mock.Object,
default(IAzureTable<QuestionRow>));
var survey = store.GetSurveyByTenantAndSlugName("tenant",
"slug-name", false);
Assert.AreEqual("tenant", survey.Tenant);
}
The test creates a mock IAzureTable<SurveyRow> instance, uses it to instantiate a SurveyStore object, invokes the GetSurveyByTenantAndSlugName method, and checks the result. It performs this test without touching Windows Azure table storage.
The Surveys application uses a similar approach to enable unit testing of the other store components that use Windows Azure BLOB and table storage.