Saturday, September 20, 2008

How to test your XAML Behavior using unit tests

With WPF and Silverlight a whole new area opens up for application development. Using XAML it is so much easier to create something ‘Cool’ than was possible before with ‘normal’ Windows Forms. Another even more important thing is the separation of the GUI and the application code; this makes testing more easily because you have a clear cut line between your GUI and code. Not to mention much more maintainable and extendable. There are many more good reasons why you might want to use WPF or Silverlight instead of the previous technologies, but going back to the ‘Cool’ part. Now It is also very possible that I as a developer create an extremely simple GUI just to verify some needed functionality and then move on to the actual code. In the meantime we send the XAML file to our design department and they start working on actually making it ‘Cool’, because be honest; no matter how easy they make it for me, I will never be able to create a cool graphical design ;) So also for this example I shamelessly took something that I think looks cool from Gøran Hansen from the MSDN Live presentations he is keeping in Norway.

Anyway below here is a screenshot of a Window that Gøran Hansen created, I only converted it to a UserControl and changed the behavior to be in the XAML definition without external data binding.

XAML UserControl


I just noticed that images are not provided to the Google Reader so if you are missing these just go to the original post.

So the XAML file gets returned to me by one of our graphical designers and now I have to check that all the previous existing functionality is still there and working. So what I really want to be able to do is replace my original file with the new one and have some tests run to verify that all my functionality still is valid. I want unit tests for my XAML Behavior.

XAML UserControl Unit tests


XAML Behavior, didn’t you just say that the GUI is now separated from the application code, so it should also be separated from any behavior right? Well no not exactly, we can still define some logic in the XAML definition, quite a lot actually. If this is something we want is another discussion, let’s assume because it is possible it will be used ;) So I did define some basic behavior in my XAML definition, namely: • I wanted the Save button to be disabled until both the Web Dashboard URL and the Poll interval where filled in. • I wanted to only be able to select a successful build sound file when the user check that option, the same for the broken build sound file. So this behavior is independent of any code or other logic than is defined in the XAML definition. Ok I admit I wrote a small convertor that is used in the XAML definition, but that is more an XAML extension.

So when I received the new design from our graphics department it looked like this:

XAML UserControl


Ok this didn’t actually go to them ;) this is me being creative, but in the process of making these huge changes to the design I broke some of the previous behavior, behavior that I am counting on to work. For example I don’t want users to click the Save button before they filled out the needed information? So how do I know I broke these behaviors? Well look at the following screenshot, isn’t that a clear picture?

XAML UserControl Unit tests


So finally I get to the interesting parts, how did I get Unit testing for my XAML Behavior? For this I wrote a really simple helper class that is responsible for loading and parsing the XAML. This helper class is then used in the unit testing framework of your choice; I used NUnit in this example.

Class: XamlUnitTestHelper
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
using System;
using System.IO;
using System.Text.RegularExpressions;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Markup;
using System.Windows.Threading;

namespace Fohjin.XamlBehaviorVerifier
{
    /// <summary>
    /// This is a static helper class to use with any unit testing framework.
    /// </summary>
    public static class XamlUnitTestHelper
    {
        private static ParserContext _XamlContext;
        private static Viewbox _Viewbox;

        /// <summary>
        /// Loads the xaml into a Viewbox so that we can parse it and verify its working.
        /// </summary>
        /// <param name="xamlFilePath">The xaml file path.</param>
        public static void LoadXaml(string xamlFilePath)
        {
            String xamlFileStream = File.ReadAllText(xamlFilePath);
            if (xamlFileStream.IndexOf("x:Class=") != -1)
                xamlFileStream = Regex.Replace(xamlFileStream, "x:Class=\\\".+([^\\\"])\\\"", "");

            object obj = XamlReader.Load(new MemoryStream(new System.Text.ASCIIEncoding().GetBytes(xamlFileStream)), XamlContext);
            _Viewbox = new Viewbox
            {
                Child = ((UIElement)obj)
            };
            _Viewbox.UpdateLayout();
            FlushDispatcher();
        }

        /// <summary>
        /// Gets the object.
        /// </summary>
        /// <typeparam name="T">This is the type of control that is being searched for</typeparam>
        /// <param name="name">The name of the control that is being searched for</param>
        /// <returns>If the control is found a reference to this control is returned else null</returns>
        public static T GetObject<T>(string name) where T : class
        {
            if (_Viewbox != null && _Viewbox.Child != null)
            {
                FrameworkElement child = _Viewbox.Child as FrameworkElement;
                if (child != null)
                {
                    return child.FindName(name) as T;
                }
            }
            return null;
        }

        /// <summary>
        /// Gets the xaml context, to be used by the XamlReader.
        /// </summary>
        /// <value>The xaml context.</value>
        private static ParserContext XamlContext
        {
            get
            {
                if (_XamlContext == null)
                {
                    _XamlContext = new ParserContext();
                    _XamlContext.XmlnsDictionary.Add("", "http://schemas.microsoft.com/winfx/2006/xaml/presentation");
                }
                return _XamlContext;
            }
        }
        /// <summary>
        /// Flushes the dispatcher, needed to get data binding working when the control is not actually rendered to screen
        /// </summary>
        private static void FlushDispatcher()
        {
            FlushDispatcher(Dispatcher.CurrentDispatcher);
        }
        /// <summary>
        /// Flushes the dispatcher, needed to get data binding working when the control is not actually rendered to screen
        /// </summary>
        private static void FlushDispatcher(Dispatcher ctx)
        {
            FlushDispatcher(ctx, DispatcherPriority.SystemIdle);
        }
        /// <summary>
        /// Flushes the dispatcher, needed to get data binding working when the control is not actually rendered to screen
        /// </summary>
        private static void FlushDispatcher(Dispatcher ctx, DispatcherPriority priority)
        {
            ctx.Invoke(priority, new DispatcherOperationCallback(delegate { return null; }), null);
        }
    }
}

As you can see it is really simple and straightforward, the FlushDispatcher is something I need for Data Binding this is I discussed in a previous post Saving a WPF Xaml FlowDocument to XPS with working DataBinding. I am not using Data Binding in this example, but in a next post I’ll see if I can get that to work also.

Class: SettingsUserControlTests
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
using System.Windows.Controls;
using Fohjin.XamlBehaviorVerifier;
using NUnit.Framework;

namespace WpfControlLibrary.Test
{
    [TestFixture]
    public class SettingsUserControlTests
    {
        private const string _XamlFilePath = @"C:\Projects\Fohjin.XamlTest\WpfControlLibrary\SettingsUserControl1.xaml";
        //private const string _XamlFilePath = @"C:\Projects\Fohjin.XamlTest\WpfControlLibrary\SettingsUserControl2.xaml";

        private const string _SoundsCheckBox = "soundsCheckBox";
        private const string _SoundsCheckBoxNotFound = "Could not find CheckBox control '" + _SoundsCheckBox + "'";
        private const string _SoundsCheckBoxIssue = "Issue with a value of '" + _SoundsCheckBox + "'";

        private const string _BrokenCheckBox = "brokenCheckBox";
        private const string _BrokenCheckBoxNotFound = "Could not find CheckBox control '" + _BrokenCheckBox + "'";
        private const string _BrokenCheckBoxIssue = "Issue with a value of '" + _BrokenCheckBox + "'";

        private const string _BtnOnSuccessFileDialog = "btnOnSuccessFileDialog";
        private const string _BtnOnSuccessFileDialogNotFound = "Could not find Button control '" + _BtnOnSuccessFileDialog + "'";
        private const string _BtnOnSuccessFileDialogIssue = "Issue with a value of '" + _BtnOnSuccessFileDialog + "'";
        
        private const string _BtnOnBrokenFileDialog = "btnOnBokenFileDialog";
        private const string _BtnOnBrokenFileDialogNotFound = "Could not find Button control '" + _BtnOnBrokenFileDialog + "'";
        private const string _BtnOnBrokenFileDialogIssue = "Issue with a value of '" + _BtnOnBrokenFileDialog + "'";

        private const string _TxtWebDashboardUrl = "txtWebDashboardUrl";
        private const string _TxtWebDashboardUrlNotFound = "Could not find TextBox control '" + _TxtWebDashboardUrl + "'";
        //private const string _TxtWebDashboardUrlIssue = "Issue with a value of '" + _TxtWebDashboardUrl + "'";

        private const string _TxtPollInterval = "txtPollInterval";
        private const string _TxtPollIntervalNotFound = "Could not find TextBox control '" + _TxtPollInterval + "'";
        //private const string _TxtPollIntervalIssue = "Issue with a value of '" + _TxtPollInterval + "'";

        private const string _BtnSave = "btnSave";
        private const string _BtnSaveNotFound = "Could not find Button control '" + _BtnSave + "'";
        private const string _BtnSaveIssue = "Issue with a value of '" + _BtnSave + "'";

        [Test]
        public void VerifyThatbtnOnSuccessFileDialogIsDisabled()
        {
            XamlUnitTestHelper.LoadXaml(_XamlFilePath);
            Button button = XamlUnitTestHelper.GetObject<Button>(_BtnOnSuccessFileDialog);
            Assert.IsNotNull(button, _BtnOnSuccessFileDialogNotFound);
            Assert.IsFalse(button.IsEnabled, _BtnOnSuccessFileDialogIssue);
        }
        [Test]
        public void VerifyThatbtnOnSuccessFileDialogIsEnabled()
        {
            XamlUnitTestHelper.LoadXaml(_XamlFilePath);
            CheckBox checkBox = XamlUnitTestHelper.GetObject<CheckBox>(_SoundsCheckBox);
            Button button = XamlUnitTestHelper.GetObject<Button>(_BtnOnSuccessFileDialog);

            Assert.IsNotNull(button, _BtnOnSuccessFileDialogNotFound);
            Assert.IsNotNull(checkBox, _SoundsCheckBoxNotFound);

            checkBox.IsChecked = true;
            Assert.IsTrue(button.IsEnabled, _BtnOnSuccessFileDialogIssue);
        }
        [Test]
        public void VerifyThatbtnOnSuccessFileDialogIsDisabledAgain()
        {
            XamlUnitTestHelper.LoadXaml(_XamlFilePath);
            CheckBox checkBox = XamlUnitTestHelper.GetObject<CheckBox>(_SoundsCheckBox);
            Button button = XamlUnitTestHelper.GetObject<Button>(_BtnOnSuccessFileDialog);

            Assert.IsNotNull(button, _BtnOnSuccessFileDialogNotFound);
            Assert.IsNotNull(checkBox, _SoundsCheckBoxNotFound);

            Assert.IsFalse(checkBox.IsChecked.Value, _SoundsCheckBoxIssue);
            Assert.IsFalse(button.IsEnabled, _BtnOnSuccessFileDialogIssue);

            checkBox.IsChecked = true;
            Assert.IsTrue(checkBox.IsChecked.Value, _SoundsCheckBoxIssue);
            Assert.IsTrue(button.IsEnabled, _BtnOnSuccessFileDialogIssue);

            checkBox.IsChecked = false;
            Assert.IsFalse(checkBox.IsChecked.Value, _SoundsCheckBoxIssue);
            Assert.IsFalse(button.IsEnabled, _BtnOnSuccessFileDialogIssue);
        }

        [Test]
        public void VerifyThatbtnOnBrokenFileDialogIsDisabled()
        {
            XamlUnitTestHelper.LoadXaml(_XamlFilePath);
            Button button = XamlUnitTestHelper.GetObject<Button>(_BtnOnBrokenFileDialog);
            Assert.IsNotNull(button, _BtnOnBrokenFileDialogNotFound);
            Assert.IsFalse(button.IsEnabled, _BtnOnBrokenFileDialogIssue);
        }
        [Test]
        public void VerifyThatbtnOnBrokenFileDialogIsEnabled()
        {
            XamlUnitTestHelper.LoadXaml(_XamlFilePath);
            CheckBox checkBox = XamlUnitTestHelper.GetObject<CheckBox>(_BrokenCheckBox);
            Button button = XamlUnitTestHelper.GetObject<Button>(_BtnOnBrokenFileDialog);

            Assert.IsNotNull(button, _BtnOnBrokenFileDialogNotFound);
            Assert.IsNotNull(checkBox, _BrokenCheckBoxNotFound);

            checkBox.IsChecked = true;
            Assert.IsTrue(button.IsEnabled, _BtnOnBrokenFileDialogIssue);
        }
        [Test]
        public void VerifyThatbtnOnBrokenFileDialogIsDisabledAgain()
        {
            XamlUnitTestHelper.LoadXaml(_XamlFilePath);
            CheckBox checkBox = XamlUnitTestHelper.GetObject<CheckBox>(_BrokenCheckBox);
            Button button = XamlUnitTestHelper.GetObject<Button>(_BtnOnBrokenFileDialog);

            Assert.IsNotNull(button, _BtnOnBrokenFileDialogNotFound);
            Assert.IsNotNull(checkBox, _BrokenCheckBoxNotFound);

            Assert.IsFalse(checkBox.IsChecked.Value, _BrokenCheckBoxIssue);
            Assert.IsFalse(button.IsEnabled, _BtnOnBrokenFileDialogIssue);

            checkBox.IsChecked = true;
            Assert.IsTrue(checkBox.IsChecked.Value, _BrokenCheckBoxIssue);
            Assert.IsTrue(button.IsEnabled, _BtnOnBrokenFileDialogIssue);

            checkBox.IsChecked = false;
            Assert.IsFalse(checkBox.IsChecked.Value, _BrokenCheckBoxIssue);
            Assert.IsFalse(button.IsEnabled, _BtnOnBrokenFileDialogIssue);
        }

        [Test]
        public void VerifyThatbtnSaveIsDisabled()
        {
            XamlUnitTestHelper.LoadXaml(_XamlFilePath);
            Button button = XamlUnitTestHelper.GetObject<Button>(_BtnSave);
            Assert.IsNotNull(button, _BtnSaveNotFound);
            Assert.IsFalse(button.IsEnabled, _BtnSaveIssue);
        }
        [Test]
        public void VerifyThatbtnSaveIsDisabled1()
        {
            XamlUnitTestHelper.LoadXaml(_XamlFilePath);
            Button button = XamlUnitTestHelper.GetObject<Button>(_BtnSave);
            TextBox txtWebDashboardUrl = XamlUnitTestHelper.GetObject<TextBox>(_TxtWebDashboardUrl);
            TextBox txtPollInterval = XamlUnitTestHelper.GetObject<TextBox>(_TxtPollInterval);
            Assert.IsNotNull(button, _BtnSaveNotFound);
            Assert.IsNotNull(txtWebDashboardUrl, _TxtWebDashboardUrlNotFound);
            Assert.IsNotNull(txtPollInterval, _TxtPollIntervalNotFound);

            txtWebDashboardUrl.Text = "test text";
            txtPollInterval.Text = "";
            Assert.IsFalse(button.IsEnabled, _BtnSaveIssue);
        }
        [Test]
        public void VerifyThatbtnSaveIsDisabled2()
        {
            XamlUnitTestHelper.LoadXaml(_XamlFilePath);
            Button button = XamlUnitTestHelper.GetObject<Button>(_BtnSave);
            TextBox txtWebDashboardUrl = XamlUnitTestHelper.GetObject<TextBox>(_TxtWebDashboardUrl);
            TextBox txtPollInterval = XamlUnitTestHelper.GetObject<TextBox>(_TxtPollInterval);
            Assert.IsNotNull(button, _BtnSaveNotFound);
            Assert.IsNotNull(txtWebDashboardUrl, _TxtWebDashboardUrlNotFound);
            Assert.IsNotNull(txtPollInterval, _TxtPollIntervalNotFound);

            txtWebDashboardUrl.Text = "";
            txtPollInterval.Text = "10";
            Assert.IsFalse(button.IsEnabled, _BtnSaveIssue);
        }
        [Test]
        public void VerifyThatbtnSaveIsEnabled()
        {
            XamlUnitTestHelper.LoadXaml(_XamlFilePath);
            Button button = XamlUnitTestHelper.GetObject<Button>(_BtnSave);
            TextBox txtWebDashboardUrl = XamlUnitTestHelper.GetObject<TextBox>(_TxtWebDashboardUrl);
            TextBox txtPollInterval = XamlUnitTestHelper.GetObject<TextBox>(_TxtPollInterval);
            Assert.IsNotNull(button, _BtnSaveNotFound);
            Assert.IsNotNull(txtWebDashboardUrl, _TxtWebDashboardUrlNotFound);
            Assert.IsNotNull(txtPollInterval, _TxtPollIntervalNotFound);

            txtWebDashboardUrl.Text = "test text";
            txtPollInterval.Text = "10";
            Assert.IsTrue(button.IsEnabled, _BtnSaveIssue);
        }
        [Test]
        public void VerifyThatbtnOnBrokenFileDialogIsDisabledAgain1()
        {
            XamlUnitTestHelper.LoadXaml(_XamlFilePath);
            Button button = XamlUnitTestHelper.GetObject<Button>(_BtnSave);
            TextBox txtWebDashboardUrl = XamlUnitTestHelper.GetObject<TextBox>(_TxtWebDashboardUrl);
            TextBox txtPollInterval = XamlUnitTestHelper.GetObject<TextBox>(_TxtPollInterval);
            Assert.IsNotNull(button, _BtnSaveNotFound);
            Assert.IsNotNull(txtWebDashboardUrl, _TxtWebDashboardUrlNotFound);
            Assert.IsNotNull(txtPollInterval, _TxtPollIntervalNotFound);

            txtWebDashboardUrl.Text = "test text";
            txtPollInterval.Text = "10";
            Assert.IsTrue(button.IsEnabled, _BtnSaveIssue);

            txtWebDashboardUrl.Text = "";
            Assert.IsFalse(button.IsEnabled, _BtnSaveIssue);
        }
        [Test]
        public void VerifyThatbtnOnBrokenFileDialogIsDisabledAgain2()
        {
            XamlUnitTestHelper.LoadXaml(_XamlFilePath);
            Button button = XamlUnitTestHelper.GetObject<Button>(_BtnSave);
            TextBox txtWebDashboardUrl = XamlUnitTestHelper.GetObject<TextBox>(_TxtWebDashboardUrl);
            TextBox txtPollInterval = XamlUnitTestHelper.GetObject<TextBox>(_TxtPollInterval);
            Assert.IsNotNull(button, _BtnSaveNotFound);
            Assert.IsNotNull(txtWebDashboardUrl, _TxtWebDashboardUrlNotFound);
            Assert.IsNotNull(txtPollInterval, _TxtPollIntervalNotFound);

            txtWebDashboardUrl.Text = "test text";
            txtPollInterval.Text = "10";
            Assert.IsTrue(button.IsEnabled, _BtnSaveIssue);

            txtPollInterval.Text = "";
            Assert.IsFalse(button.IsEnabled, _BtnSaveIssue);
        }
    }
}

Pretty cool and simple, right? Well you need to make sure that your unit tests are running in a ApartmentState.STA thread. When you are using NUnit you only need to add a configuration file to your test library with the following content:

App.Config file
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
 <configSections>
  <sectionGroup name="NUnit">
   <section name="TestRunner" type="System.Configuration.NameValueSectionHandler"></section>
  </sectionGroup>
 </configSections>

 <NUnit>
  <TestRunner>
   <add key="ApartmentState" value="STA"></add>
  </TestRunner>
 </NUnit>
</configuration>
I intend to continue working on this a bit more too see how far I can take this without it becoming too big for unit testing. I hope you can also see the value in these types of tests. You can download the full solution from here: http://downloads.fohjin.com/Fohjin.XamlBehaviorTester.rar

4 comments:

Anonymous said...

This is a great post that high lights an interesting approach to testing.

Once you get DataBinding working this becomes really useful.

The only criticism I have is that this testing approach depends heavily on named UI elements. I struggle to use as few named elements as possible. There is a couple of reasons for that:

1) You don't really need many of those any more. You use commands instead of events, and data binding in stead of "pushing" values to/from your controls

2)Depending on allot of named elements gives the UX designer less flexibility to change control types, or totally rearrange the UI.

But that being said you can't get around them completely, for instance if using triggers or animations Blend will name the elements.

Looking forward to your next post including data binding.

Mark Nijhof said...

Hi Jonas,

I totally agree with you that having to rely on names is not a ideal solution, but as you said I don't think there is a way around this, since I want to be able to test the logic that resides in the XAML itself.

Except maybe trying to get the controls by their binding, or their command name. Those are (have to be) known anyway. So than the individual controls don't need names, but I'll have to figure out if it is possible.

So instead of having to provide the names of the two text boxes you provide their data binding and the same with the save button, but than the command that is attached to it to check if the correct behavior is achieved.

Anyway give me a couple days to figure this out ;)

Anonymous said...

Hi Mark
Great post! It’s very good that you have taken your ideas about testing the views (xaml) and put them into practice.

I agree with Jonas regarding the dependency on the names of the GUI components.

I think you are on to something regarding the inspection of binding and commands on the GUI components. After you have Instantiated View based on the XAML file, you can traverse the logical three, and inspect the Binding property of every UI component. Then use the Binding information to validate if the View connects to .NET object correctly. By correctly I mean; you validate that the GUI Component binds to a Property that exist on the Object. You can also check if the GUI component has configured TwoWay databinding. If so, check if the Type that it’s binding to has implemented the INotifyPropertyChanged interface.

My experience from using the Presentation Model pattern and WPF is very good. The errors that occasionally occurs is when I refactoring the Presentation Models and rename properties. Then I have no tool to validate that the Views connects correctly to the Presentation models.

Keep up the good work. I might use your tool in the Continuous Integration Monitor project:)

Cheers

Gøran

Mark Nijhof said...

Gøran,

Verifying the Bindings and Commands that they match the object I bind to is the next step. It would be valuable to see if the XAML has more or less bindings and/or commands then the object it binds to.

But I believe that in some cases I'll keep using the text based search I presented in this post. For example if I have an Advanced option in my form that just makes some form elements visible, this is not related to any binding or command, but it is still GUI logic that I like to verify. In that case I'll probably don't have a way around this.

P.s. use extend change as much as you want (anyone for that matter), if you made improvements I'd like to hear about it.

Thanks guys for your comments :)