.NET Maui单元测试中读取MAUI资源文件的问题
解决MAUI xUnit测试中读取JSON文件的
NotImplementedInReferenceAssemblyException 问题 问题根源
单元测试项目中引用的Microsoft.Maui.Essentials是引用程序集,仅包含接口定义,没有平台特定的实现逻辑。而FileSystem.OpenAppPackageFileAsync的实际实现依赖平台环境,测试环境无法加载对应实现,因此抛出该异常;真机或模拟器运行时会加载平台专属实现,所以能正常工作。
可行解决方案
方案1:抽象文件读取逻辑,依赖注入解耦
通过抽象接口将文件读取逻辑与ViewModel分离,测试时替换为测试专用实现,这是最符合SOLID原则的方案。
- 定义文件服务接口
public interface IFileService { Task<Stream> OpenAppPackageFileAsync(string filename); }
- MAUI项目实现接口
public class MauiFileService : IFileService { public async Task<Stream> OpenAppPackageFileAsync(string filename) { return await FileSystem.OpenAppPackageFileAsync(filename); } }
- 修改ViewModel注入依赖
public class ContactsPageViewModel { private readonly IFileService _fileService; // 通过构造函数注入IFileService public ContactsPageViewModel(IFileService fileService) { _fileService = fileService; } public async Task LoadContacts() { using var stream = await _fileService.OpenAppPackageFileAsync("contacts.json"); using var reader = new StreamReader(stream); string contents = await reader.ReadToEndAsync(); List<Contact> distributors = JsonSerializer.Deserialize<List<Contact>>(contents); // 后续业务逻辑... } }
- MAUI项目注册服务
在MauiProgram.cs中添加服务注册:
builder.Services.AddSingleton<IFileService, MauiFileService>();
- 测试项目实现测试服务
public class TestFileService : IFileService { public async Task<Stream> OpenAppPackageFileAsync(string filename) { // 需将测试项目中的contacts.json设置为"复制到输出目录"(始终复制/较新时复制) string filePath = Path.Combine(AppContext.BaseDirectory, filename); return File.OpenRead(filePath); } } [Fact] public async Task TestLoadContacts() { var testFileService = new TestFileService(); var viewModel = new ContactsPageViewModel(testFileService); await viewModel.LoadContacts(); // 添加断言逻辑,例如验证联系人列表是否正确加载 }
方案2:直接读取测试项目中的物理文件
如果不想重构ViewModel,可以通过重载方法绕过FileSystem调用。
- ViewModel添加重载方法
// 新增接收Stream的重载方法 public async Task LoadContacts(Stream stream) { using var reader = new StreamReader(stream); string contents = await reader.ReadToEndAsync(); List<Contact> distributors = JsonSerializer.Deserialize<List<Contact>>(contents); } // 原方法调用重载 public async Task LoadContacts() { using var stream = await FileSystem.OpenAppPackageFileAsync("contacts.json"); await LoadContacts(stream); }
- 测试中读取物理文件
[Fact] public async Task TestLoadContacts() { // 测试项目中需将contacts.json设置为"复制到输出目录" string filePath = Path.Combine(AppContext.BaseDirectory, "contacts.json"); using var stream = File.OpenRead(filePath); var viewModel = new ContactsPageViewModel(); await viewModel.LoadContacts(stream); // 添加断言逻辑 }
方案3:使用Mock框架模拟文件系统行为
利用Moq等Mock框架模拟IFileSystem接口的行为(MAUI Essentials提供了IFileSystem接口用于依赖注入)。
- ViewModel注入IFileSystem
public class ContactsPageViewModel { private readonly IFileSystem _fileSystem; public ContactsPageViewModel(IFileSystem fileSystem) { _fileSystem = fileSystem; } public async Task LoadContacts() { using var stream = await _fileSystem.OpenAppPackageFileAsync("contacts.json"); using var reader = new StreamReader(stream); string contents = await reader.ReadToEndAsync(); List<Contact> distributors = JsonSerializer.Deserialize<List<Contact>>(contents); } }
- MAUI项目注册服务
builder.Services.AddSingleton<IFileSystem>(FileSystem.Current);
- 测试中Mock IFileSystem
[Fact] public async Task TestLoadContacts() { // 读取测试项目中的JSON内容 string filePath = Path.Combine(AppContext.BaseDirectory, "contacts.json"); string jsonContent = await File.ReadAllTextAsync(filePath); var mockStream = new MemoryStream(Encoding.UTF8.GetBytes(jsonContent)); // 模拟IFileSystem的OpenAppPackageFileAsync方法 var mockFileSystem = new Mock<IFileSystem>(); mockFileSystem.Setup(fs => fs.OpenAppPackageFileAsync("contacts.json")) .ReturnsAsync(mockStream); var viewModel = new ContactsPageViewModel(mockFileSystem.Object); await viewModel.LoadContacts(); // 添加断言逻辑,验证解析结果是否符合预期 }
内容的提问来源于stack exchange,提问作者John Hultgren
相关产品推荐
相关产品推荐

