Using the Fakes Framework to Test TFS API Code (Part 1 of 2)

TFSAPI
44223

(Here’s the link to Part 2)

If you’ve ever written a utility to do TFS “stuff” using the TFS API, you probably tested by hitting F5 and stepping through a bit before letting any large loops do their thing. So what happened to all the goodness is expected of good developers – for example unit testing?

Well, it turns out it’s insanely difficult to test any code that depends on the TFS APIs. You could craft tests that hit an actual TFS server, but then you’d have to worry about clean up so that your tests could be repeated. And of course you’d *never* do this sort of testing against a Live server (or a Live project), right?

Besides, unit tests, at least in theory, are supposed to have no dependencies on external systems. So how do you go about unit testing code that uses the TFS API?

Scenario: Copy Work Items

Let’s imagine you’ve written a console app to copy work items (obtained from executing a stored query) to a target iteration. Here’s the code for the WorkItemCopyer class and Program.cs:

class WorkItemCopyer
{
public WorkItemCollection WorkItems { get; private set; }
public QueryHierarchy QueryHierarchy { get; private set; }

public WorkItemStore Store { get; private set; }
public TfsTeamProjectCollection TPC { get; private set; }
public string TeamProjectName { get; private set; }

public WorkItemCopyer(TfsTeamProjectCollection tpc, string teamProjectName)
{
TPC = tpc;
TeamProjectName = teamProjectName;
Store = TPC.GetService();
QueryHierarchy = Store.Projects[TeamProjectName].QueryHierarchy;
}

public void RunQuery(QueryDefinition queryDef)
{
var dict = new Dictionary()
{
{ "project", TeamProjectName },
{ "me", GetCurrentUserDisplayName() }
};

var query = new Query(Store, queryDef.QueryText, dict);
WorkItems = query.RunQuery();
}

private string GetCurrentUserDisplayName()
{
var securityService = TPC.GetService();
var accountName = string.Format("{0}\\{1}", Environment.UserDomainName, Environment.UserName);
var memberInfo = securityService.ReadIdentity(SearchFactor.AccountName, accountName, QueryMembership.None);
if (memberInfo != null)
{
return memberInfo.DisplayName;
}
return Environment.UserName;
}

public int CopyWorkItems(string targetIterationPath)
{
foreach (WorkItem workItem in WorkItems)
{
var copy = workItem.Copy();
copy.IterationPath = targetIterationPath;
copy.Save();
}
return WorkItems.Count;
}

public QueryDefinition FindQuery(string queryName)
{
return FindQueryInFolder(QueryHierarchy, queryName);
}

private QueryDefinition FindQueryInFolder(QueryFolder folder, string queryName)
{
foreach (var query in folder.OfType())
{
if (query.Name == queryName)
{
return query;
}
}
QueryDefinition subQuery = null;
foreach (var subFolder in folder.OfType())
{
subQuery = FindQueryInFolder(subFolder, queryName);
if (subQuery != null)
{
return subQuery;
}
}
return null;
}
}

class Program
{
static void Main(string[] args)
{
var tpcUrl = args[0];
var teamProjectName = args[1];
var queryName = args[2];
var targetIterationPath = args[3];

var tpc = TfsTeamProjectCollectionFactory.GetTeamProjectCollection(new Uri(tpcUrl));
var copyer = new WorkItemCopyer(tpc, teamProjectName);

var query = copyer.FindQuery(queryName);
copyer.RunQuery(query);
var count = copyer.CopyWorkItems(targetIterationPath);

Console.WriteLine("Successfully copied {0} work items", count);
Console.WriteLine("Press to quit...");
Console.ReadLine();
}
}

Now we get to the interesting part: unit testing. Let’s start off assuming we have a test project that we can run the tests against. A CopyTest would look something like this:

[TestMethod]
public void TestCopyWithDependencies()
{
var tpc = TfsTeamProjectCollectionFactory.GetTeamProjectCollection(new Uri("http://localhost:8080/tfs/defaultcollection"));
var target = new WorkItemCopyer(tpc, "Code11");

// test finding the query
var query = target.FindQuery("Tasks_Release1_Sprint1");
Assert.IsNotNull(query);

// test running the query
target.RunQuery(query);
Assert.IsTrue(target.WorkItems.Count > 0);

// test copy
var count = target.CopyWorkItems(@"Code11\Release 1\Sprint 2");
Assert.AreEqual(target.WorkItems.Count, count);
}

But there’s a problem here: if we run these tests and the server is down, they’ll fail. If someone renames the query, the tests will fail. If for some reason the query return 0 work items, the test will at best be inconclusive. So we clearly need to isolate the test from the TFS server. Enter the Fakes framework.


Fakes


The Fakes Framework came out of the Moles Framework from the MS RiSE team. It allows you to isolate your test code from almost anything using an interception mechanism.


To use Fakes, you’ll first need to create the Fake assemblies. We’ll right click each TeamFoundation dll reference, select “Create Fakes” and we’ll be ready to go.


image


You’ll see the Fakes assemblies in your References now.


image


Faking enough to Instantiate a WorkItemCopyer


The first thing you need to know about fakes is that they only work inside a ShimsContext, which you can wrap into a using. In order to construct the WorkItemCopyer, we’re going to need a TeamProjectCollection object. So let’s see if we can fake it:

[TestMethod]
public void TestCopyerInstantiate()
{
using (ShimsContext.Create())
{
// set up the fakes
var fakeTPC = new ShimTfsTeamProjectCollection();

var target = new WorkItemCopyer(fakeTPC, "Code11");
Assert.IsNotNull(target);
}
}

If you run this code, you’ll get an error in the WorkItemCopyer constructor:

image

The GetService method seems to be confused. We’ll need to fake that call to return a fake WorkItemStore. So we create a fake store (newing up a ShimWorkItemStore). But now how do we fix the GetService call? You’ll notice it’s fake counterpart is not on the ShimTeamProjectCollection class. This is because the method doesn’t exist on the TeamProjectCollection class, but on it’s base class, TfsConnection. Now according to the MSDN Fakes documentation (which talks about faking methods in base classes), I would have expected this code to work:

var fakeStore = new ShimWorkItemStore();
var fakeTPC = new ShimTfsTeamProjectCollection();
var fakeBase = new ShimTfsConnection(fakeTPC);
fakeBase.GetServiceOf1(() => fakeStore);

But for some reason, this doesn’t work. So we’ll do the next best thing is to fake the GetService method for all instances of TfsConnection (which will include and TeamProjectCollection object as well). Here’s the code now:

[TestMethod]
public void TestCopyerInstantiate()
{
using (ShimsContext.Create())
{
// set up the fakes
var fakeStore = new ShimWorkItemStore();
var fakeTPC = new ShimTfsTeamProjectCollection();
ShimTfsConnection.AllInstances.GetServiceOf1((t) => fakeStore);

var target = new WorkItemCopyer(fakeTPC, "Code11");
Assert.IsNotNull(target);
}
}

Now we get a bit further – we get to the code that initializes the QueryHierarchy property and then we get a ShimNotImplementedException:


image


This is because the getter method Projects on our fake store is not faked. So we’ll change the code to return a fake TeamProjectCollection, which in turn needs a fake string indexer method to get a TeamProject which in turn needs a fake QueryHierarchy getter method… to save time, I’ll show you the completed code:

[TestMethod]
public void TestCopyerInstantiate()
{
using (ShimsContext.Create())
{
// set up the fakes
var fakeHierarchy = new ShimQueryHierarchy();
var fakeProject = new ShimProject()
{
NameGet = () => "TestProject",
QueryHierarchyGet = () => fakeHierarchy
};
var fakeProjectCollection = new ShimProjectCollection()
{
ItemGetString = (projectName) => fakeProject
};
var fakeStore = new ShimWorkItemStore()
{
ProjectsGet = () => fakeProjectCollection
};
var fakeTPC = new ShimTfsTeamProjectCollection();
ShimTfsConnection.AllInstances.GetServiceOf1((t) => fakeStore);

// test
var target = new WorkItemCopyer(fakeTPC, "Code11");
Assert.IsNotNull(target);
}
}

So far so good: we can at least instantiate the WorkItemCopyer. In the Part 2 post we’ll fake some Query folders and Query objects as well as some WorkItems so that we can complete a test for copying work items.


Happy faking!