I have been working on Prism for Xamarin.Forms for a long time now, and one thing kept causing me nothing but headaches… testing anything that involves the Xamarin.Forms Application class.  I had a work-around in place for awhile now, but it started causing me issues trying to automate my CI process.  The work-around involved creating a new Test build configuration with a number of #ifdefs.  While this worked, nothing but pain came during my CI build scripts.  So, I had to figure this out once and for all.

The Problem

Running tests against the Xamarin.Forms Application class is a well known pain in the butt.  Let’s assume we have a simple test like this (I’m using xunit):

[Fact]
public void ApplicationIsNotNull()
{
    var app = new ApplicationMock();
    Assert.NotNull(app);
}

You would think that this seems like a pretty basic test that should pass with no problems.  Well, you would be wrong!  This fails immediately with the well known “System.InvalidOperationException : You MUST call Xamarin.Forms.Init(); prior to using it.” message.

Testing Xamarin.Forms Application class fails

The reason this exception occurs is because Xamarin.Forms is trying to initialize the platform code required to run the Application.  This is equivalent to:

  • Android (in MainActivity.cs): Xamarin.Forms.Forms.Init(this, bundle);
  • iOS in (AppDelegate.cs): Xamarin.Forms.Forms.Init();

Obviously we are not in one of those platforms, but rather a simple test library.  So, how do we fix this so that we can actually test our Xamarin.Forms code?

The Solution

In order to solve this problem, I had to dig into the Xamarin.Forms source code.  The first thing I did was search for the error message “You MUST call Xamarin.Forms.Init()”.  The search results found two instances of this message both in the Device.cs class.  One in the setter of the Info property and the other in the setter of the PlatformServices property.

If you follow that up the chain, you’ll see that for each platform in the Forms.Init() call, these values are being set based on the platform.  So this means we need to create our own Init() call for our unit tests!

Note: My test project is a .NET Core class library using XUnit

In our Init() method, we need to make sure we set the DeviceInfo.Info and Device.PlatformServices property to an appropriate mock object.  Let’s go ahead and create all of our supporting mock objects:

internal class MockPlatformServices : IPlatformServices
{
    Action<Action> _invokeOnMainThread;
    Action<Uri> _openUriAction;
    Func<Uri, CancellationToken, Task<Stream>> _getStreamAsync;

    public MockPlatformServices(Action<Action> invokeOnMainThread = null, Action<Uri> openUriAction = null, Func<Uri, CancellationToken, Task<Stream>> getStreamAsync = null)
    {
        _invokeOnMainThread = invokeOnMainThread;
        _openUriAction = openUriAction;
        _getStreamAsync = getStreamAsync;
    }

    public string GetMD5Hash(string input)
    {
        throw new NotImplementedException();
    }
    static int hex(int v)
    {
        if (v < 10)
            return '0' + v;
        return 'a' + v – 10;
    }

    public double GetNamedSize(NamedSize size, Type targetElement, bool useOldSizes)
    {
        switch (size)
        {
            case NamedSize.Default:
                return 10;
            case NamedSize.Micro:
                return 4;
            case NamedSize.Small:
                return 8;
            case NamedSize.Medium:
                return 12;
            case NamedSize.Large:
                return 16;
            default:
                throw new ArgumentOutOfRangeException("size");
        }
    }

    public void OpenUriAction(Uri uri)
    {
        if (_openUriAction != null)
            _openUriAction(uri);
        else
            throw new NotImplementedException();
    }

    public bool IsInvokeRequired
    {
        get { return false; }
    }

    public string RuntimePlatform { get; set; }

    public void BeginInvokeOnMainThread(Action action)
    {
        if (_invokeOnMainThread == null)
            action();
        else
            _invokeOnMainThread(action);
    }

    public Ticker CreateTicker()
    {
        return new MockTicker();
    }

    public void StartTimer(TimeSpan interval, Func<bool> callback)
    {
        Timer timer = null;
        TimerCallback onTimeout = o => BeginInvokeOnMainThread(() => {
            if (callback())
                return;

            timer.Dispose();
        });
        timer = new Timer(onTimeout, null, interval, interval);
    }

    public Task<Stream> GetStreamAsync(Uri uri, CancellationToken cancellationToken)
    {
        if (_getStreamAsync == null)
            throw new NotImplementedException();
        return _getStreamAsync(uri, cancellationToken);
    }

    public Assembly[] GetAssemblies()
    {
        return new Assembly[0];
    }

    public IIsolatedStorageFile GetUserStoreForApplication()
    {
        throw new NotImplementedException();
    }
}

internal class MockDeserializer : IDeserializer
{
    public Task<IDictionary<string, object>> DeserializePropertiesAsync()
    {
        return Task.FromResult<IDictionary<string, object>>(new Dictionary<string, object>());
    }

    public Task SerializePropertiesAsync(IDictionary<string, object> properties)
    {
        return Task.FromResult(false);
    }
}

internal class MockResourcesProvider : ISystemResourcesProvider
{
    public IResourceDictionary GetSystemResources()
    {
        var dictionary = new ResourceDictionary();
        Style style;
        style = new Style(typeof(Label));
        dictionary[Device.Styles.BodyStyleKey] = style;

        style = new Style(typeof(Label));
        style.Setters.Add(Label.FontSizeProperty, 50);
        dictionary[Device.Styles.TitleStyleKey] = style;

        style = new Style(typeof(Label));
        style.Setters.Add(Label.FontSizeProperty, 40);
        dictionary[Device.Styles.SubtitleStyleKey] = style;

        style = new Style(typeof(Label));
        style.Setters.Add(Label.FontSizeProperty, 30);
        dictionary[Device.Styles.CaptionStyleKey] = style;

        style = new Style(typeof(Label));
        style.Setters.Add(Label.FontSizeProperty, 20);
        dictionary[Device.Styles.ListItemTextStyleKey] = style;

        style = new Style(typeof(Label));
        style.Setters.Add(Label.FontSizeProperty, 10);
        dictionary[Device.Styles.ListItemDetailTextStyleKey] = style;

        return dictionary;
    }
}

internal class MockTicker : Ticker
{
    bool _enabled;

    protected override void EnableTimer()
    {
        _enabled = true;

        while (_enabled)
        {
            SendSignals(16);
        }
    }

    protected override void DisableTimer()
    {
        _enabled = false;
    }
}

internal class MockDeviceInfo : DeviceInfo
{
    public override Size PixelScreenSize => throw new NotImplementedException();

    public override Size ScaledScreenSize => throw new NotImplementedException();

    public override double ScalingFactor => throw new NotImplementedException();
}

Next, lets create our own static Init() method and make sure we properly instantiate our objects.  We can’t forget to register our mocks with the DependencyService.

public static void Init()
{
    Device.Info = new MockDeviceInfo();
    Device.PlatformServices = new MockPlatformServices();

    DependencyService.Register<MockResourcesProvider>();
    DependencyService.Register<MockDeserializer>();
}

Now, let’s update our test class to add a ctor and make a call to our custom Init() method.

public ResourcesFixture()
{
    Tests.Xamarin.Forms.Mocks.MockForms.Init();
}

Let’s re-run our tests, and BAM!  SUCCESS!

Testing Xamarin.Forms Application class passes

Now that this is properly mocks we can start testing all kinds of cool stuff.  We can test converters, navigation, resources, markup extensions, and all kinds of other interesting stuff.

Summary

Be sure to check out all the source code on GitHub.  As always, feel free contact me on my blog, connect with me on Twitter (@brianlagunas), or leave a comment below for any questions or comments you may have.

Brian Lagunas

View all posts

2 comments

  • Project file not formatted correctly to load project in Visual Studio 2015. Definitely interested in code as we are trying to move to TDD. Pulled your code, but will make own project to run it. Appreciate your work.

Follow Me

Follow me on Twitter, subscribe to my YouTube channel, and watch me stream live on Twitch.