If you've worked through the long form already, try the
short form (instructions only).
*****
Source code for this kata published to
github.
The goal of this article is to take you through the process of creating a Windows Phone 7 app using Test-Driven Development, designing each feature of the app through a series of incremental tests. The concept is loosely modeled on the
TDD Calculator kata as explained by Roy Osherove, although introducing an entire framework, with as many steps as required in the instructions below, moves outside the boundaries of a properly focused kata to what might more properly be called a tutorial. However, my goal is that these steps could be repeatedly practised, like a kata, to develop a consistent approach to TDD using the Windows Phone 7 unit testing framework.
To be able to test effectively in the presentation layer, we want to use the appropriate pattern. The pattern for XAML frameworks (WPF, Silverlight, and now Windows Phone 7) is the
Model-View-ViewModel (MVVM) design pattern, in which the ViewModel is isolated from the presentation layer and therefore testable (much like the controller in the MVC pattern, or presenter in the MVP pattern.) The difference here is that the passing of data back and forth between ViewModel and View is done by the framework itself, using a data binding implementation. (This will get much clearer as you work through the tests.)
The initial tests will build the ViewModel itself, ensuring that it has all the pieces its need (implemented interfaces, constructor parameters, and so on) to perform its role in the MVVM pattern. Items not directly part of the SUT (system under test) will be mocked in.
NOTE Like many presentation layer patterns, we will want to use mock objects to substitute classes that are not directly part of the system under test (in this case, the ViewModel.) However, Windows Phone 7 runs on the .NET Compact Framework, which does not support full mocking frameworks such as Rhino Mocks and Moq. Therefore, in this kata we will create fakes (custom mock classes) to verify the repository queries have been called. As per the terminology used by
Gerard Meszaros (see
Mocks, Fakes, Stubs and Dummies) perhaps the best term to describe what we are building for our custom mock is a "Test Spy".
So let's get started.
Pre-Requisite: Install Windows Phone 7 Unit Testing Framework
To unit test a Windows Phone 7 application, as pre-requisite you must install and configure the Windows Phone 7-specific version of the Silverlight unit testing framework (in which test results are displayed directly in the Winodws Phone emulator screen.) NUnit and other xUnit testing frameworks are not currently compatible with Silverlight and Windows Phone 7.
To install:
1) Download/install Visual Studio 2010 Express for Windows Phone from
here.
2) Download the zip file containing the 2 unsigned DLLs from
Jeff Wilcox's website.
3) Unzip the 2 DLLs. Then, right-click each DLL individually and select "Properties".
4) In the Properties window for each DLL, click the "Unblock" button. (DLLs off the net are blocked for safety by default.)
5) Launch Visual Studio 2010 Express for Windows Phone.
6) Create a new solution where the project type is "Windows Phone Application"
7) In the Windows Phone Application project, add References for the 2 DLLs that you have just unblocked to this Windows project:
- Microsoft.Silverlight.Testing
- Microsoft.VisualStudio.QualityTools.UnitTesting.Silverlight
8) DON'T create any separate class library projects. For now, the unit testing framework requires that all tests/supporting classes be INSIDE the Windows project.
9) DO create a sub-folder in the Windows project, and give it a name to imply this is the application code (it will display in the namespace path). (examples: "AppCode" or "Support")
10) Finally, for the test framework to recognize your tests, you must add code to the code-behind class for the MainPage XAML file.
11) Open the file MainPage.xaml.cs
12) Add using statements to the Silverlight Unit Testing Framework DLLs you referenced earlier:
using Microsoft.Phone.Controls;
using Microsoft.Silverlight.Testing;
13) FINALLY, add the following lines of code (thanks to Jeff Wilcox for this) at the end of the constructor, just below the SupportedOrientations assignment. Note that we have declared a boolean constant (runUnitTests) which we've assigned to true, and then wrapped the code which initializes the unit testing framework into a conditional block based on this constant. With this constant set to true, the Windows Phone emulator will display the test results from the test runner. Later, when we want to view the phone app itself, we'll be setting this constant to false.
const bool runUnitTests = true;
if (runUnitTests)
{
Content = UnitTestSystem.CreateTestPage();
IMobileTestPage imtp = Content as IMobileTestPage;
if (imtp != null)
{
BackKeyPress += (x, xe) => xe.Cancel = imtp.NavigateBack();
}
}
14) Proceed to the TDD kata.
TDD Kata Setup
Within the application code sub-folder (created in the pre-requisites above), create the following sub-folders to function as namespaces: Domain, Repository, ViewModel, and Tests.Unit.
When completed, your SolutionExplorer should look like this:
Test #1 (ViewModel injects corresponding domain entity)
1) In the Tests.Unit sub-folder, create CustomerViewModelTests.cs class.
2) Add the following using statements:
using Microsoft.Silverlight.Testing.UnitTesting;
using Microsoft.VisualStudio.TestTools.UnitTesting;
3) Add the [TestClass] attribute to class. (Silverlight Unit Testing Framework is built on top of the MSTest framework, hence the naming convention for the test attributes is the same.)
4) The first test will requite a customer repository, so declare a private instance variable of non-existent type ICustomerRepository.
private ICustomerRepository _customerRepository;
5) Use the
Visual Studio 2010 Generate from Usage feature to create the interface:
- click into the type name, press Alt-Shift-F10
- select "Generate new type...'"
- In the Generate New Type dialog (see above), be sure to change the definition to interface, and then click OK.
- In SolutionExplorer, select the ICustomerRepository class that has been generated in the Tests.Unit folder and use cut-and-paste to MOVE it into the Repository folder
- In the Repository folder, double-click to open ICustomerRepository and fix its namespace declaration to be for the Repository folder.
- Return to the test class; where the type reference is now broken
- click on the type ICustomerRepository and press Alt-Shift-F10 to add a proper reference to the Repository namespace.
6) Create a test setup method.
[TestInitialize]
public void SetUp()
{
}
7) In the setup method, instantiate the _customerRepository interface as a new class, FakeCustomerRepository. Use Alt-Shft-F10 (Generate By Usage) to create this class. In the class signature, add ICustomerRepository as its implemented interface. Now move the class (cut-and-paste it within SolutionExplorer to move it to the Repository folder. Don't forget to edit its namespace entry to match the Repository.)
[TestInitialize]
public void SetUp()
{
_customerRepository = new FakeCustomerRepository();
}
8) Create a test for CustomerViewModel, in which _customerRepository and a new instance of Customer are passed as parameters to the constructor. Since Customer is a new class; use Alt-Shft-F10 (Generate By Usage) to create the class, and to initialize FirstName and LastName properties. In Solution Explorer, cut-and-paste to move this class to the Domain sub-folder, then edit its namespace declaration to match.
[TestMethod]
public void Constructor_CustomerAndRepositoryInput_InstantiatesSuccessfully()
{
var customer = new Customer { FirstName = "June", LastName = "Wong" };
var sut = new CustomerViewModel(_customerRepository, customer);
Assert.IsNotNull(sut);
}
NOTE It is common in tests to keep track of which class is the "System Under Test" by naming the class variable "sut". The supplied code snippets will use this convention.
9) You are ready for your first test run using the Windows Phone 7 unit test framework. Right-click on the solution, and click "Deploy Solution". The test should pass, displaying as follows:
NOTE The items listed in the test results can be drilled-down into for more detail. Click the item to drill-down, click the back arrow icon at the bottom of the phone emulator to go back.
Test #2 (ViewModel property values match domain property values)
10) The correct implementation of the MVVM pattern is the ViewModel "wraps" the model (i.e the domain.) Therefore a domain entity, Customer, should have it's properties exposed, but not replicated, through the ViewModel. Therefore, create test for CustomerViewModel which verifies that:
* Customer.FirstName is wrapped by CustomerViewModel.FirstName
* Customer.LastName is wrapped by CustomerViewModel.LastName
[TestMethod]
public void Constructor_CustomerAndRepositoryInput_CustomerPropertiesEqualViewModelProperties()
{
var customer = new Customer { FirstName = "June", LastName = "Wong" };
var sut = new CustomerViewModel(_customerRepository, customer);
Assert.AreEqual(customer.FirstName, sut.FirstName);
Assert.AreEqual(customer.LastName, sut.LastName);
}
CustomerViewModel properties
public string FirstName
{
get
{
return _customer.FirstName;
}
set
{
_customer.FirstName = value;
}
}
public string LastName
{
get
{
return _customer.LastName;
}
set
{
_customer.LastName = value;
}
}
Test #3 (ViewModel.VerifyPropertyName(string property))
11) Create test for CustomerViewModel which verifies that the VerifyPropertyName(string property) throws a custom exception if the property is not part of the model.
[TestMethod]
[ExpectedException(typeof(VerifyPropertyNameException))]
public void VerifyPropertyNameMethod_NonExistentPropertyString_ThrowsException()
{
var customer = new Customer() { FirstName = "June", LastName = "Smith" };
var sut = new CustomerViewModel(_customerRepository, customer);
sut.VerifyPropertyName("NonExistentPropertyName");
}
corresponding method
public void VerifyPropertyName(string propertyName)
{
foreach(var propertyInfo in this.GetType().GetProperties())
{
if(propertyName == propertyInfo.Name)
{
return;
}
}
throw new VerifyPropertyNameException();
}
corresponding method, refactored to LINQ
public void VerifyPropertyName(string propertyName)
{
if (this.GetType().GetProperties().Any(propertyInfo => propertyInfo.Name.Equals(propertyName)))
{
return;
}
throw new VerifyPropertyNameException();
}
Test #4 (ViewModel implements INotifyPropertyChanged)
12) Create test for CustomerViewModel which verifies that CustomerViewModel is (of type) INotifyPropertyChanged.
[TestMethod]
public void Constructor_CustomerInput_ImplementsINotifyPropertyChanged()
{
var customer = new Customer() { FirstName = "June", LastName = "Smith" };
var customerViewModel = new CustomerViewModel(customer);
Assert.IsInstanceOfType(customerViewModel, typeof(INotifyPropertyChanged));
}
Test #5 (ViewModel.PropertyChanged Event fires when ViewModel property setters called)
13) Create test for CustomerViewModel which verifies that after FirstName/LastName properties have been changed, that the PropertyChanged event has fired.
[TestMethod]
public void ClassProperties_WhenSet_PropertyChangedEventFires()
{
var customer = new Customer();
var sut = new CustomerViewModel(_customerRepository, customer);
var receivedEvents = new List();
sut.PropertyChanged += ((sender, e) => receivedEvents.Add(e.PropertyName));
sut.FirstName = "Sabrina";
Assert.AreEqual(1, receivedEvents.Count);
Assert.AreEqual("FirstName", receivedEvents[0]);
sut.LastName = "Moore";
Assert.AreEqual(2, receivedEvents.Count);
Assert.AreEqual("LastName", receivedEvents[1]);
}
event raising implementation
protected virtual void OnPropertyChanged(string propertyName)
{
this.VerifyPropertyName(propertyName);
var handler = this.PropertyChanged;
if (handler == null) return;
var e = new PropertyChangedEventArgs(propertyName);
handler(this, e);
}
example of call to OnPropertyChanged within a property setter
public string FirstName
{
get
{
return _customer.FirstName;
}
set
{
_customer.FirstName = value;
OnPropertyChanged("FirstName");
}
}
Test #6 (Domain entity properties stay in sync when ViewModel property setters called)
14) Create test for CustomerViewModel which verifies that after FirstName/LastName properties have been changed, that the Customer domain entity properties still match (i.e. have stayed in sync.).
[TestMethod]
public void DomainProperties_ReassignedValues_ViewModelPropertiesTrackChanges()
{
var customer = new Customer() { FirstName = "June", LastName = "Smith" };
var sut = new CustomerViewModel(_customerRepository, customer);
Assert.AreEqual(customer.FirstName, sut.FirstName);
Assert.AreEqual(customer.LastName, sut.LastName);
customer.FirstName = "New First Name";
customer.LastName = "New Last Name";
Assert.AreEqual(customer.FirstName, sut.FirstName);
Assert.AreEqual(customer.LastName, sut.LastName);
}
15) Refactoring: Move the interface, the PropertyChanged event, and all related methods into a superclass, ViewModelBase. Ensure that all tests are still passing.
public abstract class ViewModelBase : INotifyPropertyChanged
{
public void VerifyPropertyName(string propertyName)
{
foreach(var propertyInfo in this.GetType().GetProperties())
{
if(propertyName == propertyInfo.Name)
{
return;
}
}
throw new VerifyPropertyNameException();
}
public virtual event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
this.VerifyPropertyName(propertyName);
var handler = this.PropertyChanged;
if (handler == null) return;
var e = new PropertyChangedEventArgs(propertyName);
handler(this, e);
}
}
With all 6 tests passing, the Windows Phone emulator should display the tests as follows:
You are now ready to add event-handling to the ViewModel, by way of command classes.
Test #7 (CustomerSaveCommand implements ICommand)
16) Create CustomerSaveCommandTests.cs class
17) Add the following using statements:
using Microsoft.Silverlight.Testing.UnitTesting;
using Microsoft.VisualStudio.TestTools.UnitTesting;
18) You will need the same FakeCustomerRepository instance for your tests, so copy the instance variable and the test setup method from the CustomerViewModelTests class.
private ICustomerRepository _customerRepository;
[TestInitialize]
public void SetUp()
{
_customerRepository = new FakeCustomerRepository();
}
19) Create test for CustomerSaveCommand which passes the _customerRepository instance, and a new Customer instance, as parameters to the constructor, then verifies that CustomerSaveCommand is (of type) ICommand.
[TestMethod]
public void Constructor_CustomerRepositoryInput_ImplementsICommand()
{
var customer = new Customer() { FirstName = "June", LastName = "Smith" };
var sut = new CustomerSaveCommand(_customerRepository, customer);
Assert.IsInstanceOfType(sut, typeof(ICommand));
}
Test #8 (CustomerViewModel.CustomerSaveCommand.CanExecute returns true when Customer is valid, false when Customer is not)
You will now attach CustomerSaveCommand as a property of the CustomerViewModel class, so the remaining tests will be written against CustomerViewModel.
20) Return to the CustomerViewModelTests.cs class.
21) Create a test for the CustomerViewModel.CustomerSaveCommand property which verifies that sut.CustomerSaveCommand.CanExecute(object param) is true.
[TestMethod]
public void CustomerSaveCommandPropertyCanExecute_ValidCustomer_ReturnsTrue()
{
var customer = new Customer() { FirstName = "June", LastName = "Smith" };
var sut = new CustomerViewModel(_customerRepository, customer);
Assert.IsTrue(sut.CustomerSaveCommand.CanExecute(null));
}
22) Refactor the CustomerSaveCommand class so that the condition under which CanExecute(object param) is true is that the properties of the _customer instance all have saveable values (they aren't null or empty.)
public bool CanExecute(object parameter)
{
return !string.IsNullOrEmpty(_customer.FirstName)
&& !string.IsNullOrEmpty(_customer.LastName);
}
23) Now test the reverse, where passing a Customer instance with empty FirstName/LastName properties to CustomerSaveCommand.CanExecute(object parameter) returns false.
[TestMethod]
public void CustomerSaveCommandPropertyCanExecute_InvalidCustomer_ReturnsTrue()
{
var customer = new Customer() { FirstName = "", LastName = "" };
var sut = new CustomerViewModel(_customerRepository, customer);
Assert.IsFalse(sut.CustomerSaveCommand.CanExecute(null));
}
Test #9 (CustomerViewModel.CustomerSaveCommand.Execute saves the Customer record)
20) Create test for CustomerViewModel.CustomerSaveCommand.Execute() method which verifies that the _customerRepository.SaveCustomer() method had been called.
NOTE Since we can't use a mocking framework such as RhinoMocks in .NET Compact Framework, we will add a test-only property to ICustomerRepository called SaveMethodWasCalled; set to false in the repository constructor, and set to true in the ICustomerRepository.SaveCustomer(Customer customer) method.
[TestMethod]
public void CustomerSaveCommandPropertyExecute_ValidCustomer_RepositoryValidatesCallOccured()
{
var customer = new Customer() { FirstName = "June", LastName = "Smith" };
var sut = new CustomerViewModel(_customerRepository, customer);
sut.CustomerSaveCommand.Execute(null);
Assert.IsTrue(_customerRepository.SaveCommandCalled);
}
CustomerSaveCommand.Execute() code
public void Execute(object parameter)
{
if (this.CanExecute(null))
{
_customerRepository.SaveCustomer(_customer);
}
}
FakeCustomerRepository.SaveCustomer() method code
public void SaveCustomer(Customer customer)
{
// call to not-yet-created WCF client web method
_saveCommandCalled = true;
}
22) In Solution Explorer, right-click on the solution and choose "Deploy Solution". All 10 tests should be passing. Click on either test class in the Win Phone 7 emulator window to view the complete list of tests.
With all tests passing, you are ready to proceed to the final step - hooking up the ViewModel to the XAML presentation layer.
[Postscript - Building the XAML presentation layer mapped to ViewModel]
23) In Solution Explorer, double-click MainPage.xaml to open it. Adjust the split view between the design and XAML views so that you can easily edit the XAML.
24) In the <phoneNavigation> element, after the xmlns:x attribute, type xmlns:vm= and Intellisense will appear with a list of optional namespaces containing the viewmodel. Press the down arrow to select the ViewModel namespace (as shown below), and then press Enter.
25) Your viewmodel namespace reference should now look like this:
25) Next, you must associate the DataContext of MainPage.xaml to the specific view model class for this page, which is CustomerViewModel.cs. This can be achieved declaratively in the <UserControl.Resources> element of the XAML, but only when your view model has a parameterless constructor. For CustomerViewModel, we will need to associate the DataContext in the code-behind class. To do this, go to the first test you created in CustomerViewModelTests, and copy the instantiation code. Paste it at the end of the MainPage constructor. You'll need to use Alt-Shft-F10 to resolve various references to your classes, and add an instantiation to FakeCustomerRepository (This could be switched out for a "real" repository class later, eg. one built as a wrapper for a web service proxy addressing your web service layer.). When you are done, the data context assignment code should look like this:
ICustomerRepository customerRepository = new FakeCustomerRepository();
var customer = new Customer();
var customerViewModel = new CustomerViewModel(customerRepository, customer);
this.DataContext = customerViewModel;
26) Drag 2 TextBox controls with labels (i.e. TextBlock controls) and a Button onto the design surface. Apply names and pretty-fication until it looks something like this:
27) Add the following attribute to the corresponding XML element for each TextBox (where Path equals the property name from the ViewModel)
for txtFirstName control
Text="{Binding Path=FirstName, Mode=TwoWay}"
for txtLastName control
Text="{Binding Path=LastName, Mode=TwoWay}"
28) You are now ready to add binding to the "SaveCustomer" button.
NOTE At this point, in WPF, the Button's Command property could be set to the data binding for the SaveCustomerCommand class. However, the Command property doesn't exist in Silverlight. This issue has been addressed in a
blog entry by Patrick Cauldwell. In the steps below, his ButtonService class is used to provide support for a custom Command binding attribute.
29) Download Patrick Cauldwell's
code zip (located at bottom of his blog entry.)
30) Extract the code zip, and locate the ButtonService.cs class.
31) Add a new folder/namespace to your existing folders, named "ThirdParty". Drag and drop ButtonService.cs into this folder.
32) Open the ButtonService.cs class, and change the namespace to match the path of your folder.
NOTE 2010Mar27 As of this writing, there is a TYPO in the ButtonService.cs class. In the initial static registration of the CommandParameterProperty, the 2nd parameter (see code snippet below) is set to typeof(string). This will cause a null error later, in the event raising method, when attempting to cast the command instance to ICommand (because it incorrectly being passed as string.) Therefore, CHANGE the 2nd parameter to be typeof(ICommand).
public static readonly DependencyProperty CommandParameterProperty =
DependencyProperty.RegisterAttached("CommandParameter", typeof(string), typeof(ButtonService),
new PropertyMetadata(OnCommandParameterChanged));
33) In MainPage.xaml, you need to add an alias to this namespace (similar to the xmlns:vm namespace you created earlier.) eg. "thirdParty". Add this directly below the xmlns:vm namespace reference.
xmlns:thirdParty="clr-namespace:CustomerApp.Support.ThirdParty"
34) Add a new attribute to the <Button> element to provide data binding to the CustomerSaveCommand.
NOTE 2010Mar27 Path must be bound to the ViewModel's property name, NOT the property type (if they are different; for example, if the property were CustomerViewModel.Save of property type CustomerSaveCommand, then the correct binding path would be "Save").
thirdParty:ButtonService.Command="{Binding Path=CustomerSaveCommand}"
35) You now need to turn off your tests to run your application. BEFORE you do that, run your tests one more time and make sure that all tests are passing.
36) With all tests passing, let's run the application now instead of the tests. To do this, go back to the MainPage.xaml.cs and switch the boolean constant (runUnitTests) to false:
const bool runUnitTests = false;
37) In the FakeCustomerRepository.cs, set a breakpoint on the line of code within the SaveCustomer() method.
38) Finally, launch the application. Enter values into the FirstName and LastName fields, and press the SaveButton.
49) Observe that the breakpoint is hit, the Customer is being passed correctly to the Customer repository's Save command, and the value of the Customer FirstName and LastName properties match the values you entered into the TextBox controls in the XAML layer.
Your Win Phone 7 application using MVVM and unit tests is now complete.