Disclaimer: Even though the scenario in the question does not follow best practices, it is still useful to demonstrate how to use TaskCompletionSource
for testing task-related code.
Below is an example of how you can test the second branch - the "timeout" branch in your code using TaskCompletionSource
Do:
- Prefer async code over sync - not to block the threads (of course, it depends)
- Don't block on async threads
- Use parameters and named constants instead of "magic values"
- Keep your unit tests fast and focused on a single scenario
- Avoid testing built-in functionality, like Wait timeout
- Implement cooperative cancellation https://learn.microsoft.com/en-us/dotnet/standard/parallel-programming/task-cancellation
- Use proper instrumentation - logging long running task is just an example, in PROD use ETW - https://learn.microsoft.com/en-us/windows/win32/etw/event-tracing-portal
I have modified your code a bit, to align to these recommendations.
I hope it's clear, if not - please let me know how I can help :-)
// This is the class that does the "long running work"
public class ClassA<T>
{
public async Task<T> doSomething(CancellationToken cancellationToken)
{
const int magicNumberOfIterations = 100;
var magicLengthOfTimeInSeconds = TimeSpan.FromSeconds(1);
for (int i = 0; i < magicNumberOfIterations; i++)
{
if (cancellationToken.IsCancellationRequested)
cancellationToken.ThrowIfCancellationRequested();
await Task.Delay(magicLengthOfTimeInSeconds); // time - consuming work
}
return default;
}
}
// This is the class that "detects" long running work based on timeout
public class SlowTaskLogger<T> {
// Don't use this in prod - this actually blocks a thread to write a metric
// Use ETW instead - https://learn.microsoft.com/en-us/windows/win32/etw/event-tracing-portal
public Task<T> RunAndLogSlowRunningTask(TimeSpan logAfterTimeout, Task<T> task)
{
if (task.Wait(logAfterTimeout)) // DO NOT use in PROD
return task; // don't block on async threads
LogWarningForLongRunningTask(task);
return task;
}
private void LogWarningForLongRunningTask(Task<T> task)
{
longRunningTasks.Add(task); // TODO: logging
}
private List<Task<T>> longRunningTasks = new(); // NOTE: for testing, replace with partial mock
public IReadOnlyCollection<Task<T>> LongRunningTasks => longRunningTasks;
}
The test could look something like this
public class InterviewTaskTests
{
private SlowTaskLogger<string> slowTaskLogger = new();
[Fact]
public async Task LogSlowRunningTaskTest()
{
var timeout = TimeSpan.FromMilliseconds(15);
var completionSource = new TaskCompletionSource<string>();
var expectedResult = "foo";
var taskWithLogging = slowTaskLogger.RunAndLogSlowRunningTask(timeout, completionSource.Task);
await Task.Delay(timeout);
// check that timeout method was called
// you can use partial mocks instead of private state for production code
Assert.Contains(taskWithLogging, slowTaskLogger.LongRunningTasks);
// complete the task and ensure proper result is returned
completionSource.SetResult(expectedResult);
var actualResult = await taskWithLogging;
Assert.Equal(expectedResult, actualResult);
}
}