在测试F#异步工作流时如何获得有用的堆栈跟踪

stm*_*max 13 f# nunit unit-testing async-workflow

我想测试以下异步工作流程(使用NUnit + FsUnit):

let foo = async {
  failwith "oops"
  return 42
}
Run Code Online (Sandbox Code Playgroud)

我为它编写了以下测试:

let [<Test>] TestFoo () =
  foo
  |> Async.RunSynchronously
  |> should equal 42
Run Code Online (Sandbox Code Playgroud)

自从foo抛出后,我在单元测试运行器中得到以下stacktrace:

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
Run Code Online (Sandbox Code Playgroud)

不幸的是,堆栈跟踪并没有告诉我异常的位置.它在RunSynchronously处停止.

某处我听说Async.Catch神奇地恢复了堆栈跟踪,所以我调整了我的测试:

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))
Run Code Online (Sandbox Code Playgroud)

现在这很难看,但至少它产生了一个有用的堆栈跟踪:

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)
Run Code Online (Sandbox Code Playgroud)

这次堆栈跟踪显示错误发生的确切位置:ExplorationTests.foo@line 71

有没有办法摆脱Async.Catch和两个选择之间的匹配,同时仍然得到有用的堆栈跟踪?有没有更好的方法来构建异步工作流测试?

stm*_*max 5

由于Async.Catch和重新抛出异常似乎是获得有用的堆栈跟踪的唯一方法,我想出了以下内容:

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"
Run Code Online (Sandbox Code Playgroud)

注意"ExceptionDispatchInfo.Capture(ex).Throw()".这是关于可以在不破坏其堆栈跟踪的情况下重新抛出异常的最好方法(缺点:仅在.NET 4.5之后可用).

现在我可以像这样重写测试"TestFooWithBetterStacktrace":

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

测试看起来好多了,重新抛出的代码不会吮吸(和以前一样多),当出现问题时,我会在测试运行器中获得有用的堆栈跟踪.