Sunday, June 13, 2010

Extending Silverlight Unit Testing framework – a shallow look.

Last week I had to do a bit of a digging in the SL unit test framework guts (the one that is part of an SL4 toolkit). The justification for that was a need to execute a service call in between test (or work items) invocations.

Specifically, I had to restore server databases to the original snapshot (via wcf service) after data has become “dirty” and we needed a clean state to continue tests with.

There are 1000+1 ways how to achieve anything in SL, and after digging and implementing I got actually other ideas of how this can be done. But exercise was good for educational purposes nonetheless. Please note, that the code is only a first draft conceptual level implementation, so it looks and is dirty.

Here is a diagram at the level I only had to dive into:

image

You enter into tests with creation of a test page by passing instance of UnitTestSettings to CreateTestPage method of UnitTestSystem:

private void Application_Startup(object sender, StartupEventArgs e)
        {
            RootVisual = UnitTestSystem.CreateTestPage(HarnessProvider.CreateDefaultSettings(this.GetType().Assembly));
        }

As diagram should be read from bottom to the top let me apply the same narration style :). We do provide customized settings by means of custom HarnessSettingsProvider class:

    public class HarnessSettingsProvider
    {
        public static UnitTestSettings CreateDefaultSettings(Assembly callingAssembly)
        {
            var settings = new UnitTestSettings();
            if (callingAssembly != null)
            {
                settings.TestAssemblies.Add(callingAssembly);
            }
            settings.TestHarness = new CustomTestHarness();
            settings.TestService = new SilverlightTestService(settings);
 
            return settings;
        }
    }

The only point of customization was settings.TestHarness = new CustomTestHarness(); where we assign our own test harness:

    public class CustomTestHarness : UnitTestHarness
    {
        public override void RestartRunDispatcher()
        {
            this.RunDispatcher = new CustomFastRunDispatcher(new Func<bool>(this.RunNextStep), this.Dispatcher);
            this.RunDispatcher.Complete += new EventHandler(this.RunDispatcherComplete);
            this.RunDispatcher.Run();
        }
        public bool DBCleanupRequired
        {
            get { return ((CustomFastRunDispatcher)RunDispatcher).DBCleanupRequired; }
            set { ((CustomFastRunDispatcher)RunDispatcher).DBCleanupRequired = value; }
        }
    }

Once again, only tiny change here – assignment of our own CustomFastRunDispatcher and introduction of DBCleanupRequired property.

UnitTestHarness is a guy who runs a dispatcher and that is where the core of problem solution is provided:

public class CustomFastRunDispatcher : FastRunDispatcher
    {
        private readonly Func<bool> _runNextStep;
        private readonly Dispatcher _dispatcher;
        private volatile bool _dbCleanupRequired = true;
        public ITestService TestPreparationService { get; set; }
 
        public CustomFastRunDispatcher( Func<bool> runNextStep, Dispatcher dispatcher ) : base(runNextStep, dispatcher)
        {
            _runNextStep = runNextStep;
            _dispatcher = dispatcher;
            TestPreparationService = new ChannelFactory<ITestService>(typeof(ITestService).Name).CreateChannel();
        }
 
        public bool DBCleanupRequired
        {
            get { return _dbCleanupRequired; }
            set { _dbCleanupRequired = value; }
        }
 
        public override void Run()
        {
            if (DBCleanupRequired)
            {
                TestPreparationService.BeginPrepareDatabases(HandlePreparationCallback, null);
            }
            else
            {
                this._dispatcher.BeginInvoke(() => { RunNext(); }); 
            }
        }
        private void HandlePreparationCallback(IAsyncResult ar)
        {
            this._dispatcher.BeginInvoke(
                () =>
                {
                    DBCleanupRequired = false;
                    TestPreparationService.EndPrepareDatabases(ar); 
                    RunNext();
                });
            
        }
        private void RunNext()
        {
            if (IsRunning || _runNextStep())
            {
                Run();
            }
            else
            {
                OnComplete();
            }
        }
    }
}

As you can see, in case when database state is marked for clean up we call a custom TestPreparation wcf client and only in its callback we progress with the next step (next work item). Again as this is only a draft concept, no exception handling here and it is still subject to verify what unhandled exception in End invocation will do to SL unit testing framework.

In SL unit testing framework we execute “work items”. We can see that for example EnqueueCallback queues actually a CallbackWorkItem for execution:

public virtual void EnqueueCallback(Action testCallbackDelegate)
{
    this.EnqueueWorkItem(new CallbackWorkItem(testCallbackDelegate));
}

Knowing this, the following test can show usage and certain advantage of the proposed customization:

        [TestMethod]
        [Asynchronous]
        public void Should_RestoreDatabasesOnDirty()
        {
            // First callback work item is going to mark db as dirty
            EnqueueCallback(() =>
            {
                MarkDatabaseAsDirty();
 
            });
            // Prior to the second call back work item we should have called the service
            // to restore to the original snaphots and we expect that DBCleanupRequired
            // is set to false on service async callback. As a bit of a manual check we 
            // can verify that service really was called between those two work items.
            EnqueueCallback(() =>
            {
                Assert.IsFalse(((CustomTestHarness) UnitTestHarness).DBCleanupRequired);
                EnqueueTestComplete();
            });
        }

Where MarkDatabaseAsDirty is a method of a base test class that sets [Custom]UnitTestHarness DBCleanupRequired property.

This is it for my exploration and first draft concept on the subject of extending Silverlight unit testing framework.

No comments: