-2

I have a Function doSomething() which should work in a specific Time (15 seconds). If Time is out then i should throw a message that says "Timeout" but i will still waiting for the result.

so i solve that with a task:

Task<A> mytask = Task.Factory.StartNew(() => ClassA.doSomething()) ;
Mytask.wait(15);  
If(mytask.isCompleted)
{
    result= mytask.Result ; 
}
else
{
    Debug(‘ Time is out ’) ;
               Mytask.wait() ;  
               result= mytask.Result ; 
}

my Question is : how i can unit test the senario of getting Timeout

my Try :

ClassA  objectA= new ClassA() ; 
Clocktime t.start() ;  
Expect.Call(objectA.doSomething()) ;
t.end 
Assert.isTrue(T > 15) ; 
Mocks.verifyAll() ; 
  • Welcome to StackOverflow! Which unit testing library are you using? – Peter Csala Jan 24 '22 at 14:52
  • [You shouldn't use `Task.Factory.StartNew` anymore](https://stackoverflow.com/a/38423507/542251) – Liam Jan 24 '22 at 14:52
  • 1
    You never actually run your task? – Liam Jan 24 '22 at 14:53
  • Possible duplicate of [Asynchronously wait for Task to complete with timeout](https://stackoverflow.com/questions/4238345/asynchronously-wait-for-taskt-to-complete-with-timeout) – Liam Jan 24 '22 at 14:54
  • It's not a good practice to let the tests use real time, (and also magic numbers) and wait, you should make the timeout a parameter that you can change in the test so the test doesn't compromise your test run speed. Alternatively you can also pass the timeout class, and in this case do a Task.FromResult to make sure you control the time. – rmjoia Jan 24 '22 at 14:55
  • This has nothing to do with `Task.FromResult` @rmjoia – Liam Jan 24 '22 at 14:56
  • Liam thanks for the link, my problem is in Unittest and not at the function – HakunaMatata Jan 24 '22 at 15:00
  • @PeterCsala xUnit – HakunaMatata Jan 24 '22 at 15:02
  • @Liam I know, I said as an alternative, you can control the task from the caller, and hence, from the unit test. – rmjoia Jan 24 '22 at 15:02
  • Does this answer your question https://stackoverflow.com/questions/20282111/xunit-net-how-can-i-specify-a-timeout-how-long-a-test-should-maximum-need/37977663? – Peter Csala Jan 24 '22 at 15:05
  • Hey, you can control the behaviour of a task using `TaskCompletionSource` https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.taskcompletionsource-1?view=net-6.0 Until you call `.SetResult` your task will still be pending, so you will naturally fall into the second branch of your code (however, it will not test that timeout is exactly 15 ms) – ironstone13 Jan 24 '22 at 15:10
  • @MehdiRahali - please let me know if you are having issues when using `TaskCompletionSource` - I can provide an example, if needed – ironstone13 Jan 24 '22 at 15:16
  • @ironstone13 it would be very nice, when you write an example for me – HakunaMatata Jan 24 '22 at 15:21
  • I'd say you 100% have problems in that function. Not least of which it's blocking on an async thread. I'd imagine your `.Result` suffers from deadlocking. Testing this is the least of your problems – Liam Jan 24 '22 at 15:59
  • Hey, @MehdiRahali, I've added an example for you. I hope it helps! I encourage you to read more on threading in general to avoid some of the pitfalls that are present in the code (as Liam pointed out). – ironstone13 Jan 24 '22 at 16:31

1 Answers1

2

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:

  1. Prefer async code over sync - not to block the threads (of course, it depends)
  2. Don't block on async threads
  3. Use parameters and named constants instead of "magic values"
  4. Keep your unit tests fast and focused on a single scenario
  5. Avoid testing built-in functionality, like Wait timeout
  6. Implement cooperative cancellation https://learn.microsoft.com/en-us/dotnet/standard/parallel-programming/task-cancellation
  7. 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);
    }
}
ironstone13
  • 3,325
  • 18
  • 24