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.
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 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:
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?
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.
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); } } } |
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); } } } |
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> |
4 comments:
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.
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 ;)
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
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 :)
Post a Comment