Introduction.
When doing Xamarin Forms unit testing, you will sooner or later hit at “You MUST call Xamarin.Forms.Init()” problem. In other words, this message simply tells that Xamarin Forms must be initialized in order to perform the test. In this post I will describe one way how I handled this issue!
Demo app.
To put the problem into the context, I prepared simple Xamarin Forms App to demonstrate how easy can developer crush into “You MUST call Xamarin.Forms.Init() “ problem.
That is to say, my demo application is very simple – Xamarin Forms application with one simple page. As I normally do, I follow Model-View-ViewModel (MVVM) pattern, because this approach has some benefits over “classical – code behind – approach”. MVVM with Dependency Injection enables to write very flexible and testable code.
Source code is available here: https://github.com/josipx/Jenx.Xamarin.UnitTestExample. I omitted iOS version of the app for code and solution brevity.
MainPage View
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" x:Class="Jenx.Xamarin.UnitTestExample.Views.MainPage" xmlns:vm="clr-namespace:Jenx.Xamarin.UnitTestExample.ViewModels" Visual="Material" Title="{Binding Title}"> <ContentPage.BindingContext> <vm:MainViewModel /> </ContentPage.BindingContext> <StackLayout Padding="15"> <Entry Text="{Binding UserEntry}" /> <Button Margin="0,10,0,0" Text="Go to Jenx.si" Command="{Binding OpenJenxSiInBrowserCommand}" /> <Button Margin="0,10,0,0" Text="Report Jenx.si was here" Command="{Binding FillJenxWasHereCommand}" /> <Button Margin="0,10,0,0" Text="Report platform" Command="{Binding FillPlatformTypeCommand}" /> </StackLayout> </ContentPage> |
MainPage ViewModel
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
using System; using System.Windows.Input; using Xamarin.Forms; namespace Jenx.Xamarin.UnitTestExample.ViewModels { public class MainViewModel : BaseViewModel { public MainViewModel() { Title = "Jenx.si Playground"; } private string _userEntry; public string UserEntry { get => _userEntry; set => SetProperty(ref _userEntry, value); } public ICommand OpenJenxSiInBrowserCommand => new Command(() => Device.OpenUri(new Uri("https://www.jenx.si"))); public ICommand FillJenxWasHereCommand => new Command(() =>{ UserEntry = "Jenx was here!";}); public ICommand FillPlatformTypeCommand => new Command(() => { switch (Device.RuntimePlatform) { case Device.Android: UserEntry = "Android was here!"; break; case Device.iOS: UserEntry = "iOS was here!"; break; default: UserEntry = "Unknown was here!"; break; } }); } } |
To be more plastic, here is the Android screenshot of my testing app. App has input field and three buttons. First button redirects to my web site, other two bind some text to input field. If you check the code, you can see that two buttons uses Device
utility class.
Unit tests.
For unit tests I used super-doper unit testing framework xUnit!
Simple unit test without Device utility class.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
using Jenx.Xamarin.UnitTestExample.ViewModels; using Xunit; namespace Jenx.Xamarin.UnitTestExample.UnitTests.ViewModels { public class MainViewModelUnitTests { [Fact] public void Execute_FillJenxWasHereCommand_UserEntryFilled() { // arrange var vm = new MainViewModel(); // act vm.FillJenxWasHereCommand.Execute(null); // assert Assert.Equal("Jenx was here!", vm.UserEntry); } } } |
Unit test runner shows everything is just fine.
Unit test with Device utility class.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
using Jenx.Xamarin.UnitTestExample.ViewModels; using Xunit; namespace Jenx.Xamarin.UnitTestExample.UnitTests.ViewModels { public class MainViewModelUnitTests { // Code removed for brevity [Fact] public void Execute_FillPlatformTypeCommand_UserEntryFilled() { // arrange var vm = new MainViewModel(); // act vm.FillPlatformTypeCommand.Execute(null); // assert Assert.Equal("Unknown was here!", vm.UserEntry); } } } |
BAM, unit test runner shows failure.
So, before executing this unit test, Xamarin Forms must be initialized. Let’s try to fix this.
Solution: Mocking Device.PlatformServices.
After short investigation, I found out that Device.PlatformServices
property must be set. This property implements Xamarin.Forms.Internals.IPlatformServices
. Thus, I tried to mock this functionality with really awesome mocking library FakeItEasy. I simply extended unit test class by mocking this functionality inside constructor (xUnit way of unit test class initialization), e.g.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
using FakeItEasy; using Jenx.Xamarin.UnitTestExample.ViewModels; using Xamarin.Forms; using Xamarin.Forms.Internals; using Xunit; namespace Jenx.Xamarin.UnitTestExample.UnitTests.ViewModels { public class MainViewModelUnitTests { public MainViewModelUnitTests() { var platformServicesFake = A.Fake<IPlatformServices>(); Device.PlatformServices = platformServicesFake; } // Code removed for brevity [Fact] public void Execute_FillPlatformTypeCommand_UserEntryFilled() { // arrange var vm = new MainViewModel(); // act vm.FillPlatformTypeCommand.Execute(null); // assert Assert.Equal("Unknown was here!", vm.UserEntry); } } } |
As a result, I got my unit tests positive!
Example of unit testing ICommand which calls Device APIs.
Now, with unit test working, I included additional unit test. I tried to put a check if underlying Device utility gets correct url to navigate to from my command.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
using FakeItEasy; using Jenx.Xamarin.UnitTestExample.ViewModels; using System; using Xamarin.Forms; using Xamarin.Forms.Internals; using Xunit; namespace Jenx.Xamarin.UnitTestExample.UnitTests.ViewModels { public class MainViewModelUnitTests { private readonly IPlatformServices _platformServicesFake; public MainViewModelUnitTests() { _platformServicesFake = A.Fake<IPlatformServices>(); Device.PlatformServices = _platformServicesFake; } [Fact] public void Execute_OpenJenxSiInBrowserCommand_PlatformOpenUriActionIsCalled() { // arrange var vm = new MainViewModel(); // act vm.OpenJenxSiInBrowserCommand.Execute(null); // assert A.CallTo(() => _platformServicesFake.OpenUriAction(new Uri("https://www.jenx.si"))).MustHaveHappened(); } // Code removed for brevity } } |
Conclusion.
In short, if you extend your Xamarin Forms with unit tests, you will surely hit “You MUST call Xamarin.Forms.Init()” exception (technically speaking System.InvalidOperationException
).
This blog post shows one way how to handle this situation by mocking Device.PlatformServices
.
How you handled this situation?