用于测试调用异步方法的ICommand的模式

时间:2016-04-26 13:33:04

标签: c# unit-testing async-await mvvmcross icommand

我正在研究单元测试的最佳实践(NUnit)ICommand,特别是MvxCommand

中的MVVMCross实现

查看模型

public ICommand GetAuthorisationCommand
{
    get { return new MvxCommand(
            async () => await GetAuthorisationToken(),
            () => !string.IsNullOrWhiteSpace(UserName) && !string.IsNullOrWhiteSpace(Password)); }
}

private async Task GetAuthorisationToken()
{
    // ...Do something async
}

单元测试

[Test]
public async Task DoLogonCommandTest()
{
    //Arrange
    ViewModel vm = new ViewModel(clubCache, authorisationCache, authorisationService);

    //Act
    await Task.Run(() => vm.GetAuthorisationToken.Execute(null));

    //Assert
    Assert.Greater(MockDispatcher.Requests.Count, 0);
}

现在我遇到的问题是测试在没有等待异步操作的情况下逐渐消失,从ICommand调用异步方法会感觉有些麻烦。

在单元测试这些ICommands和异步方法时是否有最佳实践?

3 个答案:

答案 0 :(得分:7)

您可以使用MvxAsyncCommand(请参阅:implementation)代替MvxCommand,并将已发布的GetAuthorisationCommand类型从ICommand更改为IMvxAsyncCommand (但是这个界面可以通过nuget获得),然后你可以调用

await vm.GetAuthorisationToken.ExecuteAsync();

答案 1 :(得分:3)

我认为the answer with MvxAsyncCommand是最好的长期解决方案。

但是,如果您想要在不依赖于预发布软件的情况下运行的东西,则可以遵循此模式which I have found helpful when dealing with asynchronous MVVM commands

首先,定义<!DOCTYPE html> <html> <head> <title></title> <link rel="stylesheet" type="text/css" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css"> <script src="https://code.jquery.com/jquery-1.12.3.min.js" integrity="sha256-aaODHAgvwQW1bFOGXMeX+pC4PZIPsvn2h1sArYOhgXQ=" crossorigin="anonymous"></script> <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js"></script> <style> .carousel-control { padding-top: 10%; width: 5%; } </style> </head> <body> <div class="container"> <div class="col-md-12"> <h1>Bootstrap 3 Thumbnail Slider</h1> <div class="well"> <div id="myCarousel" class="carousel slide"> <!-- Carousel items --> <div class="carousel-inner"> <div class="item active"> <div class="row"> <div class="col-sm-3"> <a href="#x"><img src="http://placehold.it/500x500" alt="Image" class="img-responsive"></a> </div> <div class="col-sm-3"> <a href="#x"><img src="http://placehold.it/500x500" alt="Image" class="img-responsive"></a> </div> <div class="col-sm-3"> <a href="#x"><img src="http://placehold.it/500x500" alt="Image" class="img-responsive"></a> </div> <div class="col-sm-3"> <a href="#x"><img src="http://placehold.it/500x500" alt="Image" class="img-responsive"></a> </div> </div> <!--/row--> </div> <!--/item--> <div class="item"> <div class="row"> <div class="col-sm-3"> <a href="#x" class="thumbnail"><img src="http://placehold.it/250x250" alt="Image" class="img-responsive"></a> </div> <div class="col-sm-3"> <a href="#x" class="thumbnail"><img src="http://placehold.it/250x250" alt="Image" class="img-responsive"></a> </div> <div class="col-sm-3"> <a href="#x" class="thumbnail"><img src="http://placehold.it/250x250" alt="Image" class="img-responsive"></a> </div> <div class="col-sm-3"> <a href="#x" class="thumbnail"><img src="http://placehold.it/250x250" alt="Image" class="img-responsive"></a> </div> </div> <!--/row--> </div> <!--/item--> <div class="item"> <div class="row"> <div class="col-sm-3"> <a href="#x" class="thumbnail"><img src="http://placehold.it/250x250" alt="Image" class="img-responsive"></a> </div> <div class="col-sm-3"> <a href="#x" class="thumbnail"><img src="http://placehold.it/250x250" alt="Image" class="img-responsive"></a> </div> <div class="col-sm-3"> <a href="#x" class="thumbnail"><img src="http://placehold.it/250x250" alt="Image" class="img-responsive"></a> </div> <div class="col-sm-3"> <a href="#x" class="thumbnail"><img src="http://placehold.it/250x250" alt="Image" class="img-responsive"></a> </div> </div> <!--/row--> </div> <!--/item--> </div> <!--/carousel-inner--> <a class="left carousel-control" href="#myCarousel" data-slide="prev">&#60;</a> <a class="right carousel-control" href="#myCarousel" data-slide="next">&#62;</a> </div> <!--/myCarousel--> </div> <!--/well--> </div> </div> <script language="javascript"> $(document).ready(function () { $('#myCarousel').carousel({ interval: 10000 }) $('#myCarousel').on('slid.bs.carousel', function () { //alert("slid"); }); }); </script> </body> </html>

IAsyncCommand

然后您可以定义interface IAsyncCommand: ICommand { Task ExecuteAsync(object parameter); } 实现:

AsyncCommand

然后在单元测试中使用public class AsyncCommand: MvxCommand, IAsyncCommand { private readonly Func<Task> _execute; public AsyncCommand(Func<Task> execute) : this(execute, null) { } public AsyncCommand(Func<Task> execute, Func<bool> canExecute) : base(async () => await execute(), canExecute) { _execute = execute; } public Task ExecuteAsync() { _execute(); } } 代替await command.ExecuteAsync()

答案 2 :(得分:0)

由于命令是火灾和遗忘事件,因此您无法直接完成任务。 我建议将测试分成两个动作(甚至创建两个单元测试)。

  1. 测试是否可以执行命令
  2. 测试异步任务是否返回预期结果
  3. 有些事情:

    //Act
    var canExecute = vm.GetAuthorisationToken.CanExecute();
    var result = await vm.GetAuthorisationToken();
    

    但是,需要GetAuthorisationToken将其保护级别从私有中更改,以便为单元测试公开它。

    <强>替代地

    您可以使用AsyncEx等库,这可以让您等待异步调用的完成。

    [Test]
    public async Task DoLogonCommandTest()
    {
        AsyncContext.Run(() =>
        {
            //Arrange
            ViewModel vm = new ViewModel(clubCache, authorisationCache, authorisationService);
    
            //Act
            await Task.Run(() => vm.GetAuthorisationToken.Execute(null));
        });
    
        //Assert
        Assert.Greater(MockDispatcher.Requests.Count, 0);
    }