Freigeben über


DM-V-VM part 4: Unit testing the DataModel

In part 3, I showed code for StockModel, a DataModel for stocks. On the Max team, we are big believers in extensive unit testing of our code. It took some time to learn the best way to test some of the WPF related stuff. In this post, I intend to demonstrate how to unit test code that involves a Dispatcher.

We use the unit testing framework built into at least certain flavors of VS 2005, but I think other frameworks like NUnit are pretty similar.

The first thing we'll define inside our class is a mock IStockQuoteProvider that returns a known value and tracks what symbol it was called with:

    [TestClass]

    public class StockModelTests

    {

        /// <summary>

        /// IStockQuoteProvider that returns 100

        /// </summary>

        private class TestQuoteProvider : IStockQuoteProvider

        {

            public bool TryGetQuote(string symbol, out double quote)

            {

                _lastSymbol = symbol;

                quote = 100.0;

 

                return true;

            }

 

            public string LastSymbol

            { get { return _lastSymbol; } }

 

            private string _lastSymbol = "";

        }

Now, I'll walk through the test method a piece at a time. First, we'll create a StockModel and check the initial state:

        [TestMethod]

        public void TestStockModelProviderGetsValue()

        {

            TestQuoteProvider testQuoteProvider = new TestQuoteProvider();

 

            StockModel stockModel = new StockModel("MSFT", testQuoteProvider);

 

            // Verify the initial model state

            Assert.AreEqual("MSFT", stockModel.Symbol);

            Assert.AreEqual(0.0, stockModel.Quote);

            Assert.AreEqual(DataModel.ModelState.Fectching, stockModel.State);

 

At this point, the StockModel has queued the work item to fetch the state, and it may have even executed it. It may seem like there's a race condition here where the quote would be updated, but there isn't. The background work item uses Dispatcher.BeginInvoke to change the state and/or quote. All this does is put something in the dispatcher queue. But, that won't get executed because the dispatcher isn't processing messages.

The way we'll get the get the dispatcher to run is with a DispatcherFrame. The basics of the DispatcherFrame is that when you call Dispatcher.PushFrame(), it will process messages in the dispatcher until DispatcherFrame.Cotinue is set to false. At that point Dispatcher.PushFrame() will return.

What we'll do is add a property changed handler that sets DispatcherFrame.Continue false when the model's state changes from fetching. I like to use an anonymous delegate for this because I find it readable and it gives easy access to the local variables we need.

            DispatcherFrame frame = new DispatcherFrame();

 

            PropertyChangedEventHandler waitForModelHandler = new PropertyChangedEventHandler(

                delegate(object sender, PropertyChangedEventArgs e)

                {

                    if (e.PropertyName == "State" && stockModel.State !=

                        DataModel.ModelState.Fectching)

                    {

                        frame.Continue = false;

                    }

                });

            stockModel.PropertyChanged += waitForModelHandler;

 

            Dispatcher.PushFrame(frame);

When Dispatcher.PushFrame returns, it means that we've hit the property changed handler and the model is up to date. Now, we can verify the final state:

            Assert.AreEqual(DataModel.ModelState.Active, stockModel.State);

            Assert.AreEqual(100.0, stockModel.Quote);

            Assert.AreEqual("MSFT", testQuoteProvider.LastSymbol);

    }

That's the test. This has tested the code path where the provider returns a quote. Another test is to use an IStockQuoteProvider that returns false and makes sure the state is set to invalid. I'm not including that code in this post because it's pretty obvious from the code above, but I'll include it in the full sample at the end.

I want to point out a couple of limitations with this approach...

The first is that if the property changed handler isn't called because of a bug in the code, the test will just hang. One solution to this is to use a DispatcherTimer that sets a failure flag and sets frame.Continue to false. If the failure flag is set when PushFrame returns, it means the property changed handler wasn't hit. If it was hit, you just need to reset the DispatcherTimer.

Another limitation is that if you have a bunch of tests run in a row using this approach, it's possible that things left in the dispatcher in one test will cause failures in a future test. This can be solved by running the test on its own thread with a private dispatcher.

Solving these limitations is left as an exercise for the reader :-) If you have questions, let me know.

Comments

  • Anonymous
    August 31, 2006
    Providing a link to the next part in the series http://blogs.msdn.com/dancre/archive/2006/08/05/689542.aspx
  • Anonymous
    September 27, 2006
    If you're doing WPF development, you really need to check out Dan Crevier 's series on DataModel-View-ViewModel.
  • Anonymous
    October 11, 2006
    I thought I should add a post with the full list of posts in the D-V-VM pattern. They are: DataModel-View-ViewModel