Tuesday, June 29, 2010

Unit Testing Dynamic XAP Files

By now, you probably are aware that you can dynamically load XAP files using the Managed Extensibility Framework (MEF) within your Silverlight applications. Have you been scratching your head, however, and wondering how on earth you would actually test something like that?

It is possible, and here's a quick post to show one way you can.

First, we need a decent deployment service. You're not really going to hard-code the download and management, are you? I didn't think so. If you need an example, look no further than the sample code I posted to Advanced Silverlight Applications using MEF. Here's what the interface looks like:

public interface IDeploymentService
{
    void RequestXap(string xapName, Action<Exception> xapLoaded);       
       
    AggregateCatalog Catalog { get; }
}

This keeps it simple. Request the xap file, then specify a delegate for a callback. You'll either get a null exception object (it was successful) or a non-null (uh... oh.)

Now, let's focus on testing it using the Silverlight Unit Testing Framework. The first caveat is that you cannot use it on the file system. This means that your project will not work if you run it with a test page rather than hooking it to a web server (local or not).

Doing this is simple. In your ASP.NET project, go to the Silverlight tab and add your test project. When you are adding it, there is an option to generate a test page. I typically have one "test" web project with all of my test Silverlight applications, so I will have multiple test pages. To run a particular test, you simply set your ASP.NET web project as the start up project, then the corresponding test page (we're talking the aspx, not the html) as the start page. I usually delete the automatically generated HTML pages.

Now we need to give MEF a test container. The caveat here is that, without a lot of work, it's not straightforward to reconfigure the host container so you'll want to make sure you test a given dynamic XAP file only once, because once it's loaded, it's loaded.

This is what my application object ends up looking like:

public partial class App
{
    public AggregateCatalog TestCatalog { get; private set; }

    public App()
    {
        Startup += Application_Startup;            
        InitializeComponent();
    }

    private void Application_Startup(object sender, StartupEventArgs e)
    {
        // set up a catalog for tests
        TestCatalog = new AggregateCatalog();
        TestCatalog.Catalogs.Add(new DeploymentCatalog());

        var container = new CompositionContainer(TestCatalog);

        CompositionHost.Initialize(container);

        // now set up the unit testing framework
        var settings = UnitTestSystem.CreateDefaultSettings();
        RootVisual = UnitTestSystem.CreateTestPage(settings);
    }              
}

Here, I haven't composed anything, just set up the container.

Now I'm going to add a simple dynamic XAP for testing. I add a new Silverlight application and wire it to the test web site but do NOT generate a test page. I blow away the App.xaml and MainPage.xaml resources, and add a simple class called Exports. Here is my class:

public class Exports
{
    private const string TESTTEXT = "TestText";

    [Export(TESTTEXT, typeof(string))]
    public string TestText { get { return TESTTEXT; } }
}

Yes, you got it - just a simple export of a string value. Now let's write our test. I create a new test class and decorate it with the TestClass attribute. I am also running asynchronous tests, so it's best to inherit the test from SilverlightTest which has some base methods for asynchronous testing.

Let's take a look at the set up for my test:

[TestClass]
    public class DeploymentServiceTest : SilverlightTest
    {
        private const string DYNAMIC_XAP = "DynamicXap.xap";
        private const string TESTTEXT = "TestText";

        private DeploymentService _target;
    
        [Import(TESTTEXT, AllowDefault = true, AllowRecomposition = true)]
        public string TestString { get; set; }
     
        public DeploymentServiceTest()
        {
            CompositionInitializer.SatisfyImports(this);
        }

        [TestInitialize]
        public void TestInit()
        {
            if (Application.Current.Host.Source.Scheme.Contains("file"))
            {
                _target = null;
            }
            else
            {
                _target = new DeploymentService();
                ((App) Application.Current).TestCatalog.Catalogs.Add(_target.Catalog);
            }
        }
}

So right now I'm simply setting up my targets. The property is key - by composing imports on construction, I register my test class with the MEF system. Right now, however, I haven't loaded anything, so it won't be able to satisfy the import. By using AllowDefault true, however, I tell it I'm expecting something later and setting it to null is fine. The recomposition is what will trigger an update once the catalogs change. I also reach out to the test catalog I set up in the main application and add the catalog from my deployment service to it. Note that if I am running on the file system, I don't bother setting up my service.

Next, I can add a stub to determine if I can even test this. If I am running from the file system, the deployment service is never set up. I created a helpful method that asserts an "inconclusive" when this is the case:

private bool _CheckWeb()
{
    if (_target == null)
    {
        Assert.Inconclusive("Cannot test deployment service from a test page. Must be hosted in web.");
        return false;
    }

    return true;
}        

Now we can write our main test. First, we check to make sure we are in a web context. Then, we load the xap, and once it is loaded, confirm there were no errors and that our property was successfully set:

[Asynchronous]
[TestMethod]
public void TestValidXap()
{
    if (!_CheckWeb())
    {
        return;
    }

    Assert.IsTrue(string.IsNullOrEmpty(TestString), "Test string should be null or empty at start of test.");
    _target.RequestXap(DYNAMIC_XAP, exception =>
                                        {
                                            Assert.IsNull(exception, "Test failed: exception returned.");
                                            Assert.IsFalse(string.IsNullOrEmpty(TestString),
                                                            "Test failed: string was not populated.");
                                            Assert.AreEqual(TESTTEXT, TestString,
                                                            "Test failed: property does not match.");
            }
}

And that's pretty much all there is to it - of course, I am also adding checks for things like contract validation (are you passing me a valid xap name?) and managing duplicates, but you get the picture.

Jeremy Likness