Article browser
Table of Contents
- Introduction
- Demo application
PART I - Unit testing on Windows Phone 7
- Setting up unit tests
- 4. Unit testing without mocking
PART II - Camera Service
- 5. Basics of the Camera Service
- 6. Using the CameraService in the emulator
- 7. Using the CameraService in unit tests
- 8. Conclusion
1. Introduction
Welcome to part 7 of the articles
series about Catel. If you haven’t read the previous article(s) of Catel yet,
it is recommended that you do. They are numbered so finding them shouldn’t be
too hard.
You might be thinking right now: why does this guy implement
a CameraService
? There is a beautiful API available using the PhotoCamera class. Remind yourself again why you even want to write
your application in MVVM? Was it because it’s the buzz-word of the century, or…
Oh yeah, now I remember, you want to be able to unit test all your view models.
Now tell me, how is that possible if you instantiate a PhotoCamera
object?
And, for example, how are you going to support all the different kinds of
cameras out there (ones that support all flash modes, some that don’t support
all flash modes, etc).
This article will explain how the CameraService
is created and more important, why it is created. The CameraService
allows you to truly interact in an MVVM manner with the camera on Windows Phone
7 Mango devices. This article uses Catel, but the service can be used on its
own if you want to.
This article is split up into several
parts. The first part is about unit testing for Windows Phone 7 in general. The
second part is about unit testing the camera service. Finally, the last part is
about the conclusion et al.
2. Demo application
2.1. Functional requirements
The demo application used in this article is very simple. The
first screen allows someone to take a picture using the camera. If there are
existing pictures for the application, the user can also “flick” to the right
to navigate through the pictures.
In the pictures screen, it is possible to flick to the next
image (right) or the previous image (left). When the first image is shown and
the user flicks left, the main page should become visible. It should also be
possible to delete image. When an image is deleted, it should always select the
image at the left of the index of the image being deleted. If the selected
image is the first and there are still images left, the next first image should
be shown. If there are no images left after deleting an item, the application
should navigate to the main page.
2.2. Requirements for unit testing
Great, the functional requirements are known. Now let’s take
a look at how to unit test this application. The following features should be
unit tested. All tests are numbered so they can be referenced easily.
MainPage
-
[MP_01] Flicking to right should not be possible if there are no
images
-
[MP_02] Flicking to right should be possible if there are images
available
-
[MP_03] Flicking to right when taking a picture should not be possible
-
[MP_04] Clicking the view button should not be able when there
are no images
-
[MP_05] Clicking the view button should be able when there no
images
-
[MP_06] Navigation to about view should be possible at all times
-
[MP_07] Taking a picture should be possible
-
[MP_08] Taking a picture when already taking a picture should not
be possible
PhotoView
-
[PV_01] Flicking to right should not be possible if there are not
images at the right
-
[PV_02] Flicking to right at an image in the middle should load
next image
-
[PV_03] Flicking to left at an image in the middle should load
previous image
-
[PV_04] Flicking to left at first image should navigate to main
page
-
[PV_05] Canceling a delete image command should not navigate away
-
[PV_06] Deleting an image should navigate to previous image
-
[PV_07] Deleting the first image when images are left should
navigate to next image
-
[PV_08] Deleting the first image when no images are left should
navigate to main page
-
[PV_09] Clicking the camera button should always navigate to the
main page
The example code that ships with this article contains unit
tests for all the situations above.
PART I Unit testing on Windows Phone 7
3. Setting up unit tests
If you already know how to set up unit tests for a
Windows Phone 7 application, you can skip this chapter.
Setting up unit tests for Windows Phone 7 isn’t as easy as
you would expect. There is no unit test framework available out of the box, nor
are there templates that allow you to create a unit test project. Some people
are trying to outsmart the system by running regular Silverlight unit tests. I
think that is a very bad idea because then you won’t test the logic against the
WP7 framework, but against the Silverlight framework which is different.
3.1. Downloading the right libraries
First of all, there are special libraries needed to enable
unit testing for WP7. The ones I use are in the Windows Phone 7 toolkit, but also
located in the lib folder of the demo project that ships with this article. The
following two libraries are needed:
-
Microsoft.Silverlight.Testing.dll
-
Microsoft.VisualStudio.QualityTools.UnitTesting.Silverlight.dll
3.2. Creating the unit test project
Now we have the required libraries, let’s go and create the
unit test project. The first step is to create a new Windows Phone 7 application:
Since we are using Mango, make sure to select Windows
Phone 7.1. After this step, make sure to add references to both your
original Windows Phone 7 application and the test libraries you downloaded earlier
in this chapter. Visual Studio might warn you that it might be unsafe to
reference Silverlight assemblies, but you can ignore that.
3.3. Modifying the MainPage
Last thing to do is to modify the MainPage.xaml.cs
which is automatically by the project template. Make sure the constructor of
the MainPage
looks like the code below:
public MainPage()
{
InitializeComponent();
Content = UnitTestSystem.CreateTestPage();
}
You are now ready to create your first unit test! Let’s do
that to make sure your unit tests are able to run. Add a new class called DemoTest
and use the following content:
using Microsoft.VisualStudio.TestTools.UnitTesting;
[TestClass]
public class DemoTest
{
[TestMethod]
public void MyFirstUnitTest()
{
Assert.Inconclusive("We need to write our first unit test");
}
}
Finally if you run the unit test project, you will see the
following screens (which should be familiar if you are used to writing unit
tests in Silverlight):
4. Unit testing without mocking
In unit tests for WPF and Silverlight, you are probably used
to mock all your interfaces. In Catel, we always provide a test implementation
of every service so you are not forced to mock the services at all times. Of
course it is still possible to mock services for WPF and Silverlight, but for
Windows Phone 7 it’s a different kind of story.
There are currently no mocking frameworks available for WP7
and there will probably never going to be mocking frameworks. You are probably
thinking? Why not, there is a big market out there and I want to be able to use
unit tests for my WP7 applications. Unfortunately, the Reflection.Emit
method
is missing in WP7. This method is necessary to mock any objects.
4.1. Testing events
The first issue that came up while writing the code for this
article was that the GestureListener
had to be declared inside code. If
you are unknown with the GestureListener
, it’s a class that allows you
to subscribe to specific gestures on a WP7 page. So, I created a command on the
view model that should be executed when a flick gesture was detected. This is
the way I keep the MVVM pattern intact:
var gestureListener = GestureService.GetGestureListener(this);
gestureListener.Flick += (sender, e) =>
{
if (ViewModel != null)
{
ViewModel.Flick.Execute(e);
}
};
As you can see, the Flick
command is executed as soon
as a Flick
gesture is detected.
The next issue that came up was the need to unit test
whether flicking left or right was that the constructor of the FlickGestureEventArgs
was internal. Why the development team of the toolkit decided to make it
internal is still unknown to me, but I had to find way around this.
The idea I came up with was pretty easy. In the application,
I created a new object FlickData
with exactly the same properties as the
FlickGestureEventArgs
. Then I created several constructors that allow
easy unit testing and can also be used inside the real application logic. Below
is the code for the FlickData
class:
public enum FlickMovement
{
LeftToRight,
RightToLeft
}
public class FlickData
{
public FlickData(FlickGestureEventArgs eventArgs)
: this(eventArgs.Angle, eventArgs.Direction, eventArgs.HorizontalVelocity, eventArgs.VerticalVelocity) { }
public FlickData(double angle, Orientation direction, double horizontalVelocity, double verticalVelocity)
{
Angle = angle;
Direction = direction;
HorizontalVelocity = horizontalVelocity;
VerticalVelocity = verticalVelocity;
}
public FlickData(FlickMovement movement)
{
Angle = 0;
Direction = Orientation.Horizontal;
VerticalVelocity = 0;
switch (movement)
{
case FlickMovement.LeftToRight:
HorizontalVelocity = 15;
break;
case FlickMovement.RightToLeft:
HorizontalVelocity = -15;
break;
default:
throw new ArgumentOutOfRangeException("movement");
}
}
public double Angle { get; set; }
public Orientation Direction { get; set; }
public double HorizontalVelocity { get; set; }
public double VerticalVelocity { get; set; }
}
Beside this class, it is also required to update the
subscription to the command (because it now cannot accept a parameter of type FlickGestureEventArgs
,
but a parameter of type FlickData
). Because the FlickData
class
accepts an object of type FlickGestureEventArgs
as argument, the code
change is very small. This is the updated code:
var gestureListener = GestureService.GetGestureListener(this);
gestureListener.Flick += (sender, e) =>
{
if (ViewModel != null)
{
ViewModel.Flick.Execute(new FlickData(e));
}
};
Now everything is in place for the view model to be tested,
let’s take a look at a simple unit test [MP_01] which tests if flicking is not
allowed in the main page if no images are available:
[TestMethod]
public void Flick_NoImagesAvailable()
{
var serviceLocator = ServiceLocator.Instance;
var photoRepository = new TestPhotoRepository(0);
serviceLocator.RegisterInstance<IPhotoRepository>(photoRepository);
var vm = new MainPageViewModel();
Assert.IsFalse(vm.Flick.CanExecute(new FlickData(FlickMovement.LeftToRight)), "Flick from left to right is never allowed");
Assert.IsFalse(vm.Flick.CanExecute(new FlickData(FlickMovement.RightToLeft)));
}
As you can see, a test implementation of the IPhotoRepository
is registered to make sure there are no images available in the view model.
Then, the unit test checks whether a flick to left is not possible (a flick to
left on the main page should never be possible) and whether a flick to right is
not possible (a flick to right should not be possible if there are no images).
4.2. Testing services
When writing services for Catel, we as developers strongly
believe in testability (otherwise, what’s the whole point of writing
applications using MVVM?) of applications. Therefore we provide a test
implementation of every service we write for the Catel MVVM toolkit. Some
people find this quite an overhead because they love to use mocking frameworks.
Whatever you use is fine with us, we just like to support both ways out of the
box.
This principle seems to come out very handy for Windows
Phone 7 since there are no mocking frameworks available in WP7. So, to test
services provided by Catel is very, very simple. In this example, the IMessageService
will be used so unit test can check whether the result is correctly handled.
First, let’s start with a unit test that makes any sense. In
the demo application, it is possible to delete existing images. The user should
be asked for confirmation so a unit test must be written to check if this
confirmation is implemented correctly. The code for the whole unit test looks
like this:
[TestMethod]
public void DeleteImage_Cancel()
{
var serviceLocator = ServiceLocator.Instance;
var photoRepository = new TestPhotoRepository(3);
serviceLocator.RegisterInstance<IPhotoRepository>(photoRepository);
_testNavigationService.ClearLastNavigationInfo();
_testMessageService.ExpectedResults.Enqueue(MessageResult.Cancel);
var vm = new PhotoViewModel();
vm.UpdateNavigationContext(CreateNavigationContextWithId(2));
vm.Delete.Execute();
Assert.AreEqual(3, photoRepository.Photos.Count, "Photo count should not have changed");
Assert.AreEqual(null, _testNavigationService.LastNavigationUri, "Should have canceled");
Assert.AreEqual(null, _testNavigationService.LastNavigationParameters, "Should have canceled");
}
As you can see, there are several services that are being
used in this unit test. One of them, the IPhoneRepository
, is created
inside the unit test. The other ones seem to be fields. The fields are
initialized in the TestInitialize
method which is shown below:
private Catel.MVVM.Services.Test.NavigationService _testNavigationService;
private Catel.MVVM.Services.Test.MessageService _testMessageService;
[TestInitialize]
public void Initialize()
{
var serviceLocator = ServiceLocator.Instance;
if (_testNavigationService == null)
{
_testNavigationService = new Catel.MVVM.Services.Test.NavigationService();
serviceLocator.RegisterInstance<INavigationService>(_testNavigationService);
}
if (_testMessageService == null)
{
_testMessageService = new Catel.MVVM.Services.Test.MessageService();
serviceLocator.RegisterInstance<IMessageService>(_testMessageService);
}
}
Instead of the real MessageService
, a test
implementation of the MessageService
is instantiated and registered in
the IoC container.
Back to our unit test and focus on the part that is shown
below:
_testNavigationService.ClearLastNavigationInfo();
_testMessageService.ExpectedResults.Enqueue(MessageResult.Cancel);
var vm = new PhotoViewModel();
vm.UpdateNavigationContext(CreateNavigationContextWithId(2));
vm.Delete.Execute();
Assert.AreEqual(3, photoRepository.Photos.Count, "Photo count should not have changed");
Assert.AreEqual(null, _testNavigationService.LastNavigationUri, "Should have canceled");
Assert.AreEqual(null, _testNavigationService.LastNavigationParameters, "Should have canceled");
The last navigation info of the INavigationService
is
cleared so we can make sure that no calls are made to the INavigationService
during our unit test. We also enqueue the expected result for the IMessageService
.
The first call to the IMessageService
will return the specified MessageResult
.
In words, the test checks whether a cancel is correctly
handled. The application should not navigate away and the number of images
should stay the same.
PART II - Camera Service
5. Basics of the Camera Service
The CameraService
provides a real implementation, but
also a test implementation. The real implementation is automatically registered
in the IoC container if a view model is instantiated the first time. If you
don’t like to use the ViewModelBase
that ships with Catel, you will have
to register the camera service like this:
serviceLocator.RegisterInstance<ICameraService>(CameraService);
The CameraService
follows the PhotoCamera
API
as much as possible. This way, there is no learning curve for using the
5.1. Starting and stopping the service
The PhotoCamera
documentation continuously states
that the camera object must be created and disposed properly. In the service,
this is encapsulated by the StartService
and StopService
methods.
To start the service, use the code below:
var cameraService = GetService<ICameraService>();
cameraService.CaptureThumbnailAvailable += OnCameraServiceCaptureThumbnailAvailable;
cameraService.CaptureImageAvailable += OnCameraServiceCaptureImageAvailable;
cameraService.Start();
To stop the service, use the code below: (note: the Close
method is feature of Catel):
protected override void Close()
{
var cameraService = GetService<ICameraService>();
cameraService.Stop();
cameraService.CaptureThumbnailAvailable -= OnCameraServiceCaptureThumbnailAvailable;
cameraService.CaptureImageAvailable -= OnCameraServiceCaptureImageAvailable;
}
5.2. Capturing images
To capture images, several things must be done. The first
action to accomplish is to subscribe to the ICameraService.CaptureImageAvailable
event. The next step is to invoke the CaptureImage
method like shown
below:
CameraService.CaptureImage();
The last part is very important. You will need to read the
image stream from the CaptureImageAvailable
event:
BitmapImage bitmap = new BitmapImage();
bitmap.SetSource(e.ImageStream);
5.3. Showing a video of camera in a view
To show a preview of the camera input on the phone, first
subscribe to the ICameraService.CaptureThumbnailImageAvailable
event. Next
step is to create a property on the view model:
public BitmapImage CurrentPhoto
{
get { return GetValue<BitmapImage>(CurrentPhotoProperty); }
set { SetValue(CurrentPhotoProperty, value); }
}
public static readonly PropertyData CurrentPhotoProperty = RegisterProperty("CurrentPhoto", typeof(BitmapImage));
This property definition is a Catel property, but if you
prefer using a different MVVM framework or your own property definition style,
you are free to do that as well.
In the view, use the Image
control to show the
current photo:
<Image Grid.Row="0"
Source="{Binding
CurrentPhoto}" />
Last but not least, we need to update the CurrentPhoto
property when a new thumbnail is available.
private void OnCameraServiceCaptureThumbnailAvailable(object sender, ContentReadyEventArgs e)
{
BitmapImage bitmap = new BitmapImage();
bitmap.SetSource(e.ImageStream);
CurrentPhoto = bitmap;
}
5.4. Instantiating the test implementation
The test implementation of the CameraService
needs to
be instantiated with an image. The service will use this image to create an
animation. The animation that will be applied is the image scrolling to the
right pixel by pixel.
To instantiate the test service, add an image to the Windows
Phone 7 project and set its build action to Resource. Then instantiate
the service like in the code below:
var testImage = new BitmapImage();
var streamResourceInfo = Application.GetResourceStream(new Uri("/MyAssembly;component/Resources/Images/MyImage.png", UriKind.RelativeOrAbsolute));
testImage.CreateOptions = BitmapCreateOptions.None;
testImage.SetSource(streamResourceInfo.Stream);
_testCameraService = new CameraService(testImage);
serviceLocator.RegisterInstance<ICameraService>(_testCameraService);
By default, the CameraService
will generate a new
thumbnail image every 50 milliseconds. It is possible to customize this with a
constructor overload.
5.5. Customizing camera settings for testing
Sometimes it is required to test different resolutions. One
way to do this is to buy all available Windows Phone 7 devices and test the
software on all the cameras. An easier way is to use the ICameraService
and customize the camera options to test how the application responds to the
different settings.
The settings are stored in the CameraServiceTestData
class. This class allows customizing all the properties normally found on the PhotoCamera
class. For example, to only allow the primary camera (because front facing
cameras are not supported by all devices), use the following code:
var cameraTestSettings = new CameraServiceTestData();
cameraTestSettings.SupportedCameraTypes = CameraType.Primary;
cameraService.UpdateTestData(cameraTestSettings);
It is also possible to change the thumbnail and final
resolution of the images:
var cameraTestSettings = new CameraServiceTestData();
cameraTestSettings.PreviewResolution = new Size(400, 800);
cameraTestSettings.Resolution = new Size(1200, 2400);
cameraService.UpdateTestData(cameraTestSettings);
6. Using the CameraService in the emulator
First of all, there must be some way of determining whether
the code is running in the emulator or on a real device. In the App.xaml.cs
of the application, register the test version of the camera service in the IoC
container:
var serviceLocator = ServiceLocator.Instance;
var testImage = new BitmapImage();
var streamResourceInfo = Application.GetResourceStream(new Uri("/MyAssembly;component/Resources/Images/MyImage.png", UriKind.RelativeOrAbsolute));
testImage.CreateOptions = BitmapCreateOptions.None;
testImage.SetSource(streamResourceInfo.Stream);
serviceLocator.RegisterInstance<ICameraService>(new MVVM.Services.Test.CameraService(testImage));
If the service is correctly implemented in the view model as
explained in showing a video of camera in a view,
you will see the thumbnails being updated on the main page:
7. Using the CameraService in unit tests
So far, we have only covered basic unit testing for Windows
Phone 7. Now let’s take a look on how to test whether the camera implementation
works correctly in the application.
We start with writing the TestInitialize
method which
instantiates the test instance:
[TestClass]
public class MainPageViewModelTest
{
private CameraService _testCameraService;
[TestInitialize]
public void Initialize()
{
var serviceLocator = ServiceLocator.Instance;
if (_testCameraService == null)
{
var testImage = new BitmapImage();
var streamResourceInfo = Application.GetResourceStream(new Uri("/MyAssembly;component/Resources/Images/MyImage.png", UriKind.RelativeOrAbsolute));
testImage.CreateOptions = BitmapCreateOptions.None;
testImage.SetSource(streamResourceInfo.Stream);
_testCameraService = new CameraService(testImage);
serviceLocator.RegisterInstance<ICameraService>(_testCameraService);
}
}
}
Now the view models that are being unit tested will use the
test implementation.
8. Conclusion
This article has shown why we, the developers of Catel, have
chosen to implement the PhotoCamera
class as a service. We hope that you
truly understand why we created this service and how powerful it is.
Some developers really don’t want to use Catel for some
reasons (for example, because other people tell them to use MVVM light (or
another MVVM framework)). In that case, it is still possible to use all the
services provided by Catel. Just register all the services manually in your IoC
container of choice.
We’d love to hear your feedback!
More information about Catel can be found at http://catel.codeplex.com