Unit Testing A CAB Controller
In my previous post I demonstrated how to create an application based on the Composite UI Application Block (CAB) while truly separating Views from Controllers by placing them in separate assemblies.
My main motivation for separating these application layers is to enable unit testing of the Controller layer. In the previous post I promised to show how to do this, so here I'll demonstrate how to unit test the Controller library I created in my last post.
Obviously, the unit test project is a new project in itself. It must have a reference to the Controller project as well as Microsoft.Practices.CompositeUI and Microsoft.Practices.ObjectBuilder, but notice that there's no reference to the UI project, System.Windows.Forms or Microsoft.Practices.CompositeUI.WinForms.
As an example, I'll write a test that asserts that when the application starts, the IMyView instance in the root WorkItem has the expected message text.
First of all, I need a bootstrapper for CAB that can host all the CAB-specific code. If you recall the previous post, in a Windows Forms-based application, this is done by deriving a class from FormShellApplication<TWorkItem, TShell> and calling its Run method. This approach is designed specifically for a Windows Forms solution, so it can't very well be used for unit testing.
FormShellApplication<TWorkItem, TShell> derives, through a series of intermediate classes, from CabApplication<TWorkItem>, which is a class that requires no presence of Windows Forms. For test purposes, I can derive a class to host the CAB environment during testing:
public class TestCabApplication :
CabApplication<MyWorkItem>
{ private TestCabApplication()
: base()
{ }
protected override void AddBuilderStrategies
(Builder builder)
{ base.AddBuilderStrategies(builder);
TypeMappingPolicy policy =
new TypeMappingPolicy(typeof(StubMyView),
"IMyView");
builder.Policies.Set<ITypeMappingPolicy>(policy,
typeof(IMyView), "IMyView");
}
protected override void OnRootWorkItemInitialized()
{ base.OnRootWorkItemInitialized();
MyWorkItem mwi = this.RootWorkItem;
mwi.Workspaces.AddNew<StubWorkspace>("mainWorkspace_"); }
protected override void Start()
{ }
internal static TestCabApplication Create()
{ TestCabApplication app = new TestCabApplication();
app.Run();
return app;
}
}
The only member you have to override is the Start method. In FormShellApplication<TWorkItem, TShell>, this is where a new instance of TShell is created and shown when the Run method is invoked. In TestCabApplication, I need to do exactly nothing, since I don't want anything to happen if it isn't controlled by my unit test code.
To initialize the application, it's always necessary to call Run, so I've created a static Create method which takes care of this for me. I could have called Run from the constructor, but Run includes calls to virtual methods in its call chain, and so is dangerous to call from the constructor (see e.g. http://msdn2.microsoft.com/en-us/library/ms182331.aspx), so instead I chose to create this static factory method; I could also just have sealed TestCabApplication, but then you would have wondered why I did that.
Notice that although I've overridden AddBuilderStrategies in a manner very similar to the code in MyShellApplication (see the previous post), this implementation is slightly different: Instead of mapping from IMyView to MyView (which, you may recall, was a Windows Forms User Control), this time I'm mapping to a class call StubMyView, which I'll get back to shortly.
In TestCabApplication I've also overridden OnRootWorkItemInitialized. This is necessary because MyWorkItem expects mainWorkspace_ to be present in the collection of work spaces, so here I'm just adding a stub work space, which is just a class that implements IWorkspace. I'm not going to show you the code for StubWorkspace because it's very simple, but takes up quite a bit of space: I just let Visual Studio auto-generate all the IWorkspace code for me, and then I modified the relevant Show method to do absolutely nothing (instead of throwing an exception).
The StubMyView class is a stub of IMyView and is very simple:
public class StubMyView : IMyView
{ private string message_;
#region IMyView Members
public string Message
{ get { return this.message_; } set { this.message_ = value; } }
#endregion
}
With this bit of scaffolding in place, I can now write the unit test:
[TestMethod]
public void SayHello()
{ TestCabApplication app =
TestCabApplication.Create();
MyWorkItem mwi = app.RootWorkItem;
IMyView mv = mwi.Items.Get<IMyView>("IMyView");
Assert.IsNotNull(mv);
Assert.AreEqual<string>("Hello World", mv.Message);}
The first thing to do is to create and initialize the CAB application. Through a long and convoluted path, this eventually causes MyWorkItem.OnRunStarted to be called, so the only thing left to do is to examine the state of the root WorkItem (MyWorkItem) after the Create method returns.
Since MyWorkItem creates a new IMyView instance and sets it Message property to "Hello World", the unit test will pass.