Apr 4, 2012

More on Fakes – the beta has issues

Thanks to Peter Provost for helping answer a couple of questions I had about Fakes – you can look at some of my code in my previous posts about using Fakes with the TFS API.

There were two scenarios that I hit snags with. The first was faking methods in a class that are actually defined on the class’s base class. The second was faking an ObservableCollection<T>.

Faking Base Class Methods

Consider this code:

[TestMethod]
public void BaseMethodWontWorkInBeta()
{
using (ShimsContext.Create())
{
var shimWorkItemStore = new ShimWorkItemStore();
var shimTfsTeamProjectCollection = new ShimTfsTeamProjectCollection();
var shimTfsConnection = new ShimTfsConnection(shimTfsTeamProjectCollection);
shimTfsConnection.GetServiceOf1<WorkItemStore>(() => shimWorkItemStore);

var workItemStore = shimTfsTeamProjectCollection.Instance.GetService<WorkItemStore>();

Assert.AreSame(shimWorkItemStore.Instance, workItemStore);
}
}

The exercise here is to try to fake the TfsTeamProjectCollection.GetService<T> method. However, we can’t do it directly on the TfsTeamProjectCollection object, since the method is defined in its base class, TfsConnection.


In line 8, we’re using the ShimTfsConnection constructor that takes in an instance of its child class (the TfsTeamProjectCollection) in order to override the method in the child class. This should work, but Peter told me that this doesn’t work in the Beta – it’ll work when the next release of fakes comes out. The workaround is to use the ShimTfsConnection.AllInstances class to override the GetService<T> method (see my earlier posts where I show how to do this).


Faking ObservableCollection<T>


Let’s look at some more code:

[TestMethod]
public void BindWontWorkBecauseOfObservableCollectionInBeta()
{
using (ShimsContext.Create())
{
var ss = new StubISharedStep();
var sharedStepReference = new StubISharedStepReference
{
FindSharedStep = () => ss
};

var actions = new List<ITestAction>
{
sharedStepReference
};

var fakeTestActions = new ShimTestActionCollection();
fakeTestActions.Bind((IList<ITestAction>)actions);

Assert.AreEqual(1, fakeTestActions.Instance.Count);

var ssr = (ISharedStepReference)(fakeTestActions.Instance[0]);
Assert.AreSame(sharedStepReference, ssr);

Assert.AreSame(ss, ssr.FindSharedStep());
}
}

The point of this code is to try to fake a TestActionCollection, which is an ObservableCollection<ITestAction>. I had successfully managed to fake other collections that were not ObservableCollections (like a WorkItemCollection), and to do that I used the Bind() method of the Shim to bind the fake collection to a list of items. However, I couldn’t get the code to work on the ObservableCollection<T>. (In the above code, I get a NullReferenceException on line 18 when calling the Bind() method.


Peter confirmed that the reason for this is twofold: one, I hadn’t added Fakes for System, which is where the ObservableCollection<T> class resides. This is a performance optimization that the Fakes creation employs so that they don’t create fakes for classes that you’ll never use.


Secondly, and rather unfortunately, even adding that Fake doesn’t work in the Beta. The Fakes creation uses a white-list to generate fakes in the System namespace, and the ObservableCollection<T> isn’t on the white-list in the Beta. In the next release, the System white-list will be customizable, so I am looking forward to getting my grubby paws on that!


Unfortunately, this scenario has no work-around in the Beta, so if you’re trying to Fake an ObservableCollection<T>, you’ll have to wait for the next release of Fakes.


I really like the Fakes framework and what it can do for testing, so I am looking forward to seeing what else comes out of the woodwork.


Happy faking!

Mar 20, 2012

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

In Part 1, we started faking some TFS objects. We got as far as faking the TeamProjectCollection and WorkItemStore. In this post, we’ll complete the test for copying work items by providing a fake QueryHierarchy and a fake list of WorkItems.

Binding IEnumerables

Since the QueryHierarchy is an IEnumerable, we’ll need to either fake the GetEnumerator() method, or find a way of making the fake QueryHierarchy enumerable! The problem with trying to fake the GetEnumerator() method is that you end up in an infinite loop – if you’re trying to get the enumerator for sub nodes of the hierarchy, you’ll also need to call GetEnumerator() which will call GetEnumerator() and so on and so on… so we’ll see if we can make the fake QueryHiearchy behave like an enumerable object. Which is really quite easy when you know how!

We create a list of QueryFolders (outside the ShimsContext, since we want these object to be real and not fake) and then call the Bind() method on the fake QueryHierarchy:

var myQueries = new QueryFolder("My Queries");
var sharedQueries = new QueryFolder("Team Queries");
sharedQueries.Add(new QueryDefinition("My Tasks", "SELECT System.Id FROM WorkItems WHERE System.WorkItemType = 'Task'"));
sharedQueries.Add(new QueryDefinition("My Bugs", "SELECT System.Id FROM WorkItems WHERE System.WorkItemType = 'Bug'"));
var topQueries = new List<QueryItem>() { myQueries, sharedQueries };

using (ShimsContext.Create())
{
// set up the fakes
var fakeHierarchy = new ShimQueryHierarchy();
fakeHierarchy.Bind(topQueries);

Now when we run the code, the QueryHierarchy behaves like an enumeration and a call to find the “My Tasks” query will result in a QueryDefinition being returned!


Faking an Interface


The next thing we’ll need to fake is the RunQuery() method on the Query. The first snag we’ll hit is that the RunQuery() method in the WorkItemCopyer uses the IGroupSecurityService to resolve the current user’s display name for any @me macros in the query. So we’ll first need to fake that. Hang on – how do you fake an interface?? Enter Stubs.


So far we’ve only used Shims. For faking interfaces, we’ll need to switch to Stubs. The only method on the IGroupSecurityService interface that we want to fake is the ReadIdentity() method. So we can fake that and return a fake Identity object. Again, we’ll run into the problem of wanting a real object in a fake world (i.e. the ShimsContext) so we’ll use a sneaky way of working around that. Here’s the code for the fake IGroupSecurityService:

var fakeSecurityService = new StubIGroupSecurityService()
{
ReadIdentitySearchFactorStringQueryMembership = (searchFactor, criteria, identity) => ShimsContext.ExecuteWithoutShims<Identity>(() => new Identity() { DisplayName = "Bob" })
};
ShimTfsConnection.AllInstances.GetServiceOf1<IGroupSecurityService>((t) => fakeSecurityService);

This will create the Identity object “outside” the current ShimsContext. Of course, we need to tell the TfsConnection GetService<T> call to return the fake security service when asked for an IGroupSecurityService (which is done in the last line above).


Faking Constructors


The next thing that the WorkItemCopyer code does is instantiate a Query object in order to execute the Work Item Query. We’ll need to fake the constructor in order to return a fake Query. The method we particularly want to fake out on the Query object is the RunQuery() method, which is going to return a WorkItemCollection. We’ll want to create a list of Work Items and bind a fake WorkItemCollection to the list so that we can enumerate the fake results. There is also a call to the IterationPath setter and a couple of getters on the WorkItem itself, so we’ll fake that at the same time. Here’s the code:

ShimQuery.ConstructorWorkItemStoreStringIDictionary = (q, store, text, dict) =>
{
new ShimQuery(q)
{
RunQuery = () =>
{
var workItemType = new ShimWorkItemType()
{
NameGet = () => "Task"
};
var list = new List<WorkItem>()
{
new ShimWorkItem()
{
IdGet = () => 12,
TitleGet = () => "Some Work Item",
TypeGet = () => workItemType,
IterationPathSetString = (path) => { },
}
};
var workItems = new ShimWorkItemCollection()
{
CountGet = () => list.Count
};
workItems.Bind(list);
return workItems;
}
};
};

Finally, we’ll create a couple of counters to make sure that our code calls the copy and the save methods of the work items for each work item.

var copyCount = 0;
ShimWorkItem.AllInstances.Copy = (w) =>
{
copyCount++;
return new ShimWorkItem(w);
};
var saveCount = 0;
ShimWorkItem.AllInstances.Save = (_) => saveCount++;

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

// test find query
var query = target.FindQuery("My Tasks");
Assert.IsNotNull(query);

// test run query
target.RunQuery(query);
Assert.AreEqual(1, target.WorkItems.Count);

// test copy work items
var count = target.CopyWorkItems("Test Iteration");
Assert.AreEqual(1, count);
Assert.AreEqual(1, copyCount);
Assert.AreEqual(1, saveCount);

Voila! We’ve been able to test our code without actually connecting to a real TFS service. And coverage? Well, it’s up to 97% for the WorkItemCopyer class. Not too bad!


The completed solution is available on my skydrive.


(More) Happy Faking!

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

(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<WorkItemStore>();
QueryHierarchy = Store.Projects[TeamProjectName].QueryHierarchy;
}

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

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

private string GetCurrentUserDisplayName()
{
var securityService = TPC.GetService<IGroupSecurityService>();
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<QueryDefinition>())
{
if (query.Name == queryName)
{
return query;
}
}
QueryDefinition subQuery = null;
foreach (var subFolder in folder.OfType<QueryFolder>())
{
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 <ENTER> 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<T> 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<T> 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<WorkItemStore>(() => fakeStore);

But for some reason, this doesn’t work. So we’ll do the next best thing is to fake the GetService<T> 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<WorkItemStore>((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<WorkItemStore>((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!