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> |
