0

It is my first time when I write unit tests for async method. I am using xUnit. I searched SO with no promissing results. Best I found, but didnt work for me, is to implement IAsyncLifetime from THIS example. I will be thankful for any hints how to trouble shoot this problem.

Currently what I have. In tested VM I have a command:

public ICommand TestResultsCommand { get; private set; }

And the command is initialized in the VM constructor as below:

TestResultsCommand = new DelegateCommand(OnTestResultExecuteAsync);

Command calls method:

private async void OnTestResultExecuteAsync(object obj)
        {
            TokenSource = new CancellationTokenSource();
            CancellationToken = TokenSource.Token;

            await TestHistoricalResultsAsync();
        }

TestHistoricalResultsAsync method's signature is as below:

private async Task TestHistoricalResultsAsync()

Now lets go to the unit test project. Currently in test class I have a method:

[Fact]//testing async void
        public void OnTestResultExecuteAsync_ShouldCreateCancellationTokenSource_True()
        {
            CancellationTokenSource tokenSource = new CancellationTokenSource();
            CancellationToken cancellationToken = tokenSource.Token;
            _viewModel.TestResultsCommand.Execute(null);
            Assert.Equal(cancellationToken.CanBeCanceled, _viewModel.CancellationToken.CanBeCanceled);
            Assert.Equal(cancellationToken.IsCancellationRequested, _viewModel.CancellationToken.IsCancellationRequested);
        }

And the test drops me an exception:

Message: System.NullReferenceException : Object reference not set to an instance of an object.

The stack trace for the exception is: enter image description here

Thank you in advance for your time and suggestions.

bakunet
  • 197
  • 1
  • 12
  • `async void` should only be used for event handlers. Reference [Async/Await - Best Practices in Asynchronous Programming](https://msdn.microsoft.com/en-us/magazine/jj991977.aspx). – Nkosi Jun 13 '19 at 13:26
  • Also the code should be refactored to be more SOLID so that you can properly test and maintain the code. Without a better view of the class under test, there is not much more that can be suggested as it appears you are trying to test a private member. – Nkosi Jun 13 '19 at 13:28
  • @Nkosi but `async void` should not be also used for `DelegateCommand`? – bakunet Jun 13 '19 at 13:30
  • @Nkosi allright, in my test method I just executed command async `await Task.Run(() => _viewModel.TestResultsCommand.Execute(null));` and it worked. Solution based on this: https://forums.xamarin.com/discussion/97504/testing-async-commands – bakunet Jun 13 '19 at 13:58

1 Answers1

2

One of the problems of async void methods is that they're difficult to test. For your problem, most developers do one of these:

  1. Define and use an IAsyncCommand interface.
  2. Make their logic async Task and public.
  3. Use a framework that supports async commands, e.g., MvvmCross.

See my MSDN magazine article on the subject for details.

Here's an example with the second approach:

TestResultsCommand = new DelegateCommand(async () => await OnTestResultExecuteAsync());

public async Task OnTestResultExecuteAsync()
{
  TokenSource = new CancellationTokenSource();
  CancellationToken = TokenSource.Token;

  await TestHistoricalResultsAsync();
}

[Fact]
public async Task OnTestResultExecuteAsync_ShouldCreateCancellationTokenSource_True()
{
  CancellationTokenSource tokenSource = new CancellationTokenSource();
  CancellationToken cancellationToken = tokenSource.Token;
  await _viewModel.OnTestResultExecuteAsync();
  Assert.Equal(cancellationToken.CanBeCanceled, _viewModel.CancellationToken.CanBeCanceled);
  Assert.Equal(cancellationToken.IsCancellationRequested, _viewModel.CancellationToken.IsCancellationRequested);
}

If you don't want to expose async Task methods just for the sake of unit testing, then you can use an IAsyncCommand of some kind; either your own (as detailed in my article) or one from a library (e.g., MvvmCross). Here's an example using the MvvmCross types:

public IMvxAsyncCommand TestResultsCommand { get; private set; }

TestResultsCommand = new MvxAsyncCommand(OnTestResultExecuteAsync);

private async Task OnTestResultExecuteAsync() // back to private
{
  TokenSource = new CancellationTokenSource();
  CancellationToken = TokenSource.Token;

  await TestHistoricalResultsAsync();
}

[Fact]
public async Task OnTestResultExecuteAsync_ShouldCreateCancellationTokenSource_True()
{
  CancellationTokenSource tokenSource = new CancellationTokenSource();
  CancellationToken cancellationToken = tokenSource.Token;
  await _viewModel.TestResultsCommand.ExecuteAsync();
  Assert.Equal(cancellationToken.CanBeCanceled, _viewModel.CancellationToken.CanBeCanceled);
  Assert.Equal(cancellationToken.IsCancellationRequested, _viewModel.CancellationToken.IsCancellationRequested);
}

If you prefer the IMvxAsyncCommand approach but don't want the MvvmCross dependency, it's not hard to define your own IAsyncCommand and AsyncCommand types.

Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
  • Ok, thank you for the feedback. It gives me the big picture. I will have to modify my `DelegateCommand` to use `TestResultsCommand = new DelegateCommand(async () => await OnTestResultExecuteAsync());`. I was trying this before, but `DelegateCommand` didnt want to accept parameter, so I stayed with void. Ill change this. Thank you! – bakunet Jun 13 '19 at 16:15
  • Hi again, I tested your approach, including creating of `AsyncCommand`, but in test method instead of `await _viewModel.OnTestResultExecuteAsync();` I had to use `await Task.Run(() => _viewModel.TestResultsCommand.Execute(null));`, otherwise seems to be that `TestHistoricalResultsAsync` is not ran async and still drops same exception. – bakunet Jun 14 '19 at 01:33
  • The `await Task.Run` can't possibly fix that. Sounds like you have a race condition, and the `Task.Run` is making it (barely) take long enough. A better solution is to fix the race condition. – Stephen Cleary Jun 14 '19 at 01:40
  • Honestly, `await Task.Run` fixed this. `TestHistoricalResultsAsync` returns only `Task`, and with `await Task.Run` it is executed async, no exception is thrown. You can find source code here: https://github.com/przemyslawbak/Horse_Picker/blob/b628978d74356d5af99106a55f7e23b88a6a8640/Horse_Picker.Tests/ViewModels/MainViewModelTests.cs#L118 And I am not sure what you mean by **fix the race condition**? – bakunet Jun 14 '19 at 02:12