I’ve written an overly simplified example in an attempt to demonstrate the problem without all the baggage of the original application I was working on. So I’ve got a text box that shows a number and a button that you can click to increment the number. You could also just type a new number in the text box. I haven’t added any of the validation code that should be here. I’m trying to stay bare bones and focus on unit testing WPF controls.
Here are the UI parts
<Window x:Class="WPFSample.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="WPFSample" Height="343" Width="400">
<Grid>
<Button Height="23" Margin="153,0,164,103" Name="incrementButton" VerticalAlignment="Bottom" Click="IncrementClicked">Increment</Button>
<TextBox Height="26" Margin="144,125,148,0" Name="valueTextBox" VerticalAlignment="Top" Text="{Binding Path=Number}"></TextBox>
</Grid>
</Window>
public partial class Window1 : System.Windows.Window
{
NumberHolder holder = new NumberHolder();
public Window1()
{
InitializeComponent();
DataContext = holder;
}
private void IncrementClicked(object sender, RoutedEventArgs args)
{
holder.Number++;
}
}
A NumberHolder is simply just
public class NumberHolder : INotifyPropertyChanged
{
private int number = 0;
public int Number
{
get { return number; }
set
{
number = value;
FirePropertyChanged("Number");
}
}
private void FirePropertyChanged(string name)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(name));
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
What I want to do is check what value is in the text box, click the button and then assert that the value is now one greater than it was. If this were Windows Forms I probably would just make the textbox and button public and call them from the test. Sure it breaks encapsulation but in this case it’s just me so I’m not that worried about it. Well it turns out that I’ve got less control over such things in WPF than I had in Windows Forms. I can’t set the visibility of the button or text box object as easily as I could before and it is defaulting to internal.
I need another way to get to the controls. We’ve faced this sort of problem for years. WPF gives us a new tool to try and use. Each control exposes something called an Automation Peer. This is normally used by the new UI Automation framework so that accessibility applications or testing applications can see and manipulate your controls. Sounds just like what I’m looking for.
Getting a peer from a control is easy. Assuming you have access to the control which, of course, is one of the problems I’m trying to solve. But I can still get to the peers if I can get to a parent. Since I’m unit testing I’m going to be creating an instance of the container (in my case a Window) so I have that. WindowAutomationPeer takes an instance of Window in its constructor so we’re all set there. Once I have the WindowAutomationPeer I can ask it for its children.
[Test]
public void ClickTheButton()
{
Window1 window = new Window1();
WindowAutomationPeer windowPeer = new WindowAutomationPeer(window);
List<AutomationPeer> children = windowPeer.GetChildren();
}
In this case GetChildren won’t return anything. The window only has children once it’s shown. Since I’m trying to unit test this class I’d rather just instantiate it and call some methods on it and be done. Looks like that isn’t going to work out yet. If I were smart I’d drop it and find another way. But I’m stubborn and I want to see where this goes. So I’ll throw in a window.Show(); call. If you’ve ever tried anything like this you’re probably thinking, “That will cause a threading issue any minute now.” Probably right but I’m going to plod on anyway.
I’m going to cheat now and rely on the fact that the children are in an expected order. I wouldn’t do that in a production app but for now it is good enough to keep going
TextBoxAutomationPeer textBoxPeer = (TextBoxAutomationPeer)children[0];
ButtonAutomationPeer buttonPeer = (ButtonAutomationPeer)children[1];
The first thing I want to do is to see the value of the text box before I do anything. I’ve got the textBoxPeer so I can do that but it isn’t obvious how to do it. Peer’s can tell you a lot about a control but they don’t let you interact with the control. Providers let you interact with controls. I could argue that checking the value of a control is not interacting with it but I didn’t write the framework so it wouldn’t get me too far. To see the value in the text box I need an IValueProvider. It turns out that TextBoxAutomationPeer is an IValueProvider but it uses explicit interface implementation so we have to cast. Finally I’ve hit an assert.
Assert.AreEqual("0", ((IValueProvider)textBoxPeer).Value);
That == green bar == happiness. But that’s boring so I’m not happy for long. Next I want to click the button and I run into the same provider vs. peer problem but with a similar solution. I need an IInvokeProvider and the button peer explicitly implements that.
((IInvokeProvider)buttonPeer).Invoke();
Assert.AreEqual("1", ((IValueProvider)textBoxPeer).Value);
That fails. The Invoke call doesn’t throw an exception but the value in the textbox is still 0. I double checked the app and clicking the button does work. So that means we are probably running into my other favorite part of testing UIs, threading. Normally that window would live in its own thread, but I’ve got it riding along in the same thread as my test. That probably leads to deep unhappiness. Cross thread communications in unit tests isn’t the easiest thing to deal with. I might end up there but for now I’d like to try something else.
AutomationPeers have an Owner property that will let you get at the actual UIElement, in this case the button. Once I can get my hands on the button I can get it to raise the click event like this
Button button = (Button) buttonPeer.Owner;
RoutedEventArgs args = new RoutedEventArgs(Button.ClickEvent, button);
button.RaiseEvent(args);
If I squeeze that in between the two asserts replacing the call to Invoke I get a passing test.
So far peers have helped but not as much as I might like. A peer can let me get at a control without having to publicly expose the control. The biggest downside is that I seem to have to show the window to get things to work. I also want to make sure I close the window when I’m done so a bit of refactoring leads us to this test fixture.
[TestFixture]
public class Window1Tests
{
private ButtonAutomationPeer buttonPeer;
private TextBoxAutomationPeer textBoxPeer;
private Window1 window;
private WindowAutomationPeer windowPeer;
[SetUp]
public void SetUp()
{
window = new Window1();
window.Show();
windowPeer = new WindowAutomationPeer(window);
List<AutomationPeer> children = windowPeer.GetChildren();
buttonPeer = (ButtonAutomationPeer)children[0];
textBoxPeer = (TextBoxAutomationPeer)children[1];
}
[Test]
public void ClickingIncrementIncrementsTheNumberInTheTextBox()
{
Assert.AreEqual("0", ((IValueProvider)textBoxPeer).Value);
Button button = (Button) buttonPeer.Owner;
RoutedEventArgs args = new RoutedEventArgs(Button.ClickEvent, button);
button.RaiseEvent(args);
Assert.AreEqual("1", ((IValueProvider)textBoxPeer).Value);
}
[TearDown]
public void TearDown()
{
window.Close();
}
}
Putting the call to Close in the TearDown makes sure that it will get closed even if the test fails.