Axemasta.Moq.INavigationService 1.0.3

dotnet add package Axemasta.Moq.INavigationService --version 1.0.3
NuGet\Install-Package Axemasta.Moq.INavigationService -Version 1.0.3
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Axemasta.Moq.INavigationService" Version="1.0.3" />
For projects that support PackageReference, copy this XML node into the project file to reference the package.
paket add Axemasta.Moq.INavigationService --version 1.0.3
#r "nuget: Axemasta.Moq.INavigationService, 1.0.3"
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
// Install Axemasta.Moq.INavigationService as a Cake Addin
#addin nuget:?package=Axemasta.Moq.INavigationService&version=1.0.3

// Install Axemasta.Moq.INavigationService as a Cake Tool
#tool nuget:?package=Axemasta.Moq.INavigationService&version=1.0.3

Moq.INavigationService

Enables Moq's Verify API over the INavigationService extensions in Prism.Maui.

This package is directly inspired by, and uses the design pattern of Moq.ILogger. I found the design interesting and really liked the API it exposes for testing.

Moq.ILogger CI NuGet Shield

If you are using a preview version of Prism 9 that is either from Sponsor Connect or self hosted, you will likely have issues using this package from NuGet. My recommendation is to pull the source code and reference it locally to avoid any issues.

Usage

A Mock<INavigationService object isn't enough for the current prism implementation, there is extra setup required. You must use the MockNavigationService object in your tests and pass it directly (it implemented INavigationService).

var mockNavigationService = new MockNavigationService();
var sut = new FooViewModel(mockNavigationService);

Setup

To setup navigation in your tests, use the SetupNavigation api. There are a few options you can provide:

  • string
navigationService.SetupNavigation("HomePage")
  • string, INavigationParameters
navigationService.SetupNavigation("HomePage", It.Is<INavigationParameters>(n => n.ContainsKey("KeyOne")))
  • Uri
navigationService.SetupNavigation(new Uri("NavigationPage/HomePage"))
  • Uri, INavigationParameters
navigationService.SetupNavigation(new Uri("NavigationPage/HomePage"), It.Is<INavigationParameters>(n => n.ContainsKey("KeyOne"))
  • Navigation Service Expression
// Uri Api
navigationService.SetupNavigation(nav => nav.NavigateAsync("HomePage"))

// Builder Api
navigationService.SetupNavigation(nav => 
    nav.CreateBuilder()
    .AddSegment<HomePage>()
    .NavigateAsync())

All of these setup methods plug into Moq and you should determine the behaviour based on your project (ie ReturnsAsync or ThrowsAsync). A complete setup would look like:

var expectedNavigationResult = new NavigationResult();

// Setup using uri api
navigationService.SetupNavigation("HomePage")
    .ReturnsAsync(expectedNavigationResult);

// Setup using navigation builder api
navigationService.SetupNavigation(nav => nav.CreateBuilder()
        .AddSegment<HomePage>()
        .AddParameter(KnownNavigationParameters.UseModalNavigation, true)
        .NavigateAsync())
    .ReturnsAsync(expectedNavigationResult);

There is also a generic setup method that will cause all navigations to return regardless of the specific navigation that occurs:

// All navigation returns true:
navigationService.SetupAllNavigationReturns(true);

// All navigation returns false:
navigationService.SetupAllNavigationReturns(false);


// All navigation fails with exception:
var expectedException = new Exception("Navigation fell over and the app crashed");
navigationService.SetupAllNavigationFails(expectedException);

Verify

To verify navigation in your tests, use the VerifyNavigation api & provide the expression you wish to verify:

navigationService.VerifyNavigation(
    nav => nav.NavigateAsync("HomePage"),
    Times.Once());

this also works for the navigation builder:

navigationService.VerifyNavigation(
    nav => nav.CreateBuilder()
        .AddSegment<HomePage>()
        .NavigateAsync(),
    Times.Once());

Not everything has been setup for mocking in this library, such as the TabbedSegmentBuilder, I haven't yet implemented a mock for it. I found that mocking the builder expressions extremely difficult and the implementation is far from ideal (I'm basically recreating the call and comparing the results).

If the verification expression fails, it will throw a VerifyNavigationException, this is a problem on your end and either your code or test is wrong.

If the a VerifyNavigationUnknownException is thrown, this is an issue with the library, please raise an issue with details of the exception.

Why?

This package exists to enable unit testing of Prism.Maui's INavigationService due to the vast majority of the navigation methods being Extension methods.

In Xamarin these methods exists on the INavigationService interface so could be mocked directly:

Xamarin Example:

This couldn't have been easier to test and was a primary reason Prism was so essential to Xamarin Forms apps.

ViewModel

public class FooViewModel
{
    private readonly INavigationService navigationService;

    public FooViewModel(INavigationService navigationService)
    {
        this.navigationService = navigationService;
    }

    public async Task NavigateToBar()
    {
        await navigationService.NavigateAsync("BarPage");
    }
}

Test

[Fact]
public async Task NavigateToBar_WhenCalled_ShouldNavigateToBarPage()
{
    // Arrange
    var mockNavigationService = new Mock<INavigationService>();
    var sut = new FooViewModel(mockNavigationService.Object);

    // Act
    await sut.NavigateToBar();

    // Assert
    mockNavigationService.Verify(
        m => m.NavigateAsync("BarPage"), 
        Times.Once(),
        "Navigation to destination: BarPage should have occurred");
}

Maui Example:

If we try this test in maui the following exception will be thrown when running the mock verification:

System.NotSupportedException : Unsupported expression: m => m.NavigateAsync("BarPage")
Extension methods (here: INavigationServiceExtensions.NavigateAsync) may not be used in setup / verification expressions.

The solution to this is understanding how the extension methods work, the extension will call the underlying interface navigation method: Task<INavigationResult> NavigateAsync(Uri uri, INavigationParameters parameters);

We can update our test to use this overload to make our tests pass:

[Fact]
public async Task NavigateToBar_WhenCalled_ShouldNavigateToBarPage()
{
    // Arrange
    var mockNavigationService = new Mock<INavigationService>();
    var sut = new FooViewModel(mockNavigationService.Object);

    // Act
    await sut.NavigateToBar();

    // Assert
    mockNavigationService.Verify(
        m => m.NavigateAsync(
			It.Is<Uri>(u => u.Equals("BarPage")),
			null),
        Times.Once(),
        "Navigation to destination: BarPage should have occurred");
}

This approach is fine however it requires a more technical understanding of the prism library under the hood and for more complicated navigation calls or if you want to use the new INavigationBuilder, verification becomes a more difficult endeavour.

The purpose of this library is to make testing prism maui viewmodels easier by acting as a testing helper library. I have opened this as a discussion on the main prism repo and all code is welcome to be used by prism, I am publishing this repo to NuGet due to the personal need for this library.

Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last updated
1.0.3 122 4/3/2024
0.1.36-alpha 145 9/8/2023
0.1.33-alpha 97 8/29/2023
0.1.24-alpha 78 8/23/2023
0.1.20-alpha 77 8/23/2023
0.1.17-alpha 94 8/23/2023
0.1.7-alpha 68 8/23/2023