Saturday, September 13, 2014

WPF Unit Testing without Automation

Here's a simpler approach to WPF unit testing that doesn't use the complex and long-winded Automation classes. I simply modify the element properties directly and use RaiseEvent to simulate button presses. Then I look at the results to determine if the test worked or not.

In this example I created a window that will accept two numbers and add them together. I test that the numbers got added correctly and also that error handling correctly detected invalid numbers.

Let's start with the main window, it ain't pretty ...

<Window x:Class="Automation.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="MainWindow" Height="350" Width="525"> <StackPanel Orientation="Horizontal" Height="20"> <TextBox Name="Operand1TextBox" Width="50"/> <TextBlock Text=" + "/> <TextBox Name="Operand2TextBox" Width="50"/> <TextBlock Text=" = "/> <TextBlock Name="ResultTextBlock" Width="50"/> <Button Name="ComputeButton" Content="Compute" Click="ComputeButton_Click"/> <TextBlock Name="ErrorTextBlock" Foreground="Red" Width="200"/> <Button Name="TestButton" Content="Test" Click="TestButton_Click"/> </StackPanel> </Window>
----------------------------------------------------------------------------------------
using System.Windows; namespace Automation { public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); } private void ComputeButton_Click(object sender, RoutedEventArgs e) { decimal Dec1 = 0; decimal Dec2 = 0; decimal Result = 0; ResultTextBlock.Text = ""; ErrorTextBlock.Text = ""; if (!(decimal.TryParse(Operand1TextBox.Text, out Dec1))) { ErrorTextBlock.Text = "Operand 1 is not a number"; return; } if (!(decimal.TryParse(Operand2TextBox.Text, out Dec2))) { ErrorTextBlock.Text = "Operand 2 is not a number"; return; } Result = Dec1 + Dec2; // Change this line when you want to break functionality ResultTextBlock.Text = Result.ToString(); } private void TestButton_Click(object sender, RoutedEventArgs e) { Window oTest = new AutomationTests(); oTest.Owner = this; oTest.ShowDialog(); } } }
The [Test] button will popup the AutomationTests window as a dialog with the MainWindow as it's owner. That allows the test window to get easy access to the window it is testing. Here's the XAML and code for the test window, it's even uglier...

<Window x:Class="Automation.AutomationTests" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="AutomationTests" Height="300" Width="300"> <StackPanel Orientation="Vertical"> <Button Name="IntegerTest" Click="IntegerTest_Click" Content="Integer Test"/> <Button Name="DecimalTest" Click="DecimalTest_Click" Content="Decimal Test"/> <Button Name="Error1Test" Click="Error1Test_Click" Content="Error1 Test"/> <Button Name="Error2TTest" Click="Error2Test_Click" Content="Error2 Test"/> <TextBlock Name="ResultTextBlock"/> </StackPanel> </Window>
-----------------------------------------------------------------------------------------
using System.Windows; using System.Windows.Controls; namespace Automation { public partial class AutomationTests : Window { public AutomationTests() { InitializeComponent(); } private void IntegerTest_Click(object sender, RoutedEventArgs e) { MainWindow oMain = (MainWindow)Owner; oMain.Operand1TextBox.Text = "100"; oMain.Operand2TextBox.Text = "200"; oMain.ComputeButton.RaiseEvent(new RoutedEventArgs(Button.ClickEvent, oMain.ComputeButton)); if (oMain.ResultTextBlock.Text == "300") ResultTextBlock.Text = "100 + 200 = 300 (Correct)"; else ResultTextBlock.Text = "Incorrect result, expected 300"; } private void DecimalTest_Click(object sender, RoutedEventArgs e) { MainWindow oMain = (MainWindow)Owner; oMain.Operand1TextBox.Text = "10.5"; oMain.Operand2TextBox.Text = "12.4"; oMain.ComputeButton.RaiseEvent(new RoutedEventArgs(Button.ClickEvent, oMain.ComputeButton)); if (oMain.ResultTextBlock.Text == "22.9") ResultTextBlock.Text = "10.5 + 12.4 = 22.9 (Correct)"; else ResultTextBlock.Text = "Incorrect result, expected 22.9"; } private void Error1Test_Click(object sender, RoutedEventArgs e) { MainWindow oMain = (MainWindow)Owner; oMain.Operand1TextBox.Text = "A"; oMain.Operand2TextBox.Text = "200"; oMain.ComputeButton.RaiseEvent(new RoutedEventArgs(Button.ClickEvent, oMain.ComputeButton)); if (oMain.ResultTextBlock.Text == "") ResultTextBlock.Text = "A + 200 = Error (Correct)"; else ResultTextBlock.Text = "Incorrect result, expected error"; } private void Error2Test_Click(object sender, RoutedEventArgs e) { MainWindow oMain = (MainWindow)Owner; oMain.Operand1TextBox.Text = "100"; oMain.Operand2TextBox.Text = "B"; oMain.ComputeButton.RaiseEvent(new RoutedEventArgs(Button.ClickEvent, oMain.ComputeButton)); if (oMain.ResultTextBlock.Text == "") ResultTextBlock.Text = "100 + B = Error (Correct)"; else ResultTextBlock.Text = "Incorrect result, expected error"; } } }
As you can see, each of the four buttons on the test window manipulates the data on the main window and then clicks the [Compute] button and tests the result. It's very easy to set up test scenarios that have predictable results.

Click on each of the four test buttons. You can see that the results are correct.




Now, in the main window, replace the + with a * so that the results will be product instead of the sum. This simulates a developer breaking functionality. Perform the tests again. You can see the first two tests fail, but the second two test still work correctly.




No comments:

Post a Comment