次の方法で共有


A - Unit Testing Windows Phone Applications

patterns & practices Developer Center

On this page: Download:
Windows Phone 7.1 SDK Abstractions | Mock Implementations | Testing Asynchronous Functionality | Using Delegates to Specify Behavior | Running Unit Tests Download code samples
Download book as PDF

Unit tests differ from integration or user acceptance tests in that they test the smallest unit of functionality possible. Typically, a unit test will test the behavior of a specific method. The table below compares unit and integration tests.

Unit Tests

Integration Tests

Dependent systems are not required

Dependent systems are required and must be online

Fast to run

Slow to run

Typically run frequently (during development and code check-in)

Typically run periodically

Tests developer expectations

Tests real system behavior

No fear of refactoring as the functionality should stay the same and the unit tests should continue to pass

No fear of refactoring as the functionality should stay the same and the unit tests should continue to pass

The most important aspect of unit testing compared to integration testing is that unit tests should not be reliant on the functionality of dependent systems to exercise the code being tested. If the method being tested handles a condition such as an IsolatedStorageException being thrown, it would not be ideal to fill up or somehow corrupt isolated storage in order to trigger the code path. Similarly, testing code that calls services or queries databases should not be reliant on dependent systems. If a test requires dependent systems to be correctly configured, running, and populated with a known set of data, the test would be very fragile and would fail if any of these conditions were not met. In such circumstances you would see many failing tests because of issues with dependent systems, rather than issues with the code under test.

A good approach to increase software testability is to isolate dependent systems and have them passed into your business logic using an abstraction such as an interface or abstract class. This approach allows the dependent system to be passed into the business logic at run time. In addition, in the interests of testability, it also allows a mock version of the dependent system to be passed in at test time. For more information see, "Forms of Dependency Injection," on Martin Fowler's website.

As previously mentioned, unit tests should run quickly. The Tailspin mobile client application unit tests run in less than 10 minutes and are triggered on every check-in to the source control system. Since the unit tests run quickly, Tailspin developers know very quickly whether their check-in caused problems. Integration tests typically take longer to run and are usually scheduled less frequently. In addition to helping catch problems with code integration, unit tests are perhaps most valuable when refactoring code. Unit tests can be thought of as the code-based documentation of how the code under test should behave. If the code under test is refactored or reorganized, the functionality should stay the same and the unit tests should continue to pass. If the unit tests don’t pass, it is possibly due to improperly refactored code. In addition, another possibility is that the desired code behavior has changed, in which case the unit tests should be updated or new tests should be added.

Windows Phone 7.1 SDK Abstractions

There are few public interfaces or abstract classes available in the Windows® Phone 7.1 SDK. For this reason, the Tailspin developers created their own. An interface was generated for each class in the Windows Phone 7.1 SDK that was used in the Tailspin mobile client application. Then, adapters and facades, which are wrapper classes that pass parameters and return values to and from the underlying Windows Phone 7.1 SDK classes, were created.

Figure 1 shows how the CameraCaptureTask class in the Microsoft.Phone.Tasks namespace is adapted by the CameraCaptureTaskAdapter class in the TailSpin.PhoneClient.Adapters namespace.

Hh821022.20271ECA64D7B10167E56F2525F9DF9E(en-us,PandP.10).png

Figure 1

Adapting the CameraCaptureTask class

The CameraCaptureTaskAdapter class implements the ICameraCaptureTask interface from the TailSpin.PhoneClient.Adapters namespace. At run time, the Tailspin mobile client application uses the CameraCaptureTaskAdapter class directly, rather than the CameraCaptureTask class.

The right hand side of Figure 1 shows the MockCameraCaptureTask class from the TailSpin.Phone.TestSupport namespace. The MockCameraCaptureTask class also implements the ICameraCaptureTask interface and is used in unit tests instead of the CameraCaptureTaskAdapter class. This allows Tailspin developers to unit test business logic that needs to interact with the camera.

The interfaces and adapters that abstract the Windows Phone 7.1 SDK classes can be found in the Tailspin.Phone.Adapters and Tailspin.PhoneClient.Adapters projects.

Mock Implementations

Unit tests should focus on how the code under test functions in response to values returned by dependent systems. By using mocks, the return values or exceptions to be thrown by mock instances of dependent systems can easily be controlled. For more information see, "Exploring the Continuum of Test Doubles," in MSDN Magazine.

The TailSpin.Phone.TestSupport project contains mock implementations of the Windows Phone 7.1 SDK adapter classes created by Tailspin. The mock classes were manually developed as it is not possible to use a mocking framework on the Windows Phone platform. Mocking frameworks require the ability to emit Microsoft intermediate language (MSIL) code, which is not currently possible on the Windows Phone platform. The mocks were developed to be general-purpose implementations with many of them having properties that accept delegates. Delegate-accepting properties enable the execution of any desired behavior necessary for the unit test. The following code example shows the MockProtectDataAdapter class.

public class MockProtectDataAdapter : IProtectData
{
  public Func<byte[], byte[], byte[]> ProtectTestCallback { get; set; }

  public Func<byte[], byte[], byte[]> UnProtectTestCallback { get; set; }

  public byte[] Protect(byte[] userData, byte[] optionalEntropy)
  {
    if(ProtectTestCallback == null)
    {
      throw new InvalidOperationException("Must set ProtectTestCallback.");
    }
    return ProtectTestCallback(userData, optionalEntropy);
  }

  public byte[] Unprotect(byte[] encryptedData, byte[] optionalEntropy)
  {
    if (UnProtectTestCallback == null)
    {
      throw new InvalidOperationException("Must set UnProtectTestCallback.");
    }
    return UnProtectTestCallback(encryptedData, optionalEntropy);
  }
}

The MockProtectDataAdapter class is an example of how a general purpose mock can be given behavior using delegates. The ProtectTestCallback property should be set with a delegate or a lambda expression so that a call to the Protect method can execute the delegate. By initializing the mock in this way, unit tests have unlimited test control of the mock.

Testing Asynchronous Functionality

The following code example shows the ViewModelGetsPictureFromCameraTask test method, which demonstrates testing asynchronous functionality. The unit test validates that the PictureQuestionViewModel class can get an image from the camera capture task and set the view model's Picture property to the returned image. The Asynchronous method attribute allows the test to run until an unhandled exception is thrown, or until the EnqueueTestComplete method is called.

[TestMethod, Asynchronous]
public void ViewModelGetsPictureFromCameraTask()
{
  var mockCameraCaptureTask = new MockCameraCaptureTask();
  WriteableBitmap picture = null;
  var imageUri = new Uri("/TailSpin.PhoneClient.Tests;
    component/ViewModels/Images/block.jpg", UriKind.Relative);
  var src = new BitmapImage();
  src.SetSource(Application.GetResourceStream(imageUri).Stream);
  picture = new WriteableBitmap(src);

  var sri = Application.GetResourceStream(imageUri);
  mockCameraCaptureTask.TaskEventArgs = new SettablePhotoResult 
  { 
    ChosenPhoto = sri.Stream 
  };
            
  var questionAnswer = new QuestionAnswer 
  { 
    QuestionText = "Will this test pass?" 
  };

  var target = new PictureQuestionViewModel(questionAnswer, 
    mockCameraCaptureTask, new MockMessageBox());
  Assert.IsNull(target.Picture);

  target.PropertyChanged += (s, e) =>
  {
    if (e.PropertyName != "Picture") return;

    Assert.IsNotNull(target.Picture);
    Assert.IsTrue(IntArraysMatch(target.Picture.Pixels, picture.Pixels));

    EnqueueTestComplete();
  };

  target.CameraCaptureCommand.Execute();
}

The test first configures an instance of the MockCameraCaptureTask class, setting its TaskEventArgs property with a SettablePhotoResult that contains a specific picture. The test listens to the view model’s PropertyChanged event looking for a change to the Picture property. In the event handler for the PropertyChanged event, the picture is validated by comparing the pixels of the source and target images.

The Execute method call on the view model’s CameraCaptureCommand triggers the code under test. The view model calls the Show method on the MockCameraCaptureTask class and then listens for the Completed event. Once the event occurs and a picture is returned, the Picture property in the view model is set.

The unit test validates this behavior but must wait for the MockCameraCaptureTaskCompleted event to be fired, and the PictureQuestionViewModelPropertyChanged event to be fired, in order to validate the results.

Using Delegates to Specify Behavior

The following code example shows the SettingPasswordUsesEncryptionServiceAndPersistsIntoIsoStore test method, which demonstrates how to initialize a mock by passing a delegate to it. The unit test validates that the SettingsStore class encrypts passwords prior to isolated storage persistence.

[TestMethod]
public void SettingPasswordUsesEncryptionServiceAndPersistsIntoIsoStore()
{
  var encoder = new UTF8Encoding();
  var mockProtectDataAdapter = new MockProtectDataAdapter();
  mockProtectDataAdapter.ProtectTestCallback =
    (userData, optionalEntropy) =>
    {
      var stringUserData = encoder.GetString(userData, 0, userData.Length);
      return encoder.GetBytes(string.Format("ENCRYPTED: {0}", 
        stringUserData));
    };
  var target = new SettingsStore(mockProtectDataAdapter);
  IsolatedStorageSettings.ApplicationSettings["PasswordSetting"] = null;

  target.Password = "testpassword";

  var encryptedByteArray = (byte[])IsolatedStorageSettings
    .ApplicationSettings["PasswordSetting"];
  Assert.AreEqual("ENCRYPTED: testpassword", 
    encoder.GetString(encryptedByteArray, 0, encryptedByteArray.Length));
}

The test first configures an instance of the MockProtectDataAdapter class, providing it behavior that will execute when its Protect method is called. In this case, the behavior is simply to prepend the string "ENCRYPTED:" to the Password property. The test triggers the code by setting the Password property on the SettingsStore class. The test then validates that the value that persists into isolated store was encrypted by the behavior defined in the test.

Running Unit Tests

Tailspin runs unit tests on the emulator and on a real device to make sure the test behavior is not affected by any behavioral differences in the core libraries on the phone as compared to the desktop. It would be even more valuable if these unit tests could be automatically run every time the code base is altered. In order to automate the running of unit tests, the developers at Tailspin use a custom Microsoft Build Engine (MSBuild) task and a custom logger.

This MSBuildTasks.RunWP7UnitTestsInEmulator task performs the following steps:

  1. Connects to the Windows Phone emulator
  2. Installs the unit test application
  3. Launches the unit test application
  4. Loads the test results file from the isolated storage in the emulator
  5. Examines the test results file for failing tests

In order to capture the test results into a file, a custom logger is used. This FileLogProvider class in the TailSpin.Phone.TestSupport project extends Microsoft.Silverlight.Testing.Harness.LogProvider and captures all messages into a text file which is saved to isolated storage.

All links in this book are accessible from the book's online bibliography. You can find the bibliography on MSDN at: https://msdn.microsoft.com/en-us/library/gg490786.aspx.

Next Topic | Previous Topic | Home

Last built: May 25, 2012