How to get a useful stacktrace when testing F# async workflows
async-workflow, f#, nunit, unit-testing
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.
Problem
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?