【问题标题】:MVVM: Unit test for asynchronous event handlerMVVM:异步事件处理程序的单元测试
【发布时间】:2015-11-10 06:49:58
【问题描述】:

我有一个带有异步任务的 viewModel。我现在不知道如何测试它。

public class MyViewModel : BindableBase
{
    public MyViewModel()
    {
        this.PropertyChanged += MyViewModel_PropertyChanged;
    }

    private void MyViewModel_PropertyChanged(object sender, PropertyChangedEventArgs e)
    {
        Action action = async () => await DoSomething();
        action();
    }

    public const string BeforeKey = "before";
    public const string AfterKey = "After";

    public string Status { get; private set; } = BeforeKey;

    public async Task DoSomething()
    {
        await Task.Delay(3000);
        Status = AfterKey;
    }

    string bindagleProp;
    public string BindagleProp
    {
        get { return bindagleProp; }
        set { SetProperty(ref bindagleProp, value); }
    }
}

这是我的测试:

[TestMethod]
public async Task TestMyViewModel()
{
    MyViewModel viewModel = new MyViewModel();
    Assert.AreEqual(viewModel.Status, MyViewModel.BeforeKey, "before check");

    viewModel.BindagleProp = "abc";
    Assert.AreEqual(viewModel.Status, MyViewModel.AfterKey, "after check");
}

测试失败,因为它没有等待任务完成。 我不想在单元测试中使用 Task.Delay,因为它不安全。 DoSomething 方法可以有未知的持续时间。

感谢您的帮助。


编辑:

事实上,这个问题并不特定于 MVVM,而是任何异步事件处理程序。 例如:

// class with some logic, can be UI or whatever.
public class MyClassA
{
    Size size;

    public Size Size
    {
        get { return size; }
        set
        {
            size = value;
            SizeChanged?.Invoke(this, EventArgs.Empty);
        }
    }

    public event EventHandler SizeChanged;
}

// this class uses the MyClassA class.
public class MyCunsomerClass
{
    readonly MyClassA myClassA = new MyClassA();

    public MyCunsomerClass()
    {
        myClassA.SizeChanged += MyClassA_SizeChanged;
    }

    public string Status { get; private set; } = "BEFORE";

    private async void MyClassA_SizeChanged(object sender, EventArgs e)
    {
        await LongRunningTaskAsync();
        Status = "AFTER";
    }

    public async Task LongRunningTaskAsync()
    {
        await Task.Delay(3000);
        ///await XYZ....;
    }

    public void SetSize()
    {
        myClassA.Size = new Size(20, 30);
    }
}

现在,我想测试一下:

    [TestMethod]
    public void TestMyClass()
    {
        var cunsomerClass = new MyCunsomerClass();
        cunsomerClass.SetSize();
        Assert.AreEqual(cunsomerClass.Status, "AFTER");
    }

测试失败。

【问题讨论】:

  • 完成的代码中是否会有await Task.Delay(3000);,或者你是用它来模拟一个长时间运行的任务?
  • 仅供模拟使用。真正的方法可以做任何长时间运行的任务。
  • 这可能已经有答案了here
  • 正如 Netscape 所指出的,你做错了。这整个讨论毫无意义。 OP 显然不知道事件如何工作或异步工作流应该如何工作。他需要先了解这些,然后才能讨论任何有关测试的内容。就目前而言,恕我直言,这个问题没有希望
  • @Maverik .Result 和 .Wait() 是黑客。不是解决方案。通常不鼓励使用它们。

标签: c# unit-testing asynchronous mvvm async-await


【解决方案1】:

好的,首先,我会将worker移到另一个类并为它创建一个接口。这样当我运行测试时,我可以注入另一个工人!

public class MyViewModel : BindableBase
{
    private IWorker _worker;

    private readonly DataHolder _data = new DataHolder(){Test = DataHolder.BeforeKey};
    public string Status { get { return _data.Status; } }

    public MyViewModel(IWorker worker = null)
    {
        _worker = worker;
        if (_worker == null)
        {
            _worker = new Worker();
        }

        this.PropertyChanged += MyViewModel_PropertyChanged;
    }

    private void MyViewModel_PropertyChanged(object sender, PropertyChangedEventArgs e)
    {

        Action action = async () => await _worker.DoSomething(_data);
        action();
    }


    string bindagleProp;
    public string BindagleProp
    {
        get { return bindagleProp; }
        set { SetProperty(ref bindagleProp, value); }
    }
}

public class DataHolder
{
    public const string BeforeKey = "before";
    public const string AfterKey = "After";

    public string Status;
}

public interface IWorker
{
    Task DoSomething(DataHolder data);
}

public class Worker : IWorker
{
    public async Task DoSomething(DataHolder data)
    {
        await Task.Delay(3000);
        data.Status = DataHolder.AfterKey;
    }
}

现在注入代码如下所示:

[TestMethod]
public async Task TestMyViewModel()
{
    TestWorker w = new TestWorker();

    MyViewModel viewModel = new MyViewModel(w);
    Assert.AreEqual(viewModel.Status, DataHolder.BeforeKey, "before check");

    viewModel.BindagleProp = "abc";
    Assert.AreEqual(viewModel.Status, DataHolder.AfterKey, "after check");
}

public class TestWorker : IWorker
{
    public Task DoSomething(DataHolder data)
    {
        data.Status = DataHolder.BeforeKey;
        return null; //you maybe should return something else here...
    }
}

【讨论】:

  • 如果它不起作用,或者为什么它是一个错误的答案,请写评论
  • 您缺少 _worker 字段并调用 _worker.DoSomething()。我想知道 _worker 在哪里调用。
  • 另外,我认为提取 DoSomething 进行测试没有意义,因为我假设它是实际正在测试的代码。
  • 感谢@Euphoric,已修复。关于提取部分,这取决于代码的作用。例如,DoSomething 写入一个大文件或一些数据库内容,那么这可能不应该在单元测试中进行测试。
  • @Euphoric 它是一个视图模型,为什么不将DoSomething 提取到模型中,或者让它成为服务层/扩展方法的一部分或其他什么?我不认为 Markus 正在提取用于测试,而是将其完全从 VM 中提取出来。
【解决方案2】:

我问了 Stehphen Cleary [著名的异步教授],他回答我:

如果“异步事件处理程序”是指async void 事件处理程序, 那么不,那些是不可测试的。但是,它们通常在 UI 中很有用 应用。所以我通常最终做的是让我所有的异步 void 方法正好是一行。它们看起来都是这样的:

async void SomeEventHandler(object sender, EventArgsOrWhatever args)
{
     await SomeEventHandlerAsync(sender, args);
}

async Task SomeEventHandlerAsync(object sender, EventArgsOrWhatever args)
{
      ... // Actual handling logic
}

那么async Task 版本是可单元测试、可组合等的。 async void 处理程序不是,但这是可以接受的,因为它不再 有任何真正的逻辑。

感谢斯蒂芬!你的想法很棒!

【讨论】:

  • 好的,我认为您在某个地方迷失了我。在您最初的问题(以及我的回答)中,任何地方都没有async void。我仍然不清楚您是否想在单元测试中运行长时间运行的任务,如果是的话,您为什么要这样做?但是除了这些,我真的不明白你的回答对你的问题有什么帮助?如果您能用自己的初始示例代码展示它,我将不胜感激。
  • 供以后参考,斯蒂芬提供的答案在他的Blog, over here的cmets中
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-06-23
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-12-01
相关资源
最近更新 更多