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. https://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.
Comments
Anonymous
May 04, 2006
The Composite UI Application Block&nbsp;(CAB) is a pretty nice piece of technology that allows you to...Anonymous
May 08, 2006
Excellent sample on implementing MVP in different assemblies, you saved my day!Anonymous
May 08, 2006
Patrik, you are most welcome. Thank you for the feedback - it's always nice to get a positive comment. Then I know that perhaps, I'm not just posting all this material into some uncaring void :)Anonymous
May 17, 2006
Many thanks Mark, I am just getting into CAB and your articles have been very informative and helpful.
Your effort is appreciated, keep up the good work =)Anonymous
September 19, 2006
Hi -
You don't need to create an instance of cab app to test your controllers. It's enough to create a TestableWorkItem. Take a look at the reference implementations (Appraisal Workbench and Bank Branch) in the Smart Client Software Factory. We developed both of them using TDD.
http://msdn.microsoft.com/smartclientfactory
Thanks
MatiasAnonymous
October 17, 2006
Hi Matias Thank you for the tip. As far as I can tell, this could work in a number of scenarios, but I'm not sure it will work in exactly this scenario. Because of the indirection in this scenario, it's necessary to register one or more ITypeMappingPolicies with the WorkItem's builder before executing the tests. This must be accessible via the WorkItem's public API, which it isn't by default. Obviously, it is possible to create a testable WorkItem by deriving from WorkItem and exposing the protected builder as a public property, just like you have done in the Smart Client Software Factory reference implementations. However, this testable WorkItem is not one of the production WorkTtem classes you would want to test. To work around this limitation, you basically have two options: Either let all WorkItem classes derive from some TestableWorkItem class which publicly expose its builder, or derive a testable WorkItem from each production WorkItem class you want to test. The latter is a pretty horrible solution, since you need to create a separate, testable descendant for each WorkItem class you want to test, which could potentially be a lot. In addition, you end up testing the testable child class, not the production class itself. Lastly, you wouldn't ever be able to seal any of your WorkItem classes (not that I'm a big fan of sealing my classes in the first case), since that would mean that you wouldn't be able to test them. The first approach (letting all WorkItem classes descend from a testable WorkItem) is less problematic, although you introduce features into the API which exist solely for testing purposes. That's not a very clean API design, so I don't like it for that reason.Anonymous
October 19, 2006
I have been trying out the CAB Controller test suggestions and nearly got there but not quite... I have a service that my WorkItem uses which is created via [ServiceDependency]. The CAB start up fails because it can't find the Service Dependency. If I change to using a default workItem using class TestCabApplication:CabApplication<WorkItem> then I run into problems with the rootItem being disposed by Run() and so not containing my workItem (which gets created by ModuleInit)when I try to access it through: app.RootWorkItem.Items.Get<MyWorkItem>("MyWorkItem"); Any suggestions?Anonymous
October 19, 2006
The comment has been removed