Unit Testing Xamarin Forms View Model
Join the DZone community and get the full member experience.
Join For FreeView Model in a Nutshell
The view model is the centerpiece of the Model-View-ViewModel pattern. When you're not using MVVM, your presentation logic is in the view (code behind), making harder to test because the logic is coupled to UI elements. On the other hand, when using MVVM, your presentation logic move into the view model, by decoupling logic from the view, you gain the ability to test logic without being bothered by UI elements.
Getting Started
To walk you through this tutorial, I created a Xamarin Forms app called WorkoutTube.
WorkoutTube allows users to
- View a list of workout videos from Youtube.
- Launch the Youtube app with the selected video, if the Youtube app is not installed, it will launch the browser version.
The project can be downloaded here.
Project Overview
The project is already implemented, we will focus on unit tests only.
In order to run the project, edit the file AppConfiguration.cs, under the WorkoutTube project, update the property YoutubeApiKey with your own, you can request Youtube API key here.
Tools
To make testing easier, the unit tests project WorkoutTube.UnitTests is using the following packages :
- Autofac: IoC container.
- AutoMock: allows you to automatically create mock dependencies
- AutoFixture: library for .NET designed to minimize the 'Arrange' phase of your unit tests
- FluentAssertions: Fluent API for asserting the results of unit tests
Making the View Model Testable
The HomePageViewModel holds dependencies to other services, instead of relying on the concrete implementation of those services, it relies on their abstractions.
Concrete implementation will be injected through an Ioc container.
xxxxxxxxxx
public HomePageViewModel(
IVideoService videoService,
IDialogService dialogService,
IYoutubeLauncherService youtubeLauncherService)
{
_videoService = videoService;
_dialogService = dialogService;
_youtubeLauncherService = youtubeLauncherService;
...
}
Having the ability to inject dependencies is great because when testing we can supply mock objects instead of real ones.
Dependency Injection
The WorkoutTube project uses AutoFac as Ioc container, dependencies are registered in App.xaml.cs.
x
public void RegisterDependencies()
{
var builder = new ContainerBuilder();
builder.RegisterType<VideoService>().As<IVideoService>().SingleInstance();
builder.RegisterType<AppConfiguration>().As<IAppConfiguration>().SingleInstance();
builder.RegisterType<DialogService>().As<IDialogService>().SingleInstance();
builder.RegisterType<YoutubeLauncherService>().As<IYoutubeLauncherService>().SingleInstance();
builder.RegisterType<HomePageViewModel>();
Container = builder.Build();
}
Writing Unit Tests
For our tests, the HomePageViewModel will be the subject under the test, we will focus on two types of testing :
- State: checking how the view model's property values are affected by certain actions.
- Interactions: checking that the view model's dependencies service's methods are called properly.
Testing States
The HomePageViewModel exposes properties and commands to which view HomePage can bind to.
Let's write a test that checks that when the HomePageViewModel is created, Videos property is empty.
x
[Test]
public void CreatingHomePageViewModel_WhenSuccessfull_VideosIsEmpty()
{
using (var mock = AutoMock.GetLoose())
{
var viewModel = mock.Create<HomePageViewModel>();
viewModel.Videos.Should().BeEmpty();
}
}
We can also write a test to check that when HomePageViewModel is created, SelectedVideo property is null, meaning there is no video selected yet.
x
[Test]
public void CreatingHomePageViewModel_WhenSuccessfull_SelectedVideoIsNull()
{
using (var mock = AutoMock.GetLoose())
{
var viewModel = mock.Create<HomePageViewModel>();
viewModel.SelectedVideo.Should().BeNull();
}
}
How about checking if the OpenVideoCommand is initialized properly.
x
[Test]
public void CreatingHomePageViewModel_WhenSuccessfull_InitializesOpenVideoCommand()
{
using (var mock = AutoMock.GetLoose())
{
var viewModel = mock.Create<HomePageViewModel>();
viewModel.OpenVideoCommand.Should().NotBeNull();
viewModel.OpenVideoCommand.Should().BeOfType<Command<VideoItem>>();
}
}
The HomePageViewModel has a method called Initialize, which is called after the view model is created, in this method, the VideoService fetch videos from Youtube API.
Let's write a test to check the following scenario: after a successful initialization, Videos property should be updated with data retrieved from the service.
x
[Test]
public async Task InitializingHomePageViewModel_WhenSuccessfull_UpdatesVideos()
{
using (var mock = AutoMock.GetLoose())
{
var videos = _fixture.CreateMany<VideoItem>(5);
mock.Mock<IVideoService>().Setup(v => v.GetVideoItems()).Returns(Task.FromResult(videos));
var viewModel = mock.Create<HomePageViewModel>();
await viewModel.Initialize();
viewModel.Videos.Should().NotBeEmpty();
viewModel.Videos.Count.Should().Equals(videos.Count());
}
}
Note: notice that we are configuring IVideoService as a mock object, that way we're not communicating with the Youtube API, unit testing is not about calling real API, that's the job of integration testing.
When testing, it is important to not only test the happy path but also the sad path (when things go wrong). In HomePageViewModel, the code that loads the videos is wrapped around a try/catch block, when we catch an exception we display the message by using IDialogService.
x
try
{
Videos.ReplaceRange(await _videoService.GetVideoItems());
}
catch (Exception ex)
{
await _dialogService.Alert(ex.Message);
}
Let's write a test for the sad path.
x
[Test]
public async Task InitializingHomePageViewModel_WhenExceptionIsRaised_CallsDialogServiceAlert()
{
using (var mock = AutoMock.GetLoose())
{
mock.Mock<IVideoService>().Setup(v => v.GetVideoItems()).Throws(new System.Exception("Unexpected error"));
var viewModel = mock.Create<HomePageViewModel>();
await viewModel.Initialize();
mock.Mock<IDialogService>().Verify(d => d.Alert(It.IsAny<string>()));
}
}
Testing Interactions
The HomePageViewModel exposes a command called OpenVideoCommand, when executed, it launches the Youtube app through a service called YoutubeServiceLauncher.
Let's write a test that checks that the method OpenAsync of YoutubeServiceLauncher has been called.
x
[Test]
public void ExecutingOpenVideoCommand_WhenSuccessfull_CallsYoutubeLauncherServiceOpenAsync()
{
using (var mock = AutoMock.GetLoose())
{
var video = _fixture.Create<VideoItem>();
mock.Mock<IYoutubeLauncherService>().Setup(y => y.OpenAsync(video.Id.VideoId));
var viewModel = mock.Create<HomePageViewModel>();
viewModel.OpenVideoCommand.Execute(video);
mock.Mock<IYoutubeLauncherService>().Verify(y => y.OpenAsync(video.Id.VideoId));
}
}
We can also write the test for the sad path.
x
[Test]
public void ExecutingOpenVideoCommand_WhenExceptionIsRaised_CallsDialogServiceAlert()
{
using (var mock = AutoMock.GetLoose())
{
var video = _fixture.Create<VideoItem>();
mock.Mock<IYoutubeLauncherService>().Setup(v => v.OpenAsync(video.Id.VideoId)).Throws(new System.Exception("Unexpected error"));
var viewModel = mock.Create<HomePageViewModel>();
viewModel.OpenVideoCommand.Execute(video);
mock.Mock<IDialogService>().Verify(d => d.Alert(It.IsAny<string>()));
}
}
Conclusion
When using MVVM, it is important to unit tests your view models, testing will increase the overall quality of your application.
I hope you enjoy this tutorial, the source code is available for download here. feel free to use it.
Published at DZone with permission of Patrick Tshibanda. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments