Home > Software design >  How to raise an event when unit testing asynchronous method in my case?
How to raise an event when unit testing asynchronous method in my case?

Time:08-14

I use MS-Test, moq 4.18.2 and FileSystem (System.IO.Abstractions) 17.0.24 for my tests.

I think I wrote a correct test for InfoLoader_LoadInfoAsync. But, I don't understand how to write a test for MyViewModel::StartLoadInfoAsync to check that InfoList was populated correctly. It seems that I have to duplicate instantiation and configuration of InfoLoader as I did in InfoLoader_LoadInfoAsync. 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
}

CodePudding user response:

In order to test StartLoadInfoAsync you need an instance of MyViewModel, so you should:

  1. Create this instance.
  2. Invoke the method StartLoadInfoAsync.
  3. Assert that its state is according to what you need.

Now obviously you have a dependency, which is InfoLoader, so you have two options:

  1. Create and configure a new instance of InfoLoader
  2. Mock InfoLoader so you can test MyViewModel independently of InfoLoader.

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.

You only need to create a mock of InfoLoader and setup its calls, just like you did with the FileSystem.

CodePudding user response:

Since the view model depends on the IInfoLoader abstraction, it can be mocked to behave as expected when the desired member is invoked.

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
        // When LoadInfoAsync is invoked
        .Setup(_ => _.LoadInfoAsync())
        // Use callback to raise event passing the custom arguments expected by the event delegate
        .Callback(() => dependency.Raise(_ => _.InfoLoaded  = null, info))
        // Then 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

Reference MOQ Quickstart: Events

  • Related