简体   繁体   English

在我的情况下,如何在单元测试异步方法时引发事件?

[英]How to raise an event when unit testing asynchronous method in my case?

I use MS-Test, moq 4.18.2 and FileSystem (System.IO.Abstractions) 17.0.24 for my tests.我使用 MS-Test、moq 4.18.2 和 FileSystem (System.IO.Abstractions) 17.0.24 进行测试。

I think I wrote a correct test for InfoLoader_LoadInfoAsync .我想我为InfoLoader_LoadInfoAsync写了一个正确的测试。 But, I don't understand how to write a test for MyViewModel::StartLoadInfoAsync to check that InfoList was populated correctly.但是,我不明白如何为MyViewModel::StartLoadInfoAsync编写测试以检查InfoList是否正确填充。 It seems that I have to duplicate instantiation and configuration of InfoLoader as I did in InfoLoader_LoadInfoAsync .看来我必须像在InfoLoader_LoadInfoAsync中那样复制InfoLoader的实例化和配置。 Is there a way around this?有没有解决的办法? How such things are usually tested?通常如何测试这些东西?

public abstract class IInfoLoader
{
    public event Action<MyInfo> InfoLoaded;
    public abstract Task LoadInfoAsync();

    protected void OnInfoLoaded(MyInfo info)
    {
        InfoLoaded?.Invoke(info);
    }
}
public class InfoLoader : IInfoLoader
{
    private readonly IFileSystem _fileSystem;
    private readonly string _path;

    public InfoLoader(string path, IFileSystem fileSystem) {...}
    
    public async override Task LoadInfoAsync()
    {
        foreach (var data in await _fileSystem.File.ReadAllLinesAsync(_path))
            OnInfoLoaded(new MyInfo(...));
    }
}
public class MyViewModel
{
    private IInfoLoader _infoLoader;
    public ObservableCollection<MyInfo> InfoList { get; }

    public MyViewModel(IInfoLoader infoLoader) { ... }

    public Task StartLoadInfoAsync()
    {
        _infoLoader.InfoLoaded += (info) =>
        {
            InfoList.Add(info);
        };
        return _infoLoader.LoadInfoAsync();
    }
}

Tests测试

[TestMethod]
public async Task InfoLoader_LoadInfoAsync_Success()
{
    var path = "...";
    var lines = new string[] { "name1", "name2" };
    var expectedInfoList = new List<MyInfo>();
    foreach(var line in lines)
        expectedInfoList.Add(new MyInfo(line));

    var fileSystem = new Mock<IFileSystem>();
    fileSystem.Setup(fs => fs.File.ReadAllLinesAsync(path, CancellationToken.None))
                .ReturnsAsync(lines);

    var actualInfoList = new List<MyInfo>();
    var infoLoader = new InfoLoader(path, fileSystem.Object);
    infoLoader.InfoLoaded += (info) => actualInfoList.Add(info);
    await infoLoader.LoadInfoAsync();

    // Assert that items in expectedInfoList and actualInfoList are equal
}
[TestMethod]
public async Task MyViewModel_StartLoadInfoAsync_Success()
{
    var expectedInfoList = new List<MyInfo>();
    
    // WHAT DO I DO HERE? DO I CREATE AND CONFIGURE infoLoader LIKE in "InfoLoader_LoadInfoAsync" TEST?
    
    var vm = new MyViewModel(infoLoader.Object);
    await vm.StartLoadInfoAsync();
    actualInfoList = vm.InfoList;
    
    // Assert that items in expectedInfoList and actualInfoList are equal
}

In order to test StartLoadInfoAsync you need an instance of MyViewModel , so you should:为了测试StartLoadInfoAsync你需要一个MyViewModel的实例,所以你应该:

  1. Create this instance.创建此实例。
  2. Invoke the method StartLoadInfoAsync .调用方法StartLoadInfoAsync
  3. Assert that its state is according to what you need.断言其 state 符合您的需要。

Now obviously you have a dependency, which is InfoLoader , so you have two options:现在显然您有一个依赖项,即InfoLoader ,因此您有两个选择:

  1. Create and configure a new instance of InfoLoader创建并配置InfoLoader的新实例
  2. Mock InfoLoader so you can test MyViewModel independently of InfoLoader .模拟InfoLoader以便您可以独立于InfoLoader MyViewModel

The second approach is what you may want to follow, this way you do not need to configure again InfoLoader , mock the FileSystem and so on.第二种方法是您可能想要遵循的,这样您就不需要再次配置InfoLoader ,模拟FileSystem等等。

You only need to create a mock of InfoLoader and setup its calls, just like you did with the FileSystem .您只需要创建一个InfoLoader模拟并设置它的调用,就像您对FileSystem所做的那样。

Since the view model depends on the IInfoLoader abstraction, it can be mocked to behave as expected when the desired member is invoked.由于视图 model 依赖于IInfoLoader抽象,因此可以模拟它以在调用所需成员时按预期运行。

Review the comments in the following example查看以下示例中的注释

[TestMethod]
public async Task MyViewModel_StartLoadInfoAsync_Success() {
    //Arrange
    var info = new MyInfo();
    List<MyInfo> expectedInfoList = new List<MyInfo>() { info };
    
    // WHAT DO I DO HERE?
    var dependency = new Mock<IInfoLoader>(); //mock the dependency
    //
    dependency.Setup(_ => _.LoadInfoAsync())
        // use callback to raise event passing the custom arguments expected by the event delegate
        .Callback(() => dependency.Raise(_ => _.InfoLoaded += null, info))
        // allow await LoadInfoAsync to complete properly
        .Returns(Task.CompletedTask); 
    
    MyViewModel subject = new MyViewModel(dependency.Object);
    
    //Act
    await subject.StartLoadInfoAsync();
    
    
    //Assert
    List<MyInfo> actualInfoList = subject.InfoList;
    
    actualInfoList.Should().NotBeEmpty()
        And.BeEquivalentTo(expectedInfoList); //Using FluentAssertions
    
}

Note how a Callback is used to capture when LoadInfoAsync is invoked by the subject so that an event can be raised by the mock, allowing the subject under test to flow to completion as desired请注意,当主体调用LoadInfoAsync时如何使用Callback来捕获,以便模拟可以引发事件,从而允许被测主体根据需要完成

Reference MOQ Quickstart: Events参考MOQ 快速入门:活动

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM