Skip to content
This repository was archived by the owner on Nov 8, 2025. It is now read-only.

Commit 25a5c28

Browse files
committed
Enhance MAUI test infrastructure and refactor tests
- Added `InternalsVisibleTo` for `IntegrationTests` in the project file. - Enhanced `IntegrationTestBase` with methods to initialize MAUI apps. - Introduced `TestMauiProgram` for test-specific MAUI app configuration. - Added `MockApplication` to support headless testing scenarios. - Documented testing patterns and limitations in `TESTING_PATTERN.md`. - Improved test cleanup documentation in `TEST_CLEANUP_SUMMARY.md`. - Refactored `ErrorHandlingTests`, `GoBackAsyncTests`, and `ShellNavigationTests`: - Removed invalid tests and mock-based setups. - Focused on testing navigation logic and route building. - Added `TestNavigation` to simulate `INavigation` behavior. - Ensured no production code changes were made to accommodate tests.
1 parent cf5123e commit 25a5c28

File tree

9 files changed

+751
-456
lines changed

9 files changed

+751
-456
lines changed

src/Plugin.Maui.SmartNavigation/Plugin.Maui.SmartNavigation.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
<ItemGroup>
1818
<InternalsVisibleTo Include="Plugin.Maui.SmartNavigation.MopupsExtensions" />
19+
<InternalsVisibleTo Include="Plugin.Maui.SmartNavigation.IntegrationTests" />
1920
</ItemGroup>
2021

2122
<ItemGroup>

tests/Plugin.Maui.SmartNavigation.IntegrationTests/Infrastructure/IntegrationTestBase.cs

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ public abstract class IntegrationTestBase : IDisposable
77
{
88
protected IServiceProvider ServiceProvider { get; private set; }
99
protected IServiceCollection Services { get; private set; }
10+
protected MauiApp? MauiApp { get; private set; }
11+
protected Application? App { get; private set; }
1012

1113
protected IntegrationTestBase()
1214
{
@@ -24,6 +26,50 @@ protected virtual void SetupServices(IServiceCollection services)
2426
// Derived classes can override to add their own services
2527
}
2628

29+
/// <summary>
30+
/// Initializes a MAUI application for testing using the host builder pattern.
31+
/// This properly initializes the MAUI infrastructure including DI, handlers, etc.
32+
/// </summary>
33+
/// <param name="mainPage">Optional main page to use. If null, a default page is created.</param>
34+
protected void InitializeMauiApp(Page? mainPage = null)
35+
{
36+
MauiApp = TestMauiProgram.CreateMauiApp(mainPage);
37+
App = MauiApp.Services.GetRequiredService<IApplication>() as Application;
38+
39+
if (App != null)
40+
{
41+
Application.Current = App;
42+
}
43+
}
44+
45+
/// <summary>
46+
/// Initializes a MAUI application with Shell for testing Shell-based navigation.
47+
/// </summary>
48+
protected void InitializeMauiAppWithShell()
49+
{
50+
MauiApp = TestMauiProgram.CreateMauiAppWithShell();
51+
App = MauiApp.Services.GetRequiredService<IApplication>() as Application;
52+
53+
if (App != null)
54+
{
55+
Application.Current = App;
56+
}
57+
}
58+
59+
/// <summary>
60+
/// Initializes a MAUI application with a regular page for testing non-Shell navigation.
61+
/// </summary>
62+
protected void InitializeMauiAppWithPage()
63+
{
64+
MauiApp = TestMauiProgram.CreateMauiAppWithPage();
65+
App = MauiApp.Services.GetRequiredService<IApplication>() as Application;
66+
67+
if (App != null)
68+
{
69+
Application.Current = App;
70+
}
71+
}
72+
2773
public void Dispose()
2874
{
2975
Dispose(true);
@@ -38,6 +84,14 @@ protected virtual void Dispose(bool disposing)
3884
{
3985
disposable.Dispose();
4086
}
87+
88+
// Clean up MAUI app
89+
if (MauiApp != null)
90+
{
91+
Application.Current = null;
92+
// MauiApp doesn't implement IDisposable, but we should clean up the reference
93+
MauiApp = null;
94+
}
4195
}
4296
}
4397
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
using Microsoft.Extensions.Logging;
2+
using Plugin.Maui.SmartNavigation.IntegrationTests.Mocks;
3+
4+
namespace Plugin.Maui.SmartNavigation.IntegrationTests.Infrastructure;
5+
6+
/// <summary>
7+
/// Provides a MAUI host builder for integration tests, similar to platform-specific entry points.
8+
/// This allows tests to work with a properly initialized MAUI application without requiring
9+
/// platform-specific UI infrastructure.
10+
/// </summary>
11+
public static class TestMauiProgram
12+
{
13+
/// <summary>
14+
/// Creates and configures a MauiApp instance for testing purposes.
15+
/// This mirrors the pattern used in platform entry points (AppDelegate, MainApplication, etc.)
16+
/// but uses test doubles instead of real UI components.
17+
/// </summary>
18+
/// <param name="mainPage">Optional main page to use for the application. If null, a default Page is created.</param>
19+
/// <returns>A configured MauiApp instance suitable for integration testing.</returns>
20+
public static MauiApp CreateMauiApp(Page? mainPage = null)
21+
{
22+
var builder = MauiApp.CreateBuilder();
23+
24+
builder
25+
.UseMauiApp<MockApplication>()
26+
.ConfigureFonts(fonts =>
27+
{
28+
// Fonts typically required for MAUI, even in headless tests
29+
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
30+
fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
31+
});
32+
33+
// Configure the main page if provided
34+
if (mainPage != null)
35+
{
36+
builder.Services.AddSingleton(mainPage);
37+
}
38+
39+
// Add test-specific services here as needed
40+
// builder.Services.AddTransient<IMyService, MyTestService>();
41+
42+
#if DEBUG
43+
builder.Logging.SetMinimumLevel(LogLevel.Trace);
44+
#endif
45+
46+
return builder.Build();
47+
}
48+
49+
/// <summary>
50+
/// Creates a MauiApp configured for Shell navigation testing.
51+
/// </summary>
52+
/// <returns>A configured MauiApp instance with Shell as the main page.</returns>
53+
public static MauiApp CreateMauiAppWithShell()
54+
{
55+
var shell = new Shell();
56+
return CreateMauiApp(shell);
57+
}
58+
59+
/// <summary>
60+
/// Creates a MauiApp configured for non-Shell navigation testing.
61+
/// </summary>
62+
/// <returns>A configured MauiApp instance with a regular Page as the main page.</returns>
63+
public static MauiApp CreateMauiAppWithPage()
64+
{
65+
var page = new ContentPage { Title = "Test Page" };
66+
return CreateMauiApp(page);
67+
}
68+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
namespace Plugin.Maui.SmartNavigation.IntegrationTests.Mocks;
2+
3+
/// <summary>
4+
/// Mock application for testing that can be used with MauiApp.CreateBuilder().UseMauiApp&lt;MockApplication&gt;()
5+
/// </summary>
6+
public class MockApplication : Application
7+
{
8+
private Page? _mainPage;
9+
10+
/// <summary>
11+
/// Default constructor required for MauiApp builder pattern
12+
/// </summary>
13+
public MockApplication()
14+
{
15+
}
16+
17+
/// <summary>
18+
/// Constructor for direct instantiation in tests (legacy pattern)
19+
/// </summary>
20+
public MockApplication(Page page) : this()
21+
{
22+
_mainPage = page;
23+
}
24+
25+
/// <summary>
26+
/// Sets the main page to be used when creating windows
27+
/// </summary>
28+
public void SetMainPage(Page page)
29+
{
30+
_mainPage = page;
31+
}
32+
33+
protected override Window CreateWindow(IActivationState? activationState)
34+
{
35+
// Use the configured main page, or create a default one if not set
36+
var page = _mainPage ?? new ContentPage { Title = "Mock Page" };
37+
return new Window(page);
38+
}
39+
}
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
# MAUI Application Testing Pattern - Solution Documentation
2+
3+
## Problem
4+
Integration tests needed to work with MAUI Application instances to test navigation scenarios, but creating testable Application/Window instances was failing because:
5+
1. Directly instantiating `Application` and calling `ActivateWindow()` doesn't populate the `Windows` collection
6+
2. The `Windows` collection requires platform-specific infrastructure that's not available in headless unit tests
7+
3. Previous attempts to mock or workaround this led to "testing the test" rather than testing actual code
8+
9+
## Solution: MauiApp Host Builder Pattern
10+
11+
The solution mirrors how platform entry points (AppDelegate on iOS, MainApplication on Android, etc.) initialize MAUI applications. These entry points call `CreateMauiApp()` which returns a properly configured `MauiApp` instance with:
12+
- Fully initialized dependency injection container
13+
- Service registrations
14+
- MAUI handlers and infrastructure
15+
- Properly configured Application instance
16+
17+
## Implementation
18+
19+
### 1. TestMauiProgram (Infrastructure/TestMauiProgram.cs)
20+
```csharp
21+
public static class TestMauiProgram
22+
{
23+
public static MauiApp CreateMauiApp(Page? mainPage = null)
24+
{
25+
var builder = MauiApp.CreateBuilder();
26+
builder.UseMauiApp<MockApplication>()
27+
.ConfigureFonts(/*...*/);
28+
// Configure services as needed
29+
return builder.Build();
30+
}
31+
}
32+
```
33+
34+
This provides test-specific variants:
35+
- `CreateMauiApp()` - Generic with optional page
36+
- `CreateMauiAppWithShell()` - Pre-configured with Shell
37+
- `CreateMauiAppWithPage()` - Pre-configured with ContentPage
38+
39+
### 2. Updated MockApplication (Mocks/MockApplication.cs)
40+
```csharp
41+
public class MockApplication : Application
42+
{
43+
// Parameterless constructor required for host builder
44+
public MockApplication() { }
45+
46+
// Optional legacy constructor for backward compatibility
47+
public MockApplication(Page page) : this() { /*...*/ }
48+
49+
protected override Window CreateWindow(IActivationState? activationState)
50+
{
51+
// Returns window with configured page
52+
}
53+
}
54+
```
55+
56+
Key change: Added parameterless constructor to support `UseMauiApp<MockApplication>()` pattern.
57+
58+
### 3. Enhanced IntegrationTestBase (Infrastructure/IntegrationTestBase.cs)
59+
```csharp
60+
public abstract class IntegrationTestBase : IDisposable
61+
{
62+
protected MauiApp? MauiApp { get; private set; }
63+
protected Application? App { get; private set; }
64+
65+
protected void InitializeMauiApp(Page? mainPage = null)
66+
{
67+
MauiApp = TestMauiProgram.CreateMauiApp(mainPage);
68+
App = MauiApp.Services.GetRequiredService<IApplication>() as Application;
69+
Application.Current = App;
70+
}
71+
72+
// Similar methods for Shell and Page variants
73+
}
74+
```
75+
76+
## Usage Pattern
77+
78+
### In Test Methods:
79+
```csharp
80+
[Fact]
81+
public void MyNavigationTest()
82+
{
83+
// Arrange - Initialize MAUI app
84+
InitializeMauiAppWithPage(); // or InitializeMauiAppWithShell()
85+
86+
// Application.Current is now properly set
87+
Application.Current.ShouldNotBeNull();
88+
89+
// Get services from DI container
90+
var navService = MauiApp.Services.GetRequiredService<INavigationService>();
91+
92+
// Act & Assert - test actual plugin code
93+
// ...
94+
}
95+
```
96+
97+
## Benefits
98+
99+
1. **Proper Initialization**: Application is initialized through the same path as production code
100+
2. **DI Container**: Full access to service provider for resolving dependencies
101+
3. **No Platform Dependencies**: Works in headless test environment
102+
4. **Matches Production**: Same pattern as platform entry points
103+
5. **Testable**: Can inject test services and mocks into the builder
104+
6. **Extensible**: Easy to add configuration for different test scenarios
105+
106+
## Current Limitations
107+
108+
- Windows collection may still be empty in headless environment (platform activation required)
109+
- UI-specific handlers and features may not be available
110+
- Platform-specific lifecycle events won't fire
111+
112+
## For UI-Level Testing
113+
114+
For tests that require actual platform UI handlers, window management, or lifecycle events, use:
115+
- Xamarin.UITest / Appium for full UI automation
116+
- Platform-specific test frameworks (XCTest, Espresso, etc.)
117+
118+
This pattern is ideal for:
119+
- Navigation service logic
120+
- Dependency injection
121+
- Service layer testing
122+
- Business logic that interacts with MAUI infrastructure
123+
124+
## Future Error Handling Tests
125+
126+
With this pattern, real error handling tests can now be written:
127+
128+
```csharp
129+
[Fact]
130+
public async Task NavigateToUnregisteredRoute_ShouldThrowInvalidOperationException()
131+
{
132+
// Arrange
133+
InitializeMauiAppWithShell();
134+
var navigationService = MauiApp.Services.GetRequiredService<ISmartNavigationService>();
135+
136+
// Act & Assert - testing ACTUAL plugin code
137+
var ex = await Should.ThrowAsync<InvalidOperationException>(() =>
138+
navigationService.GoToAsync("unregistered/route"));
139+
ex.Message.ShouldContain("not registered");
140+
}
141+
```
142+
143+
This tests real navigation service behavior, not mock setup code.

0 commit comments

Comments
 (0)