bakunet
bakunet

Reputation: 197

How to test async void DelegateCommand method containing await with xUnit?

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.

Upvotes: 0

Views: 859

Answers (1)

Stephen Cleary
Stephen Cleary

Reputation: 457302

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.

Upvotes: 2

Related Questions