UWP Prism Unit Test

I’m sure we all hear about unit testing, test driven development and so on. These practices are useful and provide a long list of benefit like writing low-coupled and mantainable code to name a few.

We want to write unit test for our UWP Prism application and we’d like, for example, to put our ViewModels under tests to make sure the logic they implement is correct. Unit testing is possible only if we can get rid of all the dependencies in our class under test because we want to run our test every time we want (for exemple when the service library our teammate is writing is not yet finished) and as fast as we can.

But this is not a problem. Since we developed our UWP app with Prism we can leverage the Dependency Injection pattern for our testing purpose. We’re going to create mock/stubs istances of the services needed by our viewmodels and test the inner logic of the methods. Creating manual mocks/stubs is error-prone and time-consuming so we need a mocking framework. In the UWP world we have little choice and we’ll use SimpleStubs. SimpleStubs is mantained by Microsoft Big Park Studios and it’s available as a NuGet package.

Let’s start.

Current limitations

Unit testing in the UWP world has some limitations. The first one is that you cannot test App but only libraries. The second one is that testing takes place inside an App that hosts the testing engine. This can be an issue if we think about headless processes like continuous integration and test run automation.

Solution structure

In order to unit test our view-models we need to separate them from the rest of the App in a dedicated assembly like this:

image

Create the test project

Right click on the solution in the Solution Explorer and we select Add –> New Project… . We select Unit Test App and we give a meaningful name. In this example we write IC6.Buongiorno.ViewModels.Tests. This name states that we’re working in the context of ViewModels and we’re going to put in this assembly tests about ViewModels.

image

SimpleStubs installation

As usual with Manage NuGet packages we search for SimpleStubs and download in the test project. In our example in the IC6.Buongiorno.ViewModels.Tests project.

image

The majority of mocking framework we know in the .Net classic framwork isn’t available for UWP because they use dynamic code generation which is limited in UWP. So SimpleStubs creates the mocks when building. Another limitaion prevents him from adding the file that contains the classes automatically so we need to create it manually the first time.

The file SimpleStubs.generated.cs is located in the directory Properties. To add it, in solution Explorer we right-click on Properties, Add, New Item and we create a new Class named SimpleStubs.generated.cs. Don’t worry about the content of the file: it will be completely overwritten by SimpleStubs at compile time.

image

Add references

As we stated at the start of this post we’re going to test a ViewModel so we need to set the references of the assembly of the view-models and of the services beacuse SimpleStubs will create the stubs for us disconvering the interfaces exposed in any referenced assembly. Right clicking on the References item in the Solution Explorer under the test project and we get something like this:

image

Now we compile the test solution and we can see that SimpleStubs replaced the content of the SimpleStubs.generated.cs file with content like this:

using System.Runtime.CompilerServices;
using Etg.SimpleStubs;
using System.Threading.Tasks;

namespace IC6.Buongiorno.Services.Weather
{
    [CompilerGenerated]
    public class StubIWeatherService : IWeatherService
    {
        private readonly StubContainer<StubIWeatherService> _stubs = new StubContainer<StubIWeatherService>();

        public MockBehavior MockBehavior { get; set; }

        void global::IC6.Buongiorno.Services.Weather.IWeatherService.Setup()
        {
            Setup_Delegate del;
            if (MockBehavior == MockBehavior.Strict)
            {
                del = _stubs.GetMethodStub<Setup_Delegate>("Setup");
            }
            else
            {
                if (!_stubs.TryGetMethodStub<Setup_Delegate>("Setup", out del))
                {
                    return;
                }
            }

            del.Invoke();
        }

        public delegate void Setup_Delegate();

        public StubIWeatherService Setup(Setup_Delegate del, int count = Times.Forever, bool overwrite = false)
        {
            _stubs.SetMethodStub(del, count, overwrite);
            return this;
        }

        public event global::System.EventHandler<global::IC6.Buongiorno.Services.Weather.WeatherInfo> WeatherChanged;

        protected void On_WeatherChanged(object sender, WeatherInfo args)
        {
            global::System.EventHandler<global::IC6.Buongiorno.Services.Weather.WeatherInfo> handler = WeatherChanged;
            if (handler != null) { handler(sender, args); }
        }

        public void WeatherChanged_Raise(object sender, WeatherInfo args)
        {
            On_WeatherChanged(sender, args);
        }

        public StubIWeatherService(MockBehavior mockBehavior = MockBehavior.Loose)
        {
            MockBehavior = mockBehavior;
        }
    }
}

namespace IC6.Buongiorno.Services.Twitter
{
    [CompilerGenerated]
    public class StubITwitterService : ITwitterService
    {
        private readonly StubContainer<StubITwitterService> _stubs = new StubContainer<StubITwitterService>();

        public MockBehavior MockBehavior { get; set; }

        global::System.Threading.Tasks.Task<global::IC6.Buongiorno.Services.Twitter.TwitterTimeline> global::IC6.Buongiorno.Services.Twitter.ITwitterService.GetTimelineAsync()
        {
            GetTimelineAsync_Delegate del;
            if (MockBehavior == MockBehavior.Strict)
            {
                del = _stubs.GetMethodStub<GetTimelineAsync_Delegate>("GetTimelineAsync");
            }
            else
            {
                if (!_stubs.TryGetMethodStub<GetTimelineAsync_Delegate>("GetTimelineAsync", out del))
                {
                    return Task.FromResult(default(global::IC6.Buongiorno.Services.Twitter.TwitterTimeline));
                }
            }

            return del.Invoke();
        }

        public delegate global::System.Threading.Tasks.Task<global::IC6.Buongiorno.Services.Twitter.TwitterTimeline> GetTimelineAsync_Delegate();

        public StubITwitterService GetTimelineAsync(GetTimelineAsync_Delegate del, int count = Times.Forever, bool overwrite = false)
        {
            _stubs.SetMethodStub(del, count, overwrite);
            return this;
        }

        public StubITwitterService(MockBehavior mockBehavior = MockBehavior.Loose)
        {
            MockBehavior = mockBehavior;
        }
    }
}

Writing a unit test

In this example we want to test the Update method of the MainPageViewModel. This method updates the Timeline property with a string that represents our Twitter timeline. We create a test with the classic pattern Assemble, Act and Assert like this:

using Microsoft.VisualStudio.TestTools.UnitTesting;
using IC6.Buongiorno.ViewModels;
using IC6.Buongiorno.Services.Twitter;
using IC6.Buongiorno.Services.Weather;
using Prism.Windows.Navigation;
using Prism.Windows.AppModel;
using Prism.Events;
using System.Threading.Tasks;
using System.Linq;

namespace IC6.Buongiorno.Tests
{
    [TestClass]
    public class UnitTest1
    {
        [TestMethod]
        public void UpdateMethodChangesTimeline()
        {
            //Assemble.
            string[] fakeTweets = { "ciao" };

            var fakeTimeline = new TwitterTimeline(fakeTweets);
            ITwitterService fakeTwitter = new StubITwitterService().GetTimelineAsync(() =>
            {
                return Task.FromResult(fakeTimeline);
            });
            IWeatherService fakeWeather = new StubIWeatherService();

            ISessionStateService fakeState = new StubSessionStateService();
            INavigationService fakeNavigation = new StubNavigationService();
            IEventAggregator fakeEventAggregator = new StubEventAggregator();

            //Act.
            var sut = new MainPageViewModel(fakeTwitter, fakeWeather, fakeNavigation, fakeState, fakeEventAggregator);
            sut.Update.Execute();

            //Assert.
            Assert.IsTrue(sut.Timeline.Contains(fakeTweets.First()));

        }
    }
}

In the Assemble section we create stubs of the services needed by the System Under Test (or SUT) that is our MainPageViewModel. The stubs StubIWeatherService and StubITwitterService are generated by SimpleStubs. We need to generate the other three stubs because those interfaces are from Prism and not exposed by our projects so SimpleStubs can’t help us. This is the code:

class StubEventAggregator : IEventAggregator
    {
        public TEventType GetEvent<TEventType>() where TEventType : EventBase, new()
        {
            return new TEventType();
        }
    }

 class StubNavigationService : INavigationService
    {
        public bool CanGoBack()
        {
            return true;
        }

        public bool CanGoForward()
        {
            return true;
        }

        public void ClearHistory()
        {

        }

        public void GoBack()
        {

        }

        public void GoForward()
        {

        }

        public bool Navigate(string pageToken, object parameter)
        {
            return true;
        }

        public void RemoveAllPages(string pageToken = null, object parameter = null)
        {

        }

        public void RemoveFirstPage(string pageToken = null, object parameter = null)
        {

        }

        public void RemoveLastPage(string pageToken = null, object parameter = null)
        {

        }

        public void RestoreSavedNavigation()
        {

        }

        public void Suspending()
        {

        }
    }

 class StubSessionStateService : ISessionStateService
    {
        public Dictionary<string, object> SessionState
        {
            get
            {
                return new Dictionary<string, object>();
            }
        }

        public Task<bool> CanRestoreSessionStateAsync()
        {
            return Task.FromResult(true);
        }

        public Dictionary<string, object> GetSessionStateForFrame(IFrameFacade frame)
        {
            return new Dictionary<string, object>();
        }

        public void RegisterFrame(IFrameFacade frame, string sessionStateKey)
        {
        }

        public void RegisterKnownType(Type type)
        {
        }

        public void RestoreFrameState()
        {
        }

        public Task RestoreSessionStateAsync()
        {
            return Task.FromResult(true);
        }

        public Task SaveAsync()
        {
            return Task.FromResult(true);
        }

        public void UnregisterFrame(IFrameFacade frame)
        {

        }
    }

As we can see the StubITwitterService we created has a more complex syntax. With that line of code we’re setting a customized behavior for the GetTimeLineAsync method and we are setting that it has to return the fakeTimeline we setup the two line of codes above The other stubs are simply placeholders and they won’t do anything so our code in the Update method will run smoothly.

In the assert section we expect the timeline to contain the text decided in the setup section.

We click Run All in the test Explorer and we can see the test passing.

Screenshot_1.png

TL;DR

In this blog post we’ve experimented a bit with unit testing in the UWP Prism app world. We’ve had to find some workarounds for the UWP limitations and to write manually some stubs. In the end we’ve written our unit test and run it successfully.

Troubleshooting

If you encounter the error “Unexepected error see the Test Pane Output for details” when everything looks fine I suggest to:

  1. Close Visual Studio
  2. delete bin and obj folder of the test project
  3. delete the %temp%\VisualStudioTestExplorerExtensions folder
  4. retry.

image

Reference

SimpleStubs GitHub official page (https://github.com/Microsoft/SimpleStubs)

Prism docs (http://prismlibrary.readthedocs.io/en/latest/)

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s