I am working on a autocomplete component. When typing, a search function is invoked to return results. This search function e.g. calls a api endpoint. Since loading the results may take some time, I want to be able to cancel previous search requests when there are new requests.
Code:
public Func<string, CancellationToken, Task<IEnumerable<T>>> SearchFuncWithCancel { get; set; }
private CancellationTokenSource _cancellationTokenSrc;
private void CancelToken()
{
_cancellationTokenSrc?.Cancel();
_cancellationTokenSrc = new CancellationTokenSource();
}
private async Task OnSearchAsync()
{
IEnumerable<T> searched_items = Array.Empty<T>();
CancelToken();
try
{
searched_items = (await SearchFuncWithCancel(Text, _cancellationTokenSrc.Token)) ?? Array.Empty<T>();
{
catch (Exception e)
{
Console.WriteLine("The search function failed to return results: " e.Message);
}
//...
}
For every search any previous (and still active) search is cancelled. I would like to unit test this behaviour.
What I've tried so far:
[Test]
public async Task Autocomplete_Should_Cancel_Search_On_Search()
{
// Arrange
var mockHttp = new MockHttpMessageHandler();
mockHttp.When("http://localhost/autocomplete")
.Respond("application/json", "[\"Foo\", \"Bar\"]");
var client = mockHttp.ToHttpClient();
var comp = Context.RenderComponent<AutocompleteTest1>();
var autocompletecomp = comp.FindComponent<MudAutocomplete<string>>();
autocompletecomp.SetParam(p => p.SearchFuncWithCancel, new Func<string, System.Threading.CancellationToken, Task<IEnumerable<string>>>(async (s, cancellationToken) => {
var result = await client.GetAsync("http://localhost/autocomplete", cancellationToken);
var content = await result.Content.ReadAsStringAsync(cancellationToken);
return JsonConvert.DeserializeObject<IEnumerable<string>>(content);
}));
// Test
autocompletecomp.Find("input").Input("Foo");
comp.WaitForAssertion(() => comp.Find("div.mud-popover").ToMarkup().Should().Contain("Foo"));
autocompletecomp.Find("input").Input("Bar");
comp.WaitForAssertion(() => comp.Find("div.mud-popover").ToMarkup().Should().Contain("Bar"));
}
This test passes, however it does not test the cancellation behaviour yet. I am not sure how to do it. Any ideas?
CodePudding user response:
As long as you can mock the API-call to return something else you should be able to use taskCompletionSources to simulate whatever behavior you desire. I'm not familiar with your specific mock framework, but perhaps something like this should work
// Setup the API method for the first call
CancellationToken cancelToken= null;
var tcs1= new TaskCompletionSource<IEnumerable<T>>();
subjectUnderTest.SearchFuncWithCancel = (t, c) => {
cancelToken= c;
return tcs.Task;
};
var call1 = subjectUnderTest.DoSearch("call1");
Assert.IsFalse(cancelToken.IsCancellationRequested);
// Setup the API-method for the second call
var tcs2= new TaskCompletionSource<IEnumerable<T>>();
subjectUnderTest.SearchFuncWithCancel = (t, c) => tcs2.Task;
var call2 = subjectUnderTest.DoSearch("call2");
Assert.IsTrue(cancelToken.IsCancellationRequested);
// Check that the first call handles a cancelled task correctly
tcs1.SetCanceled();
Assert.IsTrue(call1.IsCanceled);
// Second call succeed
tcs2.SetResult(...);
Assert.IsTrue(call2.IsCompletedSuccessfully);
The first call to the API will return a task that never completes. So when the second call occurs we can check that the cancellation token for the first call is cancelled. I hope the pseudocode is possible to translate to your particular mock framework.