Introduction
This article presents an example on unit testing of Silverlight applications with asynchronous service callbacks.
Background
Compared with other types of unit tests, the tests of Silverlight applications face two special challenges:
- Silverlight applications run in the sandbox of the web browsers. Certain functionalities that are available to other types of applications may not be available to Silverlight applications. If we test a Silverlight application in the way that we test other types of applications, the Silverlight application may pass the unit test, but fails in the integration tests, system tests and even possibly in the production.
- If the Silverlight applications use any WCF/web services, the service calls will normally be asynchronous calls. The callback functions either partially or completely run in a different thread than the calling thread. Because of this, the test methods may finish earlier than the callback functions and thus produce wrong test results.
This article is to present an example on unit testing of Silverlight applications with asynchronous service callbacks to address these two challenges. For unit testing of Silverlight applications, you can find some other references, such as this one, this one, and this one.
The attached Visual Studio solution has three projects:
- The project "
SilverlightApplication
" is the Silverlight application to be tested. - The project "
SilverlightApplicationUnitTest
" is the test project. This test project itself is a special Silverlight application that is created to test the "SilverlightApplication
" project. - The "
SilverlightHostWebApplication
" is an ASP.NET web application. The "SilverlightApplication
" project is hosted in the "Default.aspx" page and the "SilverlightApplicationUnitTest
" project is hosted in the "TestPage.aspx" page. This project also exposes a WCF service to be consumed asynchronously by the "SilverlightApplication
".
This Visual Studio solution is developed in Visual Studio 2010 and Silverlight 4. I will first introduce the WCF service in the "SilverlightHostWebApplication
" project and then introduce the "SilverlightApplication
" project. After that, I will show you how the test project "SilverlightApplicationUnitTest
" is created and how we can use it to test the asynchronous WCF service calls made in the MVVM view model class in the "SilverlightApplication
" project.
The WCF Service
The WCF service to be consumed by the "SilverlightApplication
" project is implemented in the "StudentService.svc.cs" file in the "SilverlightHostWebApplication
" project:
using System;
using System.Collections.Generic;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.Text;
namespace SilverlightHostWebApplication
{
[DataContract]
public class Student
{
[DataMember(Order = 0)]
public string ID { get; set; }
[DataMember(Order = 1)]
public string Name { get; set; }
[DataMember(Order = 2)]
public DateTime EnrollmentDate { get; set; }
[DataMember(Order = 3)]
public int Score { get; set; }
[DataMember(Order = 4)]
public string Gender { get; set; }
}
[ServiceContract]
public class StudentService
{
[OperationContract]
public List<Student> GenerateStudents(int NoOfStudents)
{
return CreateStudentsList(NoOfStudents);
}
[OperationContract]
public List<Student> GenerateStudentsWithError(int NoOfStudents)
{
return CreateStudentsList(NoOfStudents - 1);
}
private List<Student> CreateStudentsList(int NoOfStudents)
{
List<Student> students = new List<Student>();
Random rd = new Random();
for (int i = 1; i <= NoOfStudents; i++)
{
int score = Convert.ToInt16(60 + rd.NextDouble() * 40);
students.Add(new Student()
{
ID = "ID No." + i.ToString(),
Name = "Student Name " + i.ToString(),
EnrollmentDate = DateTime.Now,
Score = score,
Gender = (score >= 80) ? "Female" : "Male"
});
}
return students;
}
}
}
This WCF service implements a Data contract "Student
" and a Service contract "StudentService
". In the service contract "StudentService
", we have two Operation contracts:
- The "
GenerateStudents
" method takes an integer input parameter "NoOfStudents
" and generates a List of "Student
" objects to send back to the callers. The number of the students in the generated list matches exactly with the input parameter "NoOfStudents
". - The "
GenerateStudentsWithError
" method does the same thing as the "GenerateStudents
" method with an artificial error. The number of the students generated is wrong and it does not match with the input parameter "NoOfStudents
".
Both operation contracts will be consumed by the "SilverlightApplication
" project by asynchronous WCF calls. We will see that the unit test project "SilverlightApplicationUnitTest
" passes the call to the "GenerateStudents
" method, but fails the call to the "GenerateStudentsWithError
" method.
The Silverlight Application
The "SilverlightApplication
" project is the Silverlight project to be tested in this article.
This simple application is built in MVVM pattern. If you are not familiar with MVVM, you can take a look at "Data and Command Bindings for Silverlight MVVM Applications" and some other references. Before going to the application's code, let us take a look at how it runs. Set the host ASP.NET application "SilverlightHostWebApplication
" as the start up project and set the "Default.aspx" page as the start page, we can lunch the "SilverlightApplication
" from Visual Studio.
We can see two buttons in this Silverlight application. Each button is intended to trigger a WCF service call to retrieve 10 students. But the button "Get 10 students from WCF" triggers a call to the method "GenerateStudents
" and the button "Get 10 students from wrong method in WCF" triggers a call to the method "GenerateStudentsWithError
".
As we can see from the above pictures, clicking on the "Get 10 students from WCF" button, we indeed receive the correct number of students from the WCF service. But when we click on the "Get 10 students from wrong method in WCF" button, incorrect number of students is returned from the WCF service. We will see how the unit test application captures this artificial error. But before we go to the unit test application, we will take a look at the code that implements the "SilverlightApplication
" project. The "SilverlightApplication
" project is a very simple Silverlight MVVM application, which has only one model class, one view model class and one XAML view file. I will introduce them one by one after taking a look at the utility classes that help us implementing Silverlight MVVM applications.
The MVVM Utility Classes
To make the implementation of the MVVM pattern in the "SilverlightApplication
" easier, I created two utility classes in the "Utilities\Utilities.cs" file:
using System;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
using System.ComponentModel;
namespace SilverlightApplication.Utilities
{
public delegate void AsyncCompleted();
public abstract class ViewModelBase
: DependencyObject, INotifyPropertyChanged
{
public event AsyncCompleted AsyncCallbackCompleted;
public event PropertyChangedEventHandler PropertyChanged;
public bool IsDesignTime
{
get { return DesignerProperties.IsInDesignTool; }
}
protected void NotifyPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
protected void InformCallbackCompleted()
{
if (AsyncCallbackCompleted != null)
{
AsyncCallbackCompleted();
}
}
}
public class RelayCommand : ICommand
{
private readonly Action handler;
private bool isEnabled;
public RelayCommand(Action handler)
{
this.handler = handler;
}
public bool IsEnabled
{
get { return isEnabled; }
set
{
if (value != isEnabled)
{
isEnabled = value;
if (CanExecuteChanged != null)
{
CanExecuteChanged(this, EventArgs.Empty);
}
}
}
}
public bool CanExecute(object parameter)
{
return IsEnabled;
}
public event EventHandler CanExecuteChanged;
public void Execute(object parameter)
{
handler();
}
}
}
The "ViewModelBase
" class implements the INotifyPropertyChanged interface. It will be used as the base class in the application's view model. The "RelayCommand
" class implements the ICommand interface. It will be used to create command objects in the application's view model. I wrote a separate article to talk about how these two classes help us to build Silverlight MVVM applications. If you are interested, you can take a look at it from here.
Besides the MVVM support, you should notice that I added an Event "AsyncCallbackCompleted
" and a public
method "InformCallbackCompleted
" to fire this event in the "ViewModelBase
" class. In order for the unit test application to test the asynchronous callback functions in a Silverlight application, the callback functions need to fire this event before they complete. You will see how we fire this event in the view model of the "SilverlightApplication
" application and how we use this event to test the callback functions in the unit test project.
The Model Class
The model class of the "SilverlightApplication
" project is implemented in the "Models\StudentModel.cs" file:
using System;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
using SilverlightApplication.StudentService;
namespace SilverlightApplication.Models
{
public class StudentModel
{
public void GetStudents(int NumberOfStudent,
EventHandler<GenerateStudentsCompletedEventArgs> targetFunction)
{
StudentServiceClient client = new StudentServiceClient();
client.GenerateStudentsCompleted
+= new EventHandler<GenerateStudentsCompletedEventArgs>(targetFunction);
client.GenerateStudentsAsync(NumberOfStudent);
}
public void GetStudentsWithError(int NumberOfStudent,
EventHandler<GenerateStudentsWithErrorCompletedEventArgs> targetFunction)
{
StudentServiceClient client = new StudentServiceClient();
client.GenerateStudentsWithErrorCompleted
+= new EventHandler<GenerateStudentsWithErrorCompletedEventArgs>
(targetFunction);
client.GenerateStudentsWithErrorAsync(NumberOfStudent);
}
}
}
Since all the student generation functionalities have been implemented in the WCF service, this model class is very simple. It has only two methods:
- The "
GetStudents
" method calls the correct method in the WCF service. - The "
GetStudentsWithError
" method calls the method in the WCF service that generates wrong number of students.
Each method takes a reference to a callback function. The callback functions will be called when the results of the WCF service calls come back. The callback functions will be implemented in the application's view model.
The View Model
The view model of the "SilverlightApplication
" project is implemented in the "ViewModels\MainPageViewModel.cs" file:
using System;
using SilverlightApplication.Utilities;
using SilverlightApplication.StudentService;
using System.Collections.Generic;
using SilverlightApplication.Models;
namespace SilverlightApplication.ViewModels
{
public class MainPageViewModel : ViewModelBase
{
private List<Student> studentList = null;
public List<Student> StudentList
{
get { return studentList; }
private set
{
studentList = value;
NotifyPropertyChanged("StudentList");
}
}
public RelayCommand GetStudentWCFCommand { get; private set; }
private void GetStudentWCF()
{
StudentModel model = new StudentModel();
model.GetStudents(10, (s, r) =>
{
StudentList = (List<Student>)r.Result;
InformCallbackCompleted();
});
}
public RelayCommand GetStudentWCFWrongMethodCommand { get; private set; }
private void GetStudentWCFWrongMethod()
{
StudentModel model = new StudentModel();
model.GetStudentsWithError(10, (s, r) =>
{
StudentList = (List<Student>)r.Result;
InformCallbackCompleted();
});
}
private void WireCommands()
{
GetStudentWCFCommand = new RelayCommand(GetStudentWCF);
GetStudentWCFCommand.IsEnabled = true;
GetStudentWCFWrongMethodCommand = new RelayCommand(GetStudentWCFWrongMethod);
GetStudentWCFWrongMethodCommand.IsEnabled = true;
}
public MainPageViewModel() : base()
{
WireCommands();
}
}
}
This view model has one public
property and two commands.
- The "
StudentList
" property is the list of students retrieved from the WCF service. - The "
GetStudentWCFCommand
" command initiates the call to the "CreateStudentsList
" method in the WCF service. - The "
GetStudentWCFWrongMethodCommand
" command initiates the call to the "GenerateStudentsWithError
" method in the WCF service.
You should pay some attention to the Anonymous callback functions. They assign the received list of students to the "StudentList
" property and fire the "AsyncCallbackCompleted
" event. By firing this event, the callback functions notify the test methods in the unit test project that the asynchronous service calls have completed and the result of the service calls is ready for the test methods to check.
The XAML View
The view of the "SilverlightApplication
" project is implemented in the "MainPage.xaml" file:
<UserControl xmlns:sdk="http://schemas.microsoft.com/winfx/2006/xaml/presentation/sdk"
x:Class="SilverlightApplication.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" FontFamily="Verdana" FontSize="12"
d:DesignHeight="300" d:DesignWidth="400">
<Grid x:Name="LayoutRoot" Margin="10" Background="White">
<Grid.RowDefinitions>
<RowDefinition Height="auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid Grid.Row="0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Button Grid.Column="0" Content="Get 10 students from WCF"
Command="{Binding Path=GetStudentWCFCommand}"
Margin="5,0,0,5"/>
<Button Grid.Column="1" Content="Get 10 students from wrong method in WCF"
Command="{Binding Path=GetStudentWCFWrongMethodCommand}"
Margin="5,0,0,5"/>
</Grid>
<sdk:DataGrid Margin="0, 8, 0, 0" Grid.Row="1"
ItemsSource="{Binding Path=StudentList}"/>
</Grid>
</UserControl>
The view model has three functional XAML components. The Data grid is bound to the "StudentList
" property and each of the two Buttons is bound to a command in the view model.
We now complete the introduction of the "SilverlightApplication
" project, and we are ready to take a look at the unit test application "SilverlightApplicationUnitTest
".
The Silverlight Test Application "SilverlightApplicationUnitTest"
As what we mentioned earlier, Silverlight applications run in the sandbox of the web browsers. Certain functionalities that are available to other types of applications may not be available to Silverlight applications. The best unit test project for a Silverlight application is another Silverlight application, so the Silverlight application and the test application share the same run time environment and we shall have better confidence on the test results.
Visual Studio 2010 has very good support to create unit test projects for Silverlight applications. The "SilverlightApplicationUnitTest
" in this article is created by the "Silverlight Unit Test Application" project template that comes with the Visual Studio installation.
Set Up the Unit Test Application
Following the steps to add a new project to the Visual Studio solution, we can see a pop up window showing the selections of the project templates:
Select the "Silverlight Unit Test Application" project template and follow the rest of the instructions, we can easily add the "SilverlightApplicationUnitTest
" to the Visual Studio solution. After the unit test project is added to the solution, we need to further set up the test environment.
- Because we are going to test the code in the "
SilverlightApplication
" project, we need to add a reference to this project from the unit test project. - In order to test the WCF service calls, we also need to add the WCF service client configuration file "
ServiceReferences.ClientConfig
" to the unit test project.
In order that we do not have two copies of the "ServiceReferences.ClientConfig" file, this file is added to the "SilverlightApplicationUnitTest
" project as a linked file. You can refer to this link on how to add an existing file as a linked file in Visual Studio.
The Implementation of the Unit Test
After setting up the environment, we can write some unit tests on the view model of the "SilverlightApplication
" project. We can test other parts of the application too, but in this article, I will limit the scope of the tests to the view model. The tests are written in the "MainPageViewModelTests
" class in the "Tests.cs" file:
using System;
using Microsoft.Silverlight.Testing;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using SilverlightApplication.ViewModels;
namespace SilverlightApplicationUnitTest
{
[TestClass]
public class MainPageViewModelTests: SilverlightTest
{
[TestMethod]
public void ViewModelInitiation()
{
MainPageViewModel vm = new MainPageViewModel();
Assert.IsTrue(vm.StudentList == null,
"At view model initiation, StudentList should be null");
Assert.IsTrue(vm.GetStudentWCFCommand != null,
"At view model initiation, GetStudentWCFCommand should be initiated");
Assert.IsTrue(vm.GetStudentWCFWrongMethodCommand != null,
"At view model initiation,
GetStudentWCFWrongMethodCommand should be initiated");
}
[TestMethod]
[Asynchronous]
public void AsynchronousWCFCallWithGetStudentWCFCommand()
{
MainPageViewModel vm = new MainPageViewModel();
bool done = false;
vm.AsyncCallbackCompleted += (() => done = true);
EnqueueCallback(() => vm.GetStudentWCFCommand.Execute(null));
EnqueueConditional(() => done);
EnqueueCallback(() => Assert.IsNotNull(vm.StudentList,
"No student retrieved from the WCF service"));
EnqueueCallback(() => Assert.IsTrue(vm.StudentList.Count == 10,
"The number of the students should be 10."));
EnqueueTestComplete();
}
[TestMethod]
[Asynchronous]
public void AsynchronousWCFCallWithGetStudentWCFWrongMethodCommand()
{
MainPageViewModel vm = new MainPageViewModel();
bool done = false;
vm.AsyncCallbackCompleted += (() => done = true);
EnqueueCallback(() => vm.GetStudentWCFWrongMethodCommand.Execute(null));
EnqueueConditional(() => done);
EnqueueCallback(() => Assert.IsNotNull(vm.StudentList,
"No student retrieved from the WCF service"));
EnqueueCallback(() => Assert.IsTrue(vm.StudentList.Count == 10,
"The number of the students should be 10.
Problem is captured by the unit test."));
EnqueueTestComplete();
}
}
}
We have three test methods in the "MainPageViewModelTests
" class. The "ViewModelInitiation
" method checks if the constructor of the view model successfully initiates its state and if the commands in the view model are successfully wired. The other two test methods test if the commands in the view model can initiate the WCF service call and if the correct number of students is obtained from the WCF service in the asynchronous callback functions.
Because the WCF service call is asynchronous, we need to use the mechanism introduced by Jeff Wilcox to ensure that we always wait until the callback function finishes before we check the result from the WCF service. We first initiate a boolean variable "done
" to false
and subscribe to the "AsyncCallbackCompleted
" event from the view model with an anonymous function to set the variable "done
" to true
. We then use the "EnqueueCallback
" method to issue the command that makes the WCF service call. If you go back to take a look at the callback functions implemented in the view model of the "SilverlightApplication
" project, you can see that the "AsyncCallbackCompleted
" is fired at the end of the callback functions. The firing of the "AsyncCallbackCompleted
" triggers the anonymous event handler in the test method to set the variable "done
" to true
. At this time, we are sure that the callback function in the view model has finished execution and the callback result is ready to be checked. To make this mechanism work, you need to call the "EnqueueTestComplete
" method after you add the function to initiate the WCF call and the functions to check the result from the WCF service with the "EnqueueCallback
" method. You can read more about this mechanism from Jeff Wilcox's blog. I hope you can get more details about it.
Run the Test Application
Yes. It is as simple as it is. We now complete both the "SilverlightApplication
" project and the unit test project. To run the unit test, we can set the "SilverlightHostWebApplication
" project as the start up project and set the "TestPage.aspx" page as the start page. If we are going to run the unit test in the Visual Studio environment, we need to start the unit test application without debug. If we debug run the unit test and if a certain part of our code fails the test, instead of finishing the test and reporting the test result to us, Visual Studio may simply stop at the code where the failure occurs.
After Type in "Ctrl+F5" to start the unit test without debug and wait until the unit test finishes, you can see the following result:
As expected, two of the three test methods succeeded and one failed in our demo application.
Click on the highlighted failed test item, we can see the details of the failure which shows that the number of the students received from the WCF service does not match the number expected. You may question that we only demonstrated how the unit test project captures logical errors in the application. How about the run time errors? The answer is that the unit test projects captures run time errors equally well. You can throw some artificial exceptions in the "SilverlightApplication
" project. If your test methods cover the code where the exceptions are thrown, the run time exceptions will be captured and shown in the report.
Conclusion
At the beginning of this article, we stated the two special challenges that the unit testing of Silverlight applications face. We can now come to the conclusion.
- The unit test project for a Silverlight application is best to be another Silverlight application, so they share the same run time environment and the test result is more convincing. We can use the "Silverlight Unit Test Application" project template in Visual Studio to easily create this type of unit test projects.
- To address the difficulty caused by asynchronous service calls, we can use the method introduced by Jeff Wilcox. The unit test project in this article has demonstrated how this method is used. The
public
event introduced in the "ViewModelBase
" class in the "SilverlightApplication
" project helps us to inform the test methods when the asynchronous callback functions complete.
Points of Interest
- This article presented an example on unit testing of Silverlight applications with asynchronous service callbacks.
- To demonstrate how we can unit test a Silverlight application, I created a simple Silverlight MVVM application. It is true that the MVVM pattern does improve testability, but it does not mean that we cannot unit test Silverlight applications that are not in the MVVM pattern.
- This article becomes pretty lengthy by introducing the Silverlight application to be tested. I hope you are not bored by my lengthy representation. It also means that the MVVM pattern can be over-killing for a simple application by introducing a lot of programming overhead. If your application is simple, you may consider not using the MVVM pattern, but directly targeting the functionalities that you need to create.
- The example unit test project in this article only demonstrated how unit tests capture logical errors. But the unit test project captures run time errors equally well. If you download the example code, you can throw some exceptions in the application. If the test methods cover the code where the exceptions are thrown, they should be captured and displayed in the test report.
- According to Jeff Wilcox, the method used in this article to unit test asynchronous callback functions may only work for Silverlight applications. There is no guarantee that it will work on other types of applications.
- For simplicity, the Silverlight application and the unit test application are both hosted in the same ASP.NET web application. In practice, it is better to host them in different web applications. If you do host them in the same web application, you need to make the test application inaccessible when you deploy your application to the production environment.
- I hope you like my postings and I hope this article can help you in one way or the other.
History
This is the first revision of this article.