MVVM:异步事件处理程序的单元测试

4

我有一个带有异步任务的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");
    }

测试失败了。


1
正如Netscape所指出的那样,你做错了。整个讨论都是毫无意义的。楼主显然不知道事件如何工作或异步工作流程应该如何工作。在讨论任何关于测试的内容之前,他需要学习这些知识。就目前而言,我认为这个问题没有希望。 - Maverik
@Maverik,我认为在事件触发时启动异步操作没有任何问题,尤其是在UI环境中。是的,我也建议不要使用事件,而是使用直接接口调用。但是事件调用者不需要关心它是否正在调用异步操作。 - Euphoric
1
@Maverik .Result和.Wait()都是hack。它们并不是解决方案。通常不鼓励使用它们。 - Euphoric
我们应该在Connect上开一张工单来废弃它们,但在没有其他同步上下文的情况下,我不知道这怎么能行。再次强调,这个讨论不是关于我们在使用await/Wait/Result方面的分歧,而是OP不知道如何使用这些东西。在他能够同步之前,测试是无法进行的。 - Maverik
@Yehudahasher 我并没有提供任何答案。这个问题有多个基本错误,只是陈述了显而易见的事实:在深入研究TDD之前,您需要更多地了解基本的C#语言行为。 - Maverik
显示剩余9条评论
2个回答

3

我向异步操作著名教授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处理程序则不行,但这是可以接受的,因为它不再具有任何真正的逻辑。

感谢Stephen!您的想法很出色!


好的,我觉得你有点把我弄糊涂了。在你的初始问题中(以及我的回答中),没有任何async void。而且我仍然不清楚你是否想在单元测试中运行长时间运行的任务,如果是的话,为什么要这样做?但是除了这些事情之外,我真的不明白你的答案如何帮助你解决问题?如果您能用自己的初始示例代码展示一下,我会非常感激。 - Markus
为了以后参考,Stephen提供的答案在他的博客评论区中。 - StuartLC

2

首先,我会将工作程序移动到另一个类中,并为其创建一个接口。这样,在运行测试时,我就可以注入另一个工作程序。

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...
    }
}

如果代码无法运行或者回答不好,请写下评论。 - Markus
你缺少了_worker字段和对_worker.DoSomething()的调用。我在想,_worker被调用在哪里了。 - Euphoric
此外,我认为提取DoSomething用于测试并没有意义,因为我假设这是实际被测试的代码。 - Euphoric
谢谢@Euphoric,问题已解决。 关于提取部分,这取决于代码的功能...例如,如果DoSomething写入大文件或进行一些数据库操作,则可能不应在单元测试中进行测试。 - Markus
在您的确切示例中,Data.Status IMO 应该从视图模型中设置。更新长时间运行的任务完成后,VM有责任更新自身。 - Euphoric
显示剩余5条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接