I've been playing with the March 2007 CTP of Orcas and the associated VSTS goodness that it contains. It did something a bit funny to me the other day that wasted an hour or two of poking around when the solution ended up being simple but not obvious.
I started out with a simple class library and then went to add a test project. I'm an NUnit guy but I frequently use the unit testing in VSTS. Of course the first thing you have to do with any unit testing project is add a reference to the assembly you want to test. When I did this I got a warning telling me that the assembly that was being referenced (the class library) was being built for a newer version of the framework than the project that was referencing it (the test project). That seemed bad.
I was trying to write some unit tests around LINQ and just couldn't get the test project to compile when I added any 3.5 code to the test project. I know that in the project properties there is a way to set the target framework, but when I went to the property pages for the project there was an exception. It's a CTP so no big deal, but I really want to write some unit tests around what I'm doing.
On a lark I closed visual studio and opened it again. When I opened it the project conversion wizard popped up telling me the test project was created with an earlier version of Visual Studio. It wasn't, but this is a CTP so no real worries. Once the project conversion wizard ran all was good.
Long story short, if you are adding a test project in the ctp, close and re-open the solution and let the project conversion wizard run before you go too far.
Thursday, March 15, 2007
Tuesday, March 06, 2007
Unit Testing WPF controls with Automation Peers
Recently I ran into a situation where I wanted to be able to test some data binding code in a WPF application. I am trying to be careful not to test the data binding framework but I also need to make sure that the way I am talking to the framework is right. That discussion could fill up a few (dozen) blog posts anyway so I’ll stay away from it for now.
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
A NumberHolder is simply just
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.
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
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.
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.
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
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.
Putting the call to Close in the TearDown makes sure that it will get closed even if the test fails.
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.
Subscribe to:
Posts (Atom)