I’m a big fan of TDD and continuous integration, so when I first started development on Windows Phone 7, I quickly found Jeff Wilcox’s Silverlight Unit Test Framework. This unit testing framework provides a nice way to write MSTest style unit tests and have them run on a device/emulator. The piece that was missing for me was the ability to capture the results of the unit test run and include these results with an automated run of MSBuild. If any unit tests fail, the MSBuild run should fail.
I’ve seen several blog posts on this topic but none that gave me an end to end solution. Armed with the latest Mango bits and the CoreCon 10 WP7 API, this task was easier than I thought. Here’s how it works…
Let’s start with the custom LogProvider:
public class FileLogProvider : LogProvider{ public const string TESTRESULTFILENAME = @"TestResults\testresults.txt"; protected override void ProcessRemainder(LogMessage message) { base.ProcessRemainder(message); AppendToFile(message); } public override void Process(LogMessage logMessage) { AppendToFile(logMessage); } private void AppendToFile(LogMessage logMessage) { UTF8Encoding encoding = new UTF8Encoding(); var carriageReturnBytes = encoding.GetBytes(new[] { '\r', '\n' }); using (IsolatedStorageFile store = IsolatedStorageFile.GetUserStoreForApplication()) { if (!store.DirectoryExists("TestResults")) { store.CreateDirectory("TestResults"); } using (IsolatedStorageFileStream isoStream = store.OpenFile(TESTRESULTFILENAME, FileMode.Append)) { var byteArray = encoding.GetBytes(logMessage.Message); isoStream.Write(byteArray, 0, byteArray.Length); isoStream.Write(carriageReturnBytes, 0, carriageReturnBytes.Length); } } }}
public partial class MainPage : PhoneApplicationPage{ // Constructor public MainPage() { this.InitializeComponent(); LogProvider fileLogProvider = new FileLogProvider(); var settings = UnitTestSystem.CreateDefaultSettings(); settings.LogProviders.Add(fileLogProvider); Content = UnitTestSystem.CreateTestPage(settings); BackKeyPress += (x, xe) => xe.Cancel = (Content as IMobileTestPage).NavigateBack(); }}
Great! Now that we’ve captured the test run results on the phone, how do we get it to the server? I’ve heard people use web services but I found using the CoreCon API worked fine.
My custom MSBuild task does the following:
Here is the source for my custom MSBuild task:
namespace MSBuildTasks{ public class RunWP7UnitTestsInEmulator : Task { private int defaultNumberOfMiliSecondsToProcess = 50000; [Required] public string PathToUnitTestXapFile { get; set; } [Required] public string ProductGuid { get; set; } public int NumberOfMilliSecondsToProcess { get; set; } public override bool Execute() { if (NumberOfMilliSecondsToProcess == 0) { Log.LogMessage(string.Format("Using default processing time: {0}", defaultNumberOfMiliSecondsToProcess)); NumberOfMilliSecondsToProcess = defaultNumberOfMiliSecondsToProcess; } Log.LogMessage(string.Format("NumberOfMilliSecondsToProcess: {0}", NumberOfMilliSecondsToProcess)); Log.LogMessage("Running tests in XAP: " + PathToUnitTestXapFile); var productGuid = new Guid(ProductGuid); var emulator = GetWP7Emulator(); Log.LogMessage("Connecting to Emulator."); emulator.Connect(); Log.LogMessage("After call to Connect()."); //Deploy application if (emulator.IsApplicationInstalled(productGuid)) { emulator.GetApplication(productGuid).Uninstall(); } emulator.InstallApplication(productGuid, productGuid, "NormalApp", null, PathToUnitTestXapFile); //Run application var application = emulator.GetApplication(productGuid); application.Launch(); Thread.Sleep(NumberOfMilliSecondsToProcess); //Get Results from Isolated Store on device var isostorefile = application.GetIsolatedStore(); var localTestResultFileName = "EmulatorTestResults-" + productGuid + ".txt"; isostorefile.ReceiveFile(@"\TestResults\testresults.txt", localTestResultFileName, true); var testResults = File.ReadAllText(localTestResultFileName); if (testResults.Contains("Exception:")) { Log.LogError("Failing test found. Look for exception in: " + localTestResultFileName); return false; } Log.LogMessage("All tests passed."); emulator.Disconnect(); return true; } private Device GetWP7Emulator() { var manager = new DatastoreManager(CultureInfo.CurrentUICulture.LCID); var wp7Platform = manager.GetPlatforms().Single(platform => platform.Name == "Windows Phone 7"); return wp7Platform.GetDevices().Single(device => device.Name == "Windows Phone Emulator"); } }}
namespace MSBuildTasks{ public class RunWP7UnitTestsInEmulator : Task { private int defaultNumberOfMiliSecondsToProcess = 50000; [Required] public string PathToUnitTestXapFile { get; set; } [Required] public string ProductGuid { get; set; } public int NumberOfMilliSecondsToProcess { get; set; } public override bool Execute() { if (NumberOfMilliSecondsToProcess == 0) { Log.LogMessage(string.Format("Using default processing time: {0}", defaultNumberOfMiliSecondsToProcess)); NumberOfMilliSecondsToProcess = defaultNumberOfMiliSecondsToProcess; } Log.LogMessage(string.Format("NumberOfMilliSecondsToProcess: {0}", NumberOfMilliSecondsToProcess)); Log.LogMessage("Running tests in XAP: " + PathToUnitTestXapFile); var productGuid = new Guid(ProductGuid); var emulator = GetWP7Emulator(); Log.LogMessage("Connecting to Emulator."); emulator.Connect(); Log.LogMessage("After call to Connect()."); //Deploy application if (emulator.IsApplicationInstalled(productGuid)) { emulator.GetApplication(productGuid).Uninstall(); } emulator.InstallApplication(productGuid, productGuid, "NormalApp", null, PathToUnitTestXapFile); //Run application var application = emulator.GetApplication(productGuid); application.Launch();
Thread.Sleep(NumberOfMilliSecondsToProcess); //Get Results from Isolated Store on device var isostorefile = application.GetIsolatedStore(); var localTestResultFileName = "EmulatorTestResults-" + productGuid + ".txt"; isostorefile.ReceiveFile(@"\TestResults\testresults.txt", localTestResultFileName, true); var testResults = File.ReadAllText(localTestResultFileName); if (testResults.Contains("Exception:")) { Log.LogError("Failing test found. Look for exception in: " + localTestResultFileName); return false; } Log.LogMessage("All tests passed."); emulator.Disconnect(); return true; } private Device GetWP7Emulator() { var manager = new DatastoreManager(CultureInfo.CurrentUICulture.LCID); var wp7Platform = manager.GetPlatforms().Single(platform => platform.Name == "Windows Phone 7"); return wp7Platform.GetDevices().Single(device => device.Name == "Windows Phone Emulator"); } }}
<Project DefaultTargets="Test" xmlns="http://schemas.microsoft.com/developer/msbuild/2003" > <UsingTask TaskName="MSBuildTasks.RunWP7UnitTestsInEmulator" AssemblyFile="MSBuildTasks.dll"/> <PropertyGroup> <SourceFilesPath>$(MSBuildProjectDirectory)\..\Source\</SourceFilesPath> </PropertyGroup> <ItemGroup> <SolutionItemsToBuild Include="$(SourceFilesPath)\**\*.sln" /> </ItemGroup> <Target Name="Build"> <MSBuild Projects="%(SolutionItemsToBuild.Identity)" Targets="Build"/> </Target> <Target Name="Test" DependsOnTargets="Build"> <RunWP7UnitTestsInEmulator NumberOfMilliSecondsToProcess="50000" PathToUnitTestXapFile="$(SourceFilesPath)\MyWP7Project.Tests\Bin\Debug\MyWP7Project.Tests.xap" ProductGuid="MyWP7ProjectProductGuid" /> </Target></Project>
You’ll need to replace the PathToUnitTexstXapFile and ProductGuid, but this proj file should build all solutions in the “Source” folder and then test the Windows Phone unit tests specified in the Test target.
I’ve had this integrated into my continuous integration server and it works great. I need to run the CI server in interactive mode because launching the emulator on my server box always pops a dialog complaining about the graphics processing unit configuration. I just keep an emulator instance open to avoid this popup.