Tuesday, April 08, 2008

CAB/SCSF: Unit Testing Presenters, Views, Events

Unit-testing a CAB/SCSF module and all its presenters and views, in addition to testing the publish/subscribe event handling, can be quite challenging to get started with. The TestableRootWorkItem class is required to get started with unit-testing and mocking, as this does all the dependency injection that CAB depends on. Just add all the components to the WorkItem to trigger the DI mechanism.

Use the TestableRootWorkItem like this:

private MockRepository _mocks;
private TestableRootWorkItem _workitem;
private MyViewPresenter _presenter;
private IMyView _view;
private IMyService _service;

[TestFixtureSetUp]
public void Initialize()
{
_mocks = new MockRepository();
_service = _mocks.CreateMock<IMyService>();
_view = _mocks.CreateMock<IMyView>();

_workitem = new TestableRootWorkItem();
_workitem.Services.Add(_service);

//perform the CAB DI-container magic
_presenter = _workitem.Items.AddNew<MyViewPresenter>();
_presenter.View = _view;
}


Note: do not try to access the workitem or any injected dependencies such as [ServiceDependency] inside constructors, things might not have been resolved by the DI container yet. This especially applies to stuff from base classes and things that are not resolved using an [InjectionConstructor].

When testing that event publishing actually triggers the subscribers, you can add fake event publishers and subscribers to help with the mocking. In addition, your unit-test should verify that the raised event causes presenters and views to be loaded by checking the content of the Items and SmartPart collections afterwards.

Implement your CAB/SCSF event-based unit-test like this:

[TestFixture]
public class ModuleTestFixture
{
. . .

[TestFixtureSetUp]
public void Initialize()
{
_workitem = new TestableRootWorkItem();
_eventPublisher = _workitem.Items.AddNew<MockEventPublisher>(_eventPublisherId);
_eventSubscriber = _workitem.Items.AddNew<MockEventSubscriber>(_eventSubscriberId);
_moduleInitializer = new Module(_workitem);
_moduleInitializer.Load();
}

[Test]
public void ShouldLaunchPriceConfigViewOnEvent()
{
int itemPresentersCount = FindItemsByTypeRecursive <PriceConfigurationViewPresenter>(_workitem);
Assert.AreEqual(0, itemPresentersCount);

int smartPartViewsCount = FindSmarPartsByTypeRecursive <PriceConfigurationView>(_workitem);
Assert.AreEqual(0, smartPartViewsCount);

_eventPublisher.OnNewOrder(new NewOrderEventArgs(123, Constants.OperationNames.StartPriceConfigurationView));


Assert.AreEqual(true, _eventPublisher.HasRaisedNewOrder, "EventPublication <NewOrder> failed");
Assert.AreEqual(true, _eventSubscriber.HasHandledNewOrder, "EventSubscription <NewOrder> failed");

itemPresentersCount = FindItemsByTypeRecursive <PriceConfigurationViewPresenter>(_workitem);
Assert.AreEqual(1, itemPresentersCount);

smartPartViewsCount = FindSmarPartsByTypeRecursive <PriceConfigurationView>(_workitem);
Assert.AreEqual(1, smartPartViewsCount);
}


private int FindItemsByTypeRecursive<T>(WorkItem workItem)
{
ICollection<T> items = workItem.Items.FindByType<T>();
int typeCount = items.Count;
foreach (System.Collections.Generic.KeyValuePair<string, WorkItem> item in workItem.WorkItems)
{
typeCount += FindItemsByTypeRecursive<T>(item.Value);
}
return typeCount;
}


private int FindSmarPartsByTypeRecursive<T>(WorkItem workItem)
{
ICollection<T> items = workItem.SmartParts.FindByType<T>();
int typeCount = items.Count;
foreach (System.Collections.Generic.KeyValuePair<string, WorkItem> item in workItem.WorkItems)
{
typeCount += FindSmarPartsByTypeRecursive<T>(item.Value);
}
return typeCount;
}

}


public class MockEventPublisher
{
[EventPublication(Constants.EventTopicNames.NewOrder, PublicationScope.Global)]
public event System.EventHandler<NewOrderEventArgs> NewOrder;

public virtual void OnNewOrder(NewOrderEventArgs eventArgs)
{
if (NewOrder != null)
{
_hasRaisedNewOrder = true;
NewOrder(this, eventArgs);
}
}

public bool HasRaisedNewOrder
{
get { return _hasRaisedNewOrder; }
set { _hasRaisedNewOrder = value; }
}
private bool _hasRaisedNewOrder = false;
}


public class MockEventSubscriber
{
[EventSubscription(Constants.EventTopicNames.NewOrder)]
public void OnNewOrder(object sender, NewOrderEventArgs eventArgs)
{
if (eventArgs.OperationName == Constants.OperationNames.StartPriceConfigurationView)
{
_hasHandledNewOrder = true;
}
}

public bool HasHandledNewOrder
{
get { return _hasHandledNewOrder; }
set { _hasHandledNewOrder = value; }
}
private bool _hasHandledNewOrder = false;
}

If you have issues with understanding that a WorkItem is just a dependency injection container or what all the different collections such as Items, Services, SmartParts, WorkSpaces, etc are for; I recommend reading Rich Newman's Introduction to CAB/SCSF. Read part 18 first.

No comments: