关于 maui-unit-testing
这项 .NET MAUI 测试技能涵盖单元测试配置、ViewModel 模式和常见陷阱。它帮助开发者将 MAUI 平台 API 隔离在可注入接口后,使 ViewModel 更易测试。
主要功能
- 01测试目标框架与项目输出配置建议
- 02为 MAUI API 使用可注入接口
- 03测试 ViewModel 状态和命令
- 04MAUI 服务的模拟策略
- 05异步流程与 CI 测试建议
适用场景
- 01为 MAUI 应用配置单元测试
- 02无需启动 UI 即可测试 ViewModel
- 03用 dotnet test 执行回归检查
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ davidortinau/ maui-skills --skill maui-unit-testing安装后在 Claude Code 中输入/maui-unit-testing确认已加载。
附带文件references/unit-testing-api.md
详细介绍
.NET MAUI 单元测试——注意事项与最佳实践
项目模板、xUnit 示例、ViewModel 测试模式和 CLI 命令,请参阅 references/unit-testing-api.md。
⚠️ TFM 陷阱:不要使用特定平台的 TFM
<TargetFramework>net9.0-ios</TargetFramework>
<TargetFrameworks>net9.0;net10.0</TargetFrameworks>
⚠️ OutputType 陷阱:将应用项目用作测试依赖项
如果测试项目引用了应用项目,应用项目会尝试以测试 TFM 的 Exe 类型构建——这会失败。请添加条件 OutputType:
<OutputType>Exe</OutputType>
<PropertyGroup Condition="'$(TargetFramework)' == 'net9.0'">
<OutputType>Library</OutputType>
</PropertyGroup>
<PropertyGroup Condition="'$(TargetFramework)' != 'net9.0'">
<OutputType>Exe</OutputType>
</PropertyGroup>
常见错误
❌ 在 ViewModel 中使用 MAUI 静态 API
直接调用 MAUI 静态 API 的 ViewModel 无法测试:
// ❌ Untestable — Shell.Current requires a running MAUI app
public async Task GoToDetail(int id)
=> await Shell.Current.GoToAsync($"detail?id={id}");
// ✅ Inject an interface — fully testable
public class MyViewModel(INavigationService nav)
{
public async Task GoToDetail(int id)
=> await nav.GoToAsync($"detail?id={id}");
}
应通过接口封装的静态 API:
Shell.Current→INavigationServiceApplication.Current→ 完全避免使用SecureStorage.Default→ISecureStorageConnectivity.Current→IConnectivity
❌ 断言 UI 绑定,而不是 ViewModel 状态
// ❌ Testing the binding — fragile, needs a running UI
Assert.Equal("Hello", label.Text);
// ✅ Testing the ViewModel — fast, no platform dependency
Assert.Equal("Hello", viewModel.Title);
Assert.True(viewModel.SaveCommand.CanExecute(null));
MAUI 服务的模拟策略
| MAUI 服务 | 模拟策略 |
|---|---|
ISecureStorage | Mock<ISecureStorage>——为 GetAsync/SetAsync 设置存根 |
IPreferences | Mock<IPreferences>——为 Get/Set/Remove 设置存根 |
IConnectivity | Mock<IConnectivity>——返回 NetworkAccess |
IGeolocation | Mock<IGeolocation>——返回固定的 Location |
IFilePicker | Mock<IFilePicker>——返回 FileResult |
IMediaPicker | Mock<IMediaPicker>——返回 FileResult |
| Shell 导航 | 通过 INavigationService 抽象 |
IDispatcher | 设置存根,让 Dispatch 同步调用操作 |
架构:优先使用接口,提升可测试性
定义服务接口,让 ViewModel 完全不依赖 MAUI 平台:
// ✅ These make your entire ViewModel layer testable
public interface INavigationService
{
Task GoToAsync(string route);
Task GoBackAsync();
}
public interface IDialogService
{
Task<bool> ConfirmAsync(string title, string message);
}
在 MauiProgram.cs 中注册实现,并将接口注入 ViewModel。
提示
- ViewModel 中不要使用
Application.Current或Shell.Current——通过可注入服务封装 - 使用
ObservableCollection<T>和[ObservableProperty](MVVM Toolkit)管理可测试状态 - 断言 ViewModel 属性和
CanExecute,而不是 UI 绑定 - 使用
TaskCompletionSource测试异步等待流程 - 在 CI 中运行
dotnet test,及早发现回归问题 - 设备端测试:使用
xunit.runner.devices测试真实平台 API(传感器、相机、蓝牙)
检查清单
- 测试项目目标框架为普通的
net9.0/net10.0(不是特定平台的 TFM) - 应用项目针对测试 TFM 设置了条件
OutputType - 所有 MAUI 静态 API 都通过可注入接口封装
- 通过属性和命令测试 ViewModel,而不是 UI 绑定
- 在测试中模拟
IDispatcher,使其同步调用 - CI 中的
dotnet test运行通过
评论
支持 Markdown