I am attempting to test an async Task
method that calls synchronous code before and after calling a separate async Task
method. The synchronous code updates a loading state enumeration that tells the view layer what to display (e.g. a loading spinner when loading, an error message when an exception is thrown, etc.). Note that I have omitted the property changed event in this example for brevity.
namespace Models
{
public class DataRepository
{
private readonly IDataService dataService;
public DataRepository(IDataService dataService)
{
this.dataService = dataService;
}
public LoadingState LoadingState { get; set; }
public async Task Refresh()
{
// Synchronous code to notify view to show loading state
this.LoadingState = LoadingState.Busy;
try
{
await this.dataService.LoadData();
// Synchronous code to notify view to show done state
this.LoadingState = LoadingState.Done;
}
catch (Exception e)
{
// Synchronous code to notify view to show error state
this.LoadingState = LoadingState.Error;
}
}
}
// This is in a separate class in the actual code. Including here for context.
public enum LoadingState
{
Busy,
Error,
Done,
}
}
I am using NSubstitute to mock IDataService
, XUnit to run the tests, and Fluent Assertions to assert. I am able to test the loading state value in the following cases:
- When the
LoadData()
method completes successfully:
[Fact]
public async void Refresh_Success_ExpectLoadingStateDone()
{
var mockDataService = Substitute.For<IDataService>();
var dataRepository = new DataRepository(mockDataService);
await dataRepository.Refresh();
dataRepository.LoadingState.Should().Be(LoadingState.Done);
}
- When the
LoadData()
method throws an exception:
[Fact]
public async void Refresh_ThrowsException_ExpectLoadingStateError()
{
var mockDataService = Substitute.For<IDataService>();
mockDataService.Throws<Exception>();
var dataRepository = new DataRepository(mockDataService);
await dataRepository.Refresh();
dataRepository.LoadingState.Should().Be(LoadingState.Error);
}
However, I cannot write a test that asserts that the loading state has been set to "Busy".
[Fact]
public async void Refresh_InitialState_ExpectLoadingStateBusy()
{
var mockDataService = Substitute.For<IDataService>();
var dataRepository = new DataRepository(mockDataService);
await dataRepository.Refresh();
// This test fails because the loading state is set to "Done" at this point.
dataRepository.LoadingState.Should().Be(LoadingState.Busy);
}
How do I write a test for the initial loading state? Is there a way to pause the call to LoadData()
so that I can assert on the initial loading state?
CodePudding user response:
You can mock LoadData
to return a task managed by you, which will complete when you say so. Then, do not await Refresh
because for this test you don't need for Refresh
to complete, you want to check the state in progress. Then after you call Refresh
(without await
) the LoadData
will be in progress and you can verify that state is Busy
. Then complete the LoadData
and wait for Refresh
to complete to finish the test. For example:
var mockDataService = Substitute.For<IDataService>();
// this will represent our LoadData in progress
// it won't complete until we tell it
var inProgressTask = new TaskCompletionSource();
// mock LoadData to return this task
mockDataService.Configure().LoadData().Returns(inProgressTask.Task);
var dataRepository = new DataRepository(mockDataService);
// now, execute Refresh but do not await it yet
var pendingRefresh = dataRepository.Refresh();
// assert that state is Busy at this point, as it should be
var state = dataRepository.LoadingState; // Busy
// signal LoadData to complete
inProgressTask.SetResult();
// await Refresh
await pendingRefresh;