Question

I'd like to test the following async workflow (with NUnit+FsUnit):

let foo = async {
  failwith "oops"
  return 42
}

I wrote the following test for it:

let [<Test>] TestFoo () =
  foo
  |> Async.RunSynchronously
  |> should equal 42

Since foo throws I get the following stacktrace in the unit test runner:

System.Exception : oops
   at Microsoft.FSharp.Control.CancellationTokenOps.RunSynchronously(CancellationToken token, FSharpAsync`1 computation, FSharpOption`1 timeout)
   at Microsoft.FSharp.Control.FSharpAsync.RunSynchronously(FSharpAsync`1 computation, FSharpOption`1 timeout, FSharpOption`1 cancellationToken)
   at ExplorationTests.TestFoo() in ExplorationTests.fs: line 76

Unfortunately the stacktrace doesn't tell me where the exception was raised. It stops at RunSynchronously.

Somewhere I heard that Async.Catch magically restores the stacktrace, so I adjusted my test:

let [<Test>] TestFooWithBetterStacktrace () =
  foo
  |> Async.Catch
  |> Async.RunSynchronously
  |> fun x -> match x with 
              | Choice1Of2 x -> x |> should equal 42
              | Choice2Of2 ex -> raise (new System.Exception(null, ex))

Now this is ugly but at least it produces a useful stacktrace:

System.Exception : Exception of type 'System.Exception' was thrown.
  ----> System.Exception : oops
   at Microsoft.FSharp.Core.Operators.Raise(Exception exn)
   at ExplorationTests.TestFooWithBetterStacktrace() in ExplorationTests.fs: line 86
--Exception
   at Microsoft.FSharp.Core.Operators.FailWith(String message)
   at ExplorationTests.foo@71.Invoke(Unit unitVar) in ExplorationTests.fs: line 71
   at Microsoft.FSharp.Control.AsyncBuilderImpl.callA@769.Invoke(AsyncParams`1 args)

This time the stacktrace shows exactly where the error happend: ExplorationTests.foo@line 71

Is there a way to get rid of the Async.Catch and the matching between two choices while still getting useful stacktraces? Is there a better way to structure async workflow tests?

Was it helpful?

Solution

Since Async.Catch and rethrowing the exception seem to be the only way to get a useful stacktrace I came up with the following:

type Async with
  static member Rethrow x =
    match x with 
      | Choice1Of2 x -> x
      | Choice2Of2 ex -> ExceptionDispatchInfo.Capture(ex).Throw()
                         failwith "nothing to return, but will never get here"

Note "ExceptionDispatchInfo.Capture(ex).Throw()". That's about the nicest way one can rethrow an exception without corrupting its stacktrace (downside: only available since .NET 4.5).

Now I can rewrite the test "TestFooWithBetterStacktrace" like that:

let [<Test>] TestFooWithBetterStacktrace () =
  foo
  |> Async.Catch
  |> Async.RunSynchronously
  |> Async.Rethrow
  |> should equal 42

The test looks much better, the rethrowing code doesn't suck (as much as before) and I get useful stacktraces in the test runner when something goes wrong.

OTHER TIPS

Quoting from some emails that I sent Don Syme a while back:

The debug experience should improve if you try setting "Catch First Chance Exceptions" in Debug --> Exceptions --> CLR Exceptions. Turning off "Just My Code" can also help.

and

Right. With async { ... }, computations are not be stack bound, hence exceptions need to be rethrown in some places to get them back to the right thread.

Judicious use of Async.Catch, or other exception handling can also help.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top