Testing

Describes how test MauiReactor components

Testing an application usually involves 3 different kinds of tests.

  1. Unit Tests: Tests of single functions or classes aiming to prove the correctness of a single behavior or feature or absence of a specific issue. You can create unit tests for your .NET MAUI app as you would for any other c# program.

  2. Component or Widget Tests: These kinds of tests are specific to UI apps that use an MVU framework and serve to prove the quality of single components. MauiReactor provides some neat classes and functions that let you test MauiReactor components: this article describes how to use them.

  3. Integration Tests: This kind of test, generally, involves the loading of external tools that simulate the real environment, the execution of the app with different settings, and the simulation of a user interacting with the app. You could create integration tests for MauiReactor using tools like https://appium.io/

Preliminary steps

First, you need to modify your .NET MAUI application project to be linked to a test project.

Open your project definition; it should be something like the following:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFrameworks>net8.0-android;net8.0-ios;net8.0-maccatalyst</TargetFrameworks>
    <TargetFrameworks Condition="$([MSBuild]::IsOSPlatform('windows'))">$(TargetFrameworks);net7.0-windows10.0.19041.0</TargetFrameworks>
    <OutputType>Exe</OutputType>
    <RootNamespace>KeeMind</RootNamespace>
    <UseMaui>true</UseMaui>
    <SingleProject>true</SingleProject>
    <Nullable>enable</Nullable>
    ....   
  </PropertyGroup>
</Project>

Add the target framework net8.0 (or the current one the app is targeting) and put a condition on the OutputType header so that the MSBuild task doesn't produce an exe under the plain net8.0 framework.

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFrameworks>net8.0-android;net8.0-ios;net8.0-maccatalyst;net8.0</TargetFrameworks>
    <TargetFrameworks Condition="$([MSBuild]::IsOSPlatform('windows'))">$(TargetFrameworks);net7.0-windows10.0.19041.0</TargetFrameworks>
    <OutputType Condition="'$(TargetFramework)' != 'net8.0'">Exe</OutputType>
    <RootNamespace>KeeMind</RootNamespace>
    <UseMaui>true</UseMaui>
    <SingleProject>true</SingleProject>
    <Nullable>enable</Nullable>
    ....  
  </PropertyGroup>
</Project>

This way the app project can be referenced in the test project.

Now let's create a test project, and choose the framework you like most (MSTest, xUnit, nUnit, etc).

As a final step, you have to reference the MAUI controls library in the test project adding the <UseMaui> header in the project definition:

  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
    <UseMaui>true</UseMaui>

    <IsPackable>false</IsPackable>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.Maui.Controls" Version="8.0.*" />
    <PackageReference Include="Microsoft.Maui.Controls.Compatibility" Version="8.0.*" />
  </ItemGroup>
  

Test components

The TemplateHost is the key class to use when you want to test a component: it creates a virtual tree of nodes starting from the component you pass to it as the constructor parameter.

To create a TemplateHost just use a code like this: TemplateHost.Create(new MyComponent());

As you have the template host you can use some helper methods on it to traverse the tree to access native controls, check properties, and raise events.

For example, say we want to test this component:

new ContentPage
{
    new VStack
    {
        new Label($"Counter: {State.Counter}"),

        new Button("Click To Increment", () =>
            SetState(s => s.Counter++))
    }
}

First, we need to add a way for each control to be identified, one easy way is to augment the code with the AutomationId() method like it's shown in the following snippet:

new ContentPage
{
    new VStack
    {
        new Label($"Counter: {State.Counter}")
            .AutomationId("Counter_Label"),

        new Button("Click To Increment", () =>
            SetState(s => s.Counter++))
            .AutomationId("Counter_Button")
    }
}

We can finally create a test to verify that the counter is working:

var mainPageNode = TemplateHost.Create(new CounterWithServicePage());

// Check that the counter is 0
mainPageNode.Find<MauiControls.Label>("Counter_Label")
    .Text
    .ShouldBe($"Counter: 0");

// Click on the button
mainPageNode.Find<MauiControls.Button>("Counter_Button")
    .SendClicked();

// Check that the counter is 1
mainPageNode.Find<MauiControls.Label>("Counter_Label")
    .Text
    .ShouldBe($"Counter: 1");

TemplateHost has many find overloads that let you traverse the tree of controls to find the one you need to test.

Components that inject services

In case your components need services from DI you have to inject them before creating the TemplateHost through the ServiceContext as shown below:

using var serviceContext = new ServiceContext(services => services.AddSingleton<IncrementService>());
var mainPageNode = TemplateHost.Create(new CounterWithServicePage());
...

You have to dispose of the ServiceContext object at the end of the test, even better, wrap it with the using clause

Attach components created with a page or modal

Often your components are hosted on a page that is created at runtime for example when the user pushes a button. In this case, you need to "attach" the new component using the NavigationContainer class as shown in the following sample code:

using var navigationContainer = new NavigationContainer();

var mainPageNode = TemplateHost.Create(new NavigationMainPage());

// Verify that initially the value is 0
mainPageNode.Find<MauiControls.Label>("MainPage_Label")
    .Text
    .ShouldBe("Value: 0");

// Click the button to open the second page
mainPageNode.Find<MauiControls.Button>("MoveToChildPage_Button")
    .SendClicked();

// here we attach the new component created after the button is clicked
var childPageNode = navigationContainer.AttachHost();

// se entry text to 12
childPageNode.Find<MauiControls.Entry>("ChildPage_Entry")
    .Text = "12";

// click the button to go back to main page
childPageNode.Find<MauiControls.Button>("MoveToMainPage_Button")
    .SendClicked();

// Verify that now the label reports the updated text
mainPageNode.Find<MauiControls.Label>("MainPage_Label")
    .Text
    .ShouldBe("Value: 12");

As for the ServiceConteineralso the NavigationContainer the object must be disposed of when the test ends; again it's a good idea to wrap it with the using keyword

Last updated