The Silverlight testing framework was recently released and shows some great potential for being
a first class application platform. For more information about the test framework see this post from
Jeff Wilcox.
To start off I choose to use a code sample that had some complexity in it. Brad Abrams just posted
a Silverlight walk End-to-End Data Centric Application with Silverlight 2. This seemed like a good
sample to use, since the post did not consider how to test the code.
Step 1: Add a test project and test class.
After the installing the assemblies and project templates, add a new unit test project to the solution.
Change the test class to inherit from SilverlightTest.
The following code demonstrates a simple integration test.
This test does the following:
Instantiates the page.
Clears the local storage.
Adds the page to the Test framework TestSurface.
Enters the letter s into the text box.
Clicks the search button.
Verifies that 9 products are displayed in the DataGrid.
Removes the page from the TestSurface.
Here is the code for the test class:
1: [TestClass]
2: public class The_data_page_should_load:SilverlightTest
3: {
4:
5: [TestMethod]
6: public void When_searching_for_products_starting_with_s_nine_products_should_be_displayed()
7: {
8: EndToEndSilverlightDemo.Page pageUnderTest = new EndToEndSilverlightDemo.Page();
9: IPageTestDriver testDriver = pageUnderTest;
10: testDriver.ClearLocalStorage();
11: this.Silverlight.TestSurface.Children.Add(pageUnderTest);
12: testDriver.TypeSearchPrefix("s");
13: testDriver.ClickSearchButton();
14: Assert.AreEqual(9,testDriver.DisplayedProductRows);
15: this.Silverlight.TestSurface.Children.Remove(pageUnderTest);
16: }
17: }
Here is the Test Driver interface:
1: public interface IPageTestDriver
2: {
3: void TypeSearchPrefix(string searchPrefix);
4: void ClickSearchButton();
5: int DisplayedProductRows { get; }
6: void ClearLocalStorage();
7: bool WebserviceHasReturnedData();
8: }
Here is the portions of the Page class that implement the IPageTestDriver interface. It is a good idea to
hide the complexity of the Page classes internal controls from the test class. This is equivalent to
creating a Test Fixture in other frameworks.
1: void IPageTestDriver.TypeSearchPrefix(string searchPrefix)
2: {
3: this.txtProductString.Text = searchPrefix;
4: }
5:
6: void IPageTestDriver.ClickSearchButton()
7: {
8: this.Button_Click(this.btnOne,null);
9: }
10:
11: int IPageTestDriver.DisplayedProductRows
12: {
13: get { return (new List<object>((IEnumerable<object>) this.dataGridResults.ItemsSource)).Count; }
14: }
15:
16: void IPageTestDriver.ClearLocalStorage()
17: {
18: settings.Clear();
19: settings.Save();
20: dataGridResults.ItemsSource = null;
21: }
22:
23: bool IPageTestDriver.WebserviceHasReturnedData()
24: {
25: return dataGridResults.ItemsSource != null;
26: }
27: }
Now run the test and everything is good right? Not so. But why is that?
The test has failed because the application is making an Asynchronous call to the web service. This means
our test runs and completes before the remote call to return data can complete and load the data into the data grid.
Solution: Use the Asynchronous features of the test framework.
The framework provides the functionality to be able to drive unit tests in an async fashion. This allows the
test to run and wait for a condition to be met before proceeding. In this case on line 10 the EnqueueConditional
call waits until the helper method returns true before the test proceeds with the next call.
1: [TestMethod,Asynchronous]
2: public void When_searching_for_products_starting_with_s_nine_products_should_be_displayed_async()
3: {
4: EndToEndSilverlightDemo.Page pageUnderTest = new EndToEndSilverlightDemo.Page();
5: IPageTestDriver testDriver = pageUnderTest;
6: testDriver.ClearLocalStorage();
7: this.Silverlight.TestSurface.Children.Add(pageUnderTest);
8: EnqueueCallback(() => testDriver.TypeSearchPrefix("s"));
9: EnqueueCallback(() => testDriver.ClickSearchButton());
10: EnqueueConditional(testDriver.WebserviceHasReturnedData);
11: EnqueueCallback(() => Assert.AreEqual(9, testDriver.DisplayedProductRows));
12: EnqueueCallback(() => this.Silverlight.TestSurface.Children.Remove(pageUnderTest));
13: EnqueueTestComplete();
14: }
And the results?....
The test passes and verifies the full remote call to the web service as well as getting the data back into the user interface.
Pretty cool? This sample demonstrates that it is possible to test drive the user interface. The code to do so is far from
being readable and comprehendible. The next post in this series will address the readability of the testing code and put a
fluent interface around the ugly Enqueue method calls.
The source code for this sample is available here; http://erichexter.googlecode.com/svn/trunk/EndToEndSilverlightDemo/EndToEndSilverlightDemo/