Please visit http://www.ningzhang.org!

This is a backup site. Please go to http://www.ningzhang.org for the latest content and best viewing. Thanks!

Oct 30, 2008

Silverlight Toolkit Unit Test

Introduction

Unit test is very important for quality software development. The Silverlight Toolkit has extensive unit tests, as well as good samples. Silverlight Toolkit unit test projects (Controls.Testing, Controls.Test.DataVisualization, Controls.Testing.Theming) use the Silverlight Unit Test Framework (Microsoft.Silverlight.Testing.dll & Microsoft.VisualStudio.QualityTools.UnitTesting.Silverlight.dll) invented by Jeff Wilcox, and a unit test class library (Controls.Testing.Common.dll) invented by Ted Glaza. You can find lots of helpful information about Silverlight Unit Test Framework from its MSDN Code Gallery Site and Jeff's blog. This post mostly talks about Ted's unit test class library released in the Silverlight Toolkit, and demonstrates with examples how our unit tests are built on top of that framework.

Below is a simplified class diagram, showing some of the classes in Controls.Testing.Common project, and the overall design pattern of our unit tests:

Silverlight Toolkit Unit Test Class Diagram

Figure 1: Controls.Testing.Common Class Diagram

Parallel Class Hierarchies

It is pretty clear from above class diagram that there are parallel class hierarchies:

  1. FrameworkElement <- Control <- ContentControl
  2. FrameworkElementTest <- ControlTest <- ContentControlTest
  3. IOverriddenFrameworkElement <- IOverriddenControl <- IOverriddenContentControl

The first hierarchy is the inheritance chain of control classes being unit tested.

The second hierarchy is the inheritance chain of corresponding unit test classes, paralleling the control classes being tested. The reason for this design is that if ContentControl is a Control, then ContentControlTest should test everything ControlTest does. There are some common patterns in test class design. Assuming XXX inherits from YYY:

  • XXXTest inherits from YYYTest. If XXXTest isn't abstract, it is marked with [TestClass] attribute.

        [TestClass
        public partial class ExpanderTest : HeaderedContentControlTest 
        {

  • XXXTest introduces three new properties: DefaultXXXToTest, XXXsToTest, OverriddenXXXsToTest, and use them to implement the override of the three properties (DefaultYYYToTest, YYYsToTest, OverriddenYYYsToTest) introduced by its base class YYYTest:

            #region HeaderedContentControls to test 
            /// <summary> 
            /// Gets a default instance of HeaderedContentControl (or a derived type) to test. 
            /// </summary> 
            public override HeaderedContentControl DefaultHeaderedContentControlToTest 
           
                get 
               
                    return DefaultExpanderToTest; 
               
           

           
    /// <summary> 
            /// Gets instances of HeaderedContentControl (or derived types) to test. 
            /// </summary> 
            public override IEnumerable<HeaderedContentControl> HeaderedContentControlsToTest 
           
                get 
               
                    return ExpandersToTest.OfType<HeaderedContentControl>(); 
               
           
     
            /// <summary> 
            /// Gets instances of IOverriddenContentControl (or derived types) to test. 
            /// </summary> 
            public override IEnumerable<IOverriddenHeaderedContentControl> OverriddenHeaderedContentControlsToTest 
           
                get 
               
                    return OverriddenExpandersToTest.OfType<IOverriddenHeaderedContentControl>(); 
               
           
            #endregion HeaderedContentControls to test 
     
            #region Expanders to test 
            /// <summary> 
            /// Gets a default instance of Expander (or a derived type) to test. 
            /// </summary> 
            public virtual Expander DefaultExpanderToTest 
           
                get 
               
                    return new Expander(); 
               
           

             /// <summary> 
            /// Gets instances of Expander (or derived types) to test. 
            /// </summary> 
            public virtual IEnumerable<Expander> ExpandersToTest 
           
                get 
               
                    yield return DefaultExpanderToTest; 
     
                    for (int i = 0; i < 4; i++) 
                   
                        Expander expander = new Expander 
                       
                            ExpandDirection = (ExpandDirection)i, 
                            IsExpanded = (i % 2 == 0) 
                        }; 
                        yield return expander; 
                   
               
           

            /// <summary> 
            /// Gets instances of IOverriddenContentControl (or derived types) to test. 
            /// </summary> 
            public virtual IEnumerable<IOverriddenExpander> OverriddenExpandersToTest 
           
                get 
               
                    yield break
               
           
            #endregion Expanders to test

  • XXXTest has a public constructor, and overrides GetDependencyPropertyTest method:

            /// <summary> 
            /// Get the dependency property tests. 
            /// </summary> 
            /// <returns>The dependency property tests.</returns> 
            public override IEnumerable<DependencyPropertyTestMethod> GetDependencyPropertyTests() 
           
                IList<DependencyPropertyTestMethod> tests = TagInherited(base.GetDependencyPropertyTests());

  • XXXTest may overrides TemplatePartsAreDefined and TemplateVisualStateAreDefined methods, if XXX has new control contract defined, or modifies its ancestors' contract contract:

            #region Control contract 
            /// <summary> 
            /// Verifies the Control's TemplateParts. 
            /// </summary> 
            [TestMethod
            [Description("Verifies the Control's TemplateParts.")] 
            public override void TemplatePartsAreDefined() 
           
                IDictionary<string, Type> templateParts = DefaultControlToTest.GetType().GetTemplateParts(); 
                Assert.AreEqual(1, templateParts.Count); 
                Assert.AreSame(typeof(ToggleButton), templateParts["ExpanderButton"]); 
           

            /// <summary> 
            /// Verify the control's template visual states. 
            /// </summary> 
            [TestMethod
            [Description("Verify the control's template visual states.")] 
            public override void TemplateVisualStatesAreDefined() 
           
                IDictionary<string, string> visualStates = DefaultControlToTest.GetType().GetVisualStates(); 

                Assert.AreEqual(12, visualStates.Count); 

                Assert.AreEqual<string>("CommonStates", visualStates["Normal"]); 
                Assert.AreEqual<string>("CommonStates", visualStates["MouseOver"]); 
                Assert.AreEqual<string>("CommonStates", visualStates["Pressed"]); 
                Assert.AreEqual<string>("CommonStates", visualStates["Disabled"]);  

                Assert.AreEqual<string>("FocusStates", visualStates["Focused"]); 
                Assert.AreEqual<string>("FocusStates", visualStates["Unfocused"]);     

                Assert.AreEqual<string>("ExpansionStates", visualStates["Expanded"]); 
                Assert.AreEqual<string>("ExpansionStates", visualStates["Collapsed"]);  

                Assert.AreEqual<string>("ExpandDirectionStates", visualStates["ExpandDown"]); 
                Assert.AreEqual<string>("ExpandDirectionStates", visualStates["ExpandUp"]); 
                Assert.AreEqual<string>("ExpandDirectionStates", visualStates["ExpandLeft"]); 
                Assert.AreEqual<string>("ExpandDirectionStates", visualStates["ExpandRight"]); 
           
            #endregion Control contract

    Please note that although control contracts, annotated with [TemplateVisualState()] and [TemplatePart()] attributes, are not inherited via class hierarchy in theory. In reality they usually are, through subclass re-declaring base class's control contract. So our unit test classes treat control contract as inherited.

 

The third hierarchy is for UI and event testing:

    /// <summary> 
    /// Interface used to test virtual members of Expander. 
    /// </summary> 
    public interface IOverriddenExpander : IOverriddenHeaderedContentControl 
   
        /// <summary> 
        /// Gets the OnExpanded test actions. 
        /// </summary> 
        OverriddenMethod ExpandedActions { get; }  

        /// <summary> 
        /// Gets the OnCollapsed test actions. 
        /// </summary> 
        OverriddenMethod CollapsedActions { get; } 
    }

There is actually a fourth parallel class hierarchy that is usually used with the third hierarchy together:

  1. OverriddenFrameworkElement <- OverriddenControl <- OverriddenContentControl

It is not yet implemented in Controls.Testing.Common, but Controls.Testing has OverriddenTreeView class, which is a good example to show what above classes would look like and how they would be used, if implemented. I may write a separate post for the two overridden class hierarchies, or modify this one to add more coverage about them.

TestBase 

TestBase

Figure 2: TestBase

public abstract class TestBase : SilverlightTest
{
// Fields
[CompilerGenerated]
private static int <DefaultVisualDelayInMilliseconds>k__BackingField;

// Methods
static TestBase();
protected TestBase();
protected void EnqueueVisualDelay(int visualDelay);
protected internal void TestAsync(FrameworkElement element, params Action[] actions);
protected internal void TestAsync(int visualDelay, FrameworkElement element, params Action[] actions);
protected internal void TestSequenceAsync<T>(IEnumerable<T> elements, params Action<T>[] actions) where T: FrameworkElement;
protected internal void TestSequenceAsync<T>(int visualDelay, IEnumerable<T> elements, params Action<T>[] actions) where T: FrameworkElement;
protected internal void TestTaskAsync(FrameworkElement element, params Action[] actions);
protected internal void TestTaskAsync(int visualDelay, FrameworkElement element, params Action[] actions);

// Properties
protected internal static int DefaultVisualDelayInMilliseconds { [CompilerGenerated] get; [CompilerGenerated] set; }
}

TestBase wraps WorkItemTest methods like EnqueueCallback, EnqueueConditional, EnqueueSleep, EnqueueTestcomplete, and provide two high level utility functions TestAsync and TestSequenceAsync. Each function has an overload that takes a delay in milliseconds, to give visual tree some time to render in between test actions.

Dependent Property Unit Test

Another clear pattern from figure 1: Control.Testing.Common Class Diagram is that all test classes use DependencyPropertyTest<T,P> generic class to implementation unit test for dependency properties they introduce. Adding unit test for dependency property PPP of class XXX usually includes three steps:

  1. Define property "DependencyPropertyTest<T,P> PPPProperty" in class XXXTest:

            /// <summary> 
            /// Gets ExpandDirection dependency property test. 
            /// </summary> 
            protected DependencyPropertyTest<Expander, ExpandDirection> ExpandDirectionProperty { get; private set; }

  2. Create PPPProperty in XXXTest's constructor:

                ExpandDirectionProperty = new DependencyPropertyTest<Expander, ExpandDirection>(this, "ExpandDirection"
               
                    Property = Expander.ExpandDirectionProperty, 
                    Initializer = initializer, 
                    DefaultValue = ExpandDirection.Down, 
                    OtherValues = new ExpandDirection[] { ExpandDirection.Up, ExpandDirection.Left, ExpandDirection.Right }, 
                    InvalidValues = new Dictionary<ExpandDirection, Type
                   
                        { (ExpandDirection)(-1), typeof(ArgumentException) }, 
                        { (ExpandDirection)4, typeof(ArgumentException) }, 
                        { (ExpandDirection)5, typeof(ArgumentException) }, 
                        { (ExpandDirection)500, typeof(ArgumentException) }, 
                        { (ExpandDirection)int.MaxValue, typeof(ArgumentException) }, 
                        { (ExpandDirection)int.MinValue, typeof(ArgumentException) } 
                   
                };

  3. Add appropriate tests for this dependency property in GetDependencyPropertyTests override:

                // ExpandDirectionProperty tests 
                tests.Add(ExpandDirectionProperty.CheckDefaultValueTest); 
                tests.Add(ExpandDirectionProperty.ChangeClrSetterTest); 
                tests.Add(ExpandDirectionProperty.ChangeSetValueTest); 
                tests.Add(ExpandDirectionProperty.ClearValueResetsDefaultTest); 
                tests.Add(ExpandDirectionProperty.InvalidValueFailsTest); 
                tests.Add(ExpandDirectionProperty.InvalidValueIsIgnoredTest); 
                tests.Add(ExpandDirectionProperty.CanBeStyledTest); 
                tests.Add(ExpandDirectionProperty.TemplateBindTest); 
                tests.Add(ExpandDirectionProperty.ChangesVisualStateTest(ExpandDirection.Down, ExpandDirection.Up, "ExpandUp")); 
                tests.Add(ExpandDirectionProperty.ChangesVisualStateTest(ExpandDirection.Up, ExpandDirection.Left, "ExpandLeft")); 
                tests.Add(ExpandDirectionProperty.ChangesVisualStateTest(ExpandDirection.Left, ExpandDirection.Right, "ExpandRight")); 
                tests.Add(ExpandDirectionProperty.ChangesVisualStateTest(ExpandDirection.Right, ExpandDirection.Down, "ExpandDown")); 
                tests.Add(ExpandDirectionProperty.SetXamlAttributeTest); 
                tests.Add(ExpandDirectionProperty.SetXamlElementTest);

    You can also add/remove/change tests for dependency properties inherited from base classes in GetDependencyPropertyTests override:

                tests.RemoveTests(HeaderProperty.TemplateBindTest); 
                tests.Add(HeaderProperty.TemplateBindTest.Bug("TODO: Investigate why this fails here but not for the Content property."));  

The post is already longer than I expected, so I will stop here for now. Hope this help you understand our unit test code and create quality software.