Async Testing
MSTest
Unit Testing
C#
Software Development

How does one test async code using MSTest

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

Async code should be tested asynchronously. In MSTest, that means test methods should normally return Task, await the code under test, and use async-aware assertions instead of blocking with .Result or .Wait().

This matters for correctness, not just style. Proper async tests surface exceptions correctly, reduce deadlock risk, and make concurrency bugs much easier to reason about.

Use async Task Test Methods

The basic MSTest pattern is simple.

csharp
1using Microsoft.VisualStudio.TestTools.UnitTesting;
2using System.Threading.Tasks;
3
4[TestClass]
5public class UserServiceTests
6{
7    [TestMethod]
8    public async Task GetNameAsync_ReturnsExpectedValue()
9    {
10        var service = new UserService();
11        var result = await service.GetNameAsync(1);
12        Assert.AreEqual("Alice", result);
13    }
14}
15
16public class UserService
17{
18    public async Task<string> GetNameAsync(int id)
19    {
20        await Task.Delay(10);
21        return id == 1 ? "Alice" : "Unknown";
22    }
23}

The important part is the signature: public async Task ..., not async void.

Test Exceptions Asynchronously

When an async method should fail, use Assert.ThrowsExceptionAsync.

csharp
1using Microsoft.VisualStudio.TestTools.UnitTesting;
2using System;
3using System.Threading.Tasks;
4
5[TestClass]
6public class ExceptionTests
7{
8    [TestMethod]
9    public async Task LoadAsync_InvalidInput_Throws()
10    {
11        var service = new ValidatingService();
12
13        await Assert.ThrowsExceptionAsync<ArgumentOutOfRangeException>(async () =>
14        {
15            await service.LoadAsync(-1);
16        });
17    }
18}
19
20public class ValidatingService
21{
22    public Task LoadAsync(int id)
23    {
24        if (id < 0) throw new ArgumentOutOfRangeException(nameof(id));
25        return Task.CompletedTask;
26    }
27}

This is much clearer than using try/catch in the test body for expected async failures.

Avoid Blocking Calls

Bad patterns in async tests:

csharp
// var result = service.GetNameAsync(1).Result;
// service.GetNameAsync(1).Wait();

Blocking can hide the real async behavior and in some environments can lead to deadlocks or confusing failures. If the production method is async, keep the test async all the way through.

Mock Async Dependencies Correctly

When the code under test depends on async collaborators, make the mock return a task.

csharp
1using Microsoft.VisualStudio.TestTools.UnitTesting;
2using Moq;
3using System.Threading.Tasks;
4
5public interface IRepository
6{
7    Task<int> GetCountAsync();
8}
9
10[TestClass]
11public class RepositoryBackedTests
12{
13    [TestMethod]
14    public async Task Service_UsesRepositoryCount()
15    {
16        var repo = new Mock<IRepository>();
17        repo.Setup(r => r.GetCountAsync()).ReturnsAsync(5);
18
19        var service = new CountService(repo.Object);
20        var value = await service.ReadCountAsync();
21
22        Assert.AreEqual(5, value);
23    }
24}
25
26public class CountService
27{
28    private readonly IRepository _repo;
29
30    public CountService(IRepository repo)
31    {
32        _repo = repo;
33    }
34
35    public Task<int> ReadCountAsync() => _repo.GetCountAsync();
36}

This keeps the test deterministic and focused on control flow rather than infrastructure.

Test Cancellation and Completion Deterministically

For async workflows with cancellation tokens or delayed completion, do not rely on arbitrary sleeps when a more explicit signal is possible.

csharp
1using Microsoft.VisualStudio.TestTools.UnitTesting;
2using System.Threading;
3using System.Threading.Tasks;
4
5[TestClass]
6public class CancellationTests
7{
8    [TestMethod]
9    public async Task WorkAsync_CanBeCancelled()
10    {
11        var svc = new Worker();
12        using var cts = new CancellationTokenSource();
13        cts.Cancel();
14
15        await Assert.ThrowsExceptionAsync<TaskCanceledException>(async () =>
16        {
17            await svc.WorkAsync(cts.Token);
18        });
19    }
20}
21
22public class Worker
23{
24    public async Task WorkAsync(CancellationToken token)
25    {
26        await Task.Delay(100, token);
27    }
28}

Tests that use explicit signals are much more reliable than tests that just “wait a bit and hope.”

Common Pitfalls

A common mistake is writing async void tests. MSTest cannot observe those reliably the way it can observe Task-returning tests.

Another issue is blocking with .Result or .Wait() and then blaming the test framework when the behavior becomes erratic.

Developers also often use Task.Delay as a substitute for proper coordination in concurrent tests. That usually creates flakiness rather than confidence.

Finally, do not skip cancellation-path tests. Async APIs often fail in production not on the happy path, but during shutdown, timeout, or retry behavior.

Summary

  • Use async Task test methods in MSTest.
  • Await the code under test instead of blocking on tasks.
  • Use Assert.ThrowsExceptionAsync for expected async failures.
  • Mock async dependencies with task-returning setups.
  • Prefer explicit completion and cancellation signals over arbitrary timing delays.

Course illustration
Course illustration

All Rights Reserved.