Xamarin.Forms MVVMLight应用Xunit测试异常:登录命令测试无限运行
排查Xamarin ViewModel单元测试无限运行的问题
嘿,我来帮你分析下为什么你的测试会无限挂起,以及怎么修复它:
核心问题分析
你的测试卡住主要是几个原因叠加导致的:
- Async Void 方法无法被测试追踪:
LoginUser是async void类型的方法,这种方法在单元测试里调用时,测试线程不会等待异步操作完成。如果异步操作里有未完成的任务(比如未Mock的服务调用),测试就会一直挂起等待。 - 静态单例依赖未被Mock:
ServiceManager.Instance.LoginUser、CommonUtils.Instance.IsConnected这些静态单例调用,在测试环境下如果没有被替换成Mock实现,可能会执行实际的网络/本地数据操作,导致任务无限等待。 - UI线程操作在测试环境下无法执行:
Device.BeginInvokeOnMainThread在单元测试里没有真正的UI线程,这段代码可能会卡住,无法继续执行后续的IsBusy = false。 - 测试未等待异步操作完成:你调用
LoginCommand.Execute(null)后直接进行断言,但登录的异步逻辑还在后台运行,测试无法判断何时结束。
具体修复步骤
1. 把异步方法从Async Void改为Async Task
MVVMLight支持异步命令(AsyncRelayCommand,需要安装MvvmLight.Async NuGet包),把LoginUser改成返回Task,并更新命令定义:
// 修改LoginUser方法 async Task LoginUser() { if (IsBusy) return; IsBusy = true; LoginCommand.RaiseCanExecuteChanged(); var validationResults = Validator.Validate(ValidationContext); if (validationResults.IsValid) { var loggedIn = await LoginFlow(); if (loggedIn) { // 建议把页面导航逻辑移到INavigationService,而不是直接操作页面 await NavigationService.NavigateTo<HomeViewModel>(); } else { IsBusy = false; await DialogService.ShowMessage(AuthenticationAlerts.InvalidCredentials, AuthenticationAlerts.LoginFailed); } } else { IsBusy = false; await DialogService.ShowMessage(validationResults.Errors[0].ErrorMessage, AuthenticationAlerts.LoginFailed); } IsBusy = false; LoginCommand.RaiseCanExecuteChanged(); } // 更新命令定义 AsyncRelayCommand loginCommand; public ICommand LoginCommand => loginCommand ?? (loginCommand = new AsyncRelayCommand(LoginUser, () => !IsBusy));
2. 替换静态单例为可注入接口
把ServiceManager、DataManager、CommonUtils这些单例改成接口,注入到ViewModel中,方便Mock:
// 定义接口 public interface IServiceManager { Task<bool> LoginUser(User user); } public interface ICommonUtils { bool IsConnected { get; } } // 修改LoginViewModel构造函数 public LoginViewModel(INavigationService navigation, IDialogService dialog, IServiceManager serviceManager, ICommonUtils commonUtils) : base(navigation, dialog) { Title = PageTitles.LoginTitle; Validator = new UserValidator(); ValidationContext = new ValidationContext<User>(User, new PropertyChain(), new RulesetValidatorSelector(new[] { "Login", "Password" })); _serviceManager = serviceManager; _commonUtils = commonUtils; } // 修改LoginFlow方法 async Task<bool> LoginFlow() { return _commonUtils.IsConnected ? await _serviceManager.LoginUser(User) : await _dataManager.LoginUser(User); }
3. 在测试中Mock所有依赖
用Moq框架来Mock这些接口,避免执行实际操作:
public UnitTest1() { // 初始化Xamarin.Forms测试环境 Xamarin.Forms.Mocks.MockForms.Init(); // Mock Device.BeginInvokeOnMainThread,直接执行传入的Action Device.InvokeOnMainThreadAsync = action => { action(); return Task.CompletedTask; }; // Mock依赖服务 var mockServiceManager = new Mock<IServiceManager>(); mockServiceManager.Setup(x => x.LoginUser(It.IsAny<User>())).ReturnsAsync(true); var mockCommonUtils = new Mock<ICommonUtils>(); mockCommonUtils.Setup(x => x.IsConnected).Returns(true); var mockNavigation = new Mock<INavigationService>(); var mockDialog = new Mock<IDialogService>(); // 注册到ServiceLocator ServiceLocator.Current.RegisterInstance<IServiceManager>(mockServiceManager.Object); ServiceLocator.Current.RegisterInstance<ICommonUtils>(mockCommonUtils.Object); ServiceLocator.Current.RegisterInstance<INavigationService>(mockNavigation.Object); ServiceLocator.Current.RegisterInstance<IDialogService>(mockDialog.Object); app = new App(); loginViewModel = new LoginViewModel(mockNavigation.Object, mockDialog.Object, mockServiceManager.Object, mockCommonUtils.Object); }
4. 修改测试方法为异步,等待操作完成
把测试方法改成async Task,并等待登录命令执行完成:
[Theory] [InlineData("ACosta", "Time2Eat!")] public async Task Login_WithValidCredentials_ShouldSucceed(string username, string password) { var model = await CreateViewModelAndLogin(username, password); Assert.NotNull(model); Assert.False(model.IsBusy); // 验证IsBusy是否回到false // 可以添加更多断言,比如验证导航服务是否被调用 // mockNavigation.Verify(x => x.NavigateTo<HomeViewModel>(), Times.Once); } private async Task<LoginViewModel> CreateViewModelAndLogin(string userName, string password) { loginViewModel.User = new User { UserName = userName, Password = password }; // 等待异步命令执行完成 await ((AsyncRelayCommand)loginViewModel.LoginCommand).ExecuteAsync(null); return loginViewModel; }
额外建议
- 避免在ViewModel里直接操作页面(比如
new NavigationPage(new HomePage())),把导航逻辑封装到INavigationService中,这样测试时只需要Mock导航服务即可。 - 尽量减少静态成员的使用,依赖注入能让代码更易于测试和维护。
内容的提问来源于stack exchange,提问作者Nidhi Sood
相关产品推荐
相关产品推荐

