In the previous post, we developed test strategies for a three tiers, MVVM based Silverlight application, which uses the Managed Extensibility Framework (MEF) in order to loosen the coupling between the components and to provide better testability and extensibility.
In this post, we’ll see how the Managed Extensibility Framework (MEF) fit in with the overall testing strategy and try to figure out the best way to use it in the application in order to provide the best degree of testability.
To MEF or not to MEF (during tests)?
When coming to test MEF based application, the first question that comes to mind is whether to use the MEF composition container to instantiate dependencies during tests.
In component tests the answer is usually NO, in order to provide good degree of separation of concerns we need to isolate the component as much as possible, following that we should not let MEF interfere with the tests.
However, in some cases, the tested functionality uses MEF in order to provide special features such as downloading and loading xaps on the fly, in such cases we’ll need to put MEF into action and allow it to instantiate the dependencies. Since we’ll still need to replace the other components in the application with test doubles (fakes, mock, dummies, etc) – we’ll need to override the default composition container with a custom container that includes the real component under test and fake every thing else.
Don’t use the static composition container in the application
Since we need to exclude MEF in some (most) tests and to replace its composition container in others – we can’t use the default static composition container that MEF provides by default. Here’s why:
1) In order to replace the default static container we need to call CompositionHost.Initialize(container) that cannot be called more than once, which means that if more than one test will try to replace the container we’ll experience an ‘InvalidOperationException’ (this is a good example for the shared fixture anti-pattern)
2) Even if we wouldn’t have to include MEF in any of our tests, when using the static container the CompositionInitializer.SatisfyImports (that put MEF into action) can be called from any class in the application, which means that we might not be not be able to exclude MEF in some of the tests
Create composition container instance and call SatifyImportsOnce on the container itself
So what can we do? In order to retire ourselves from the static container, the application should create CompositionContainer object upon initialization, add the appropriate catalogs and/or the appropriate objects (via ComposePart) to the container, and call SatifyImportsOnce on the composition container itself in order to satisfy the imports of a given instance (usually the root object).
Allow replacing the container instance with a test double
If internal classes in the application need to satisfy imports on the fly, we need to provide them with a way to access the global composition container such that we can replace the calls to the container with an empty implementation during tests.
In order to accomplish that, we’ll encapsulate the CompositionContainer with CompositionContainerService class that is accessible only though ICompositionService interface. We’ll introduce a new singleton called CompositionServiceProvide that will be shared widely and expose ICompositionService interface to enable access to the CompositionContainerService. The CompositionServiceProvide will allow replacing the CompositionContainerService with a dummy test double through the ICompositionServiceProviderTesting interface (implemented explicitly).
Here’s the code for the CompositionContainerService :
1: public class CompositionContainerService : ICompositionService
2: {
3: private readonly CompositionContainer m_container;
4: private readonly AggregateCatalog m_aggregateCatalog;
5:
6: public CompositionContainerService()
7: {
8: m_aggregateCatalog = new AggregateCatalog();
9: m_container = new CompositionContainer(m_aggregateCatalog);
10:
11: }
12:
13: public void SatisfyImports(object o)
14: {
15: m_container.SatisfyImportsOnce(o);
16: }
17:
18: public void ComposeParts(params object[] attributedParts)
19: {
20: m_container.ComposeParts(attributedParts);
21: }
22:
23: public void AddCatalog(ComposablePartCatalog catalog)
24: {
25: m_aggregateCatalog.Catalogs.Add(catalog);
26: }
27:
28: public CompositionContainer Container
29: {
30: get { return m_container; }
31: }
32: }
Here’s the code for the singleton CompositionServiceProvider :
1: public interface ICompositionServiceProviderTesting
2: {
3: void SetCompositionService(ICompositionService compositionService);
4: }
5:
6: public class CompositionServiceProvider : ICompositionServiceProviderTesting
7: {
8: static readonly CompositionServiceProvider s_instance = new CompositionServiceProvider();
9:
10: public static CompositionServiceProvider Instance
11: {
12: get { return s_instance; }
13: }
14:
15: private ICompositionService m_compositionService;
16:
17: public ICompositionService Service
18: {
19: get
20: {
21: if (m_compositionService == null)
22: {
23: m_compositionService = new CompositionContainerService();
24: }
25:
26: return m_compositionService;
27: }
28: }
29:
30: void ICompositionServiceProviderTesting.SetCompositionService(ICompositionService compositionService)
31: {
32: m_compositionService = compositionService;
33: }
34: }
Using MEF during tests
With MEF for Silverlight, developers can take advantage of the new DeploymentCatalog for downloading only a portion of the code when the user connect, and adding code (downloading extra xap files) on demand as the application makes progress. When a new xap is downloaded to the client browser and loaded into the container, MEF searches the xap for exports and start a process of re-satisfying the imports.
We’ll add the required functionality for downloading (AddXap) and loading (LoadXap) xaps to the container into the CompositionServiceProvider (described above)
1: public class CompositionServiceProvider : ICompositionServiceProviderTesting
2: {
3: ...
4:
5: readonly Dictionary<string, DeploymentCatalog> m_catalogs = new Dictionary<string, DeploymentCatalog>();
6:
7: public void AddXap(string uri, EventHandler<AsyncCompletedEventArgs> downloadCompletedCallback)
8: {
9: var catalog = new DeploymentCatalog(uri);
10: m_catalogs.Add(uri, catalog);
11:
12: catalog.DownloadCompleted += downloadCompletedCallback;
13: catalog.DownloadAsync();
14: }
15:
16: public void LoadXap(string uri)
17: {
18: DeploymentCatalog catalog = m_catalogs[uri];
19: Service.AddCatalog(catalog);
20:
21: }
22:
23: }
Take the following application for example, when the user connect it’s presented with a basic UI that allows initiating basic activities, while the user is looking at the screen, another xap that includes an extra user control is downloaded in the background. When the user click on the ‘Extension’ button, the second xap (given that it is already available on the client) is loaded into the container, and as a result a dedicated property on the VIew that is configured to import the new user control (according to its special metadata) is automatically populated. Once the property is populated with the new user control, the View takes care of displaying the user control on the gray panel.
Here’s the application:
Here’s the View code:
1: [Export]
2: public partial class BooksPage : Page, IPartImportsSatisfiedNotification
3: {
4: [ImportingConstructor]
5: public BooksPage(BooksViewModel viewModel)
6: {
7: InitializeComponent();
8:
9: DataContext = viewModel;
10: }
11:
12: [ImportMany(AllowRecomposition=true)]
13: public Lazy<UserControl, IWidgetMetadata>[] Widgets { get; set; }
14:
15: private void extentionButton_Click(object sender, System.Windows.RoutedEventArgs e)
16: {
17: // Load the xap (which as an effect will also re-satisfy the dependencies)
18: CompositionServiceProvider.Instance.LoadXap("PrototypeMVVM.Extentions.xap");
19: }
20:
21: public void OnImportsSatisfied()
22: {
23: wrapPanel1.Children.Clear();
24:
25: foreach (var widget in Widgets)
26: {
27: if (widget.Metadata.Location == WidgetLocation.Bottom)
28: {
29: wrapPanel1.Children.Add(widget.Value);
30: }
31: }
32: }
33: }
In order to verify that once the ‘Extension’ button is clicked the appropriate xap is loaded and the View is updated appropriately -we need to take the following steps;
1) Replace the original container with a new container
2) Create a real View, real ViewModel, fake DAL and add them to the container via ComposePart (notice that we didn’t use static TypeCatalog in order to configure the container, It’s simpler to simply add the objects that are required to satisfy the imports)
3) Download the second xap and wait until the download complete.
4) Simulate click on the ‘Extension’ button
5) Verify that the new user control is displayed
Here’s the test code:
1: [TestMethod]
2: [Asynchronous]
3: public void CheckCompositionAfterPackageLoad()
4: {
5: // Create the dependencies and add them to the container
6: var dalFake = new BooksDataServiceProviderFake();
7: var viewModel = new BooksViewModel(dalFake);
8: var view = new BooksPage(viewModel);
9:
10: // *** UsersWindow is a lazy import that is not being used in the test
11: // nevertheless, we need to instantiate it (or add it to TypeCatalog) and
12: // its dependencies (and there dependencies) and add them to the container
13: var usersWindow = new UsersWindow();
14: var usersViewModel = new UsersViewModel(new UsersDataServiceProviderFake());
15:
16: var container = CompositionServiceProvider.Instance.Service.Container;
17: container.ComposeParts(view, usersWindow, usersViewModel);
18: container.SatisfyImportsOnce(view);
19:
20: TestPanel.Children.Add(view);
21:
22: // download the extension package
23: const string extentionsXap = "PrototypeMVVM.Extentions.xap";
24: CompositionServiceProvider.Instance.AddXap(extentionsXap, (sender, args) => m_xapDownloaded = true);
25:
26: EnqueueConditionalTimeoutChecker timeoutChecker = new EnqueueConditionalTimeoutChecker();
27:
28: // wait for download the complete
29: EnqueueConditional(() =>
30: {
31: timeoutChecker.Check();
32:
33: return m_xapDownloaded;
34: });
35:
36: // Click on the extention button
37: TestApi.EnqueueButtonClick(view.ExtensionButton, this);
38:
39: // since we only loading the xap and not downloading it, the UI re-composition should
40: // happen synchronously
41: EnqueueCallback(() => Assert.AreEqual(1, view.WrapPanel1.Children.Count));
42:
43: EnqueueTestComplete();
44: }
No comments:
Post a Comment