【问题标题】:How to get a useful stacktrace when testing F# async workflows测试 F# 异步工作流时如何获得有用的堆栈跟踪
【发布时间】:2013-08-12 16:43:32
【问题描述】:

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

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

我为它写了以下测试:

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

由于 foo throws 我在单元测试运行器中得到以下堆栈跟踪:

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

不幸的是,堆栈跟踪没有告诉我异常是在哪里引发的。它在 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))

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

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)

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

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

【问题讨论】:

  • 我遇到了同样的问题,Async.Catch 是我能找到的唯一解决方法
  • 我已向 Don Syme 发送电子邮件,他认为这是 .NET 的基本限制,而 Async.Catch 是唯一的选择。
  • @JohnPalmer 听起来像是对我的回答

标签: unit-testing f# nunit async-workflow


【解决方案1】:

由于 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"

注意“ExceptionDispatchInfo.Capture(ex).Throw()”。这是在不破坏堆栈跟踪的情况下重新抛出异常的最佳方式(缺点:仅在 .NET 4.5 之后可用)。

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

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

测试看起来好多了,重新抛出的代码不会像以前那样糟糕,而且当出现问题时,我会在测试运行器中获得有用的堆栈跟踪。

【讨论】:

    【解决方案2】:

    引用我不久前发给 Don Syme 的一些电子邮件:

    如果您尝试设置“Catch First”,调试体验应该会有所改善 Debug --> Exceptions --> CLR Exceptions 中的“机会异常”。转动 关闭“仅我的代码”也可以提供帮助。

    没错。使用 async { ... },计算不受堆栈限制,因此 需要在某些地方重新抛出异常以使它们回到 正确的线程。

    明智地使用 Async.Catch 或其他异常处理也可以 帮助。

    【讨论】:

    • 嗨,约翰,引用#1 启用在第一次机会异常时中断并没有太大帮助,因为我几乎从不使用调试器。我需要在单元测试运行器的输出中使用有用的堆栈跟踪。引用 #2 这是否意味着我已经在第二次测试(TestFooWithBetterStacktrace)中做对了?
    猜你喜欢
    • 1970-01-01
    • 2014-03-30
    • 1970-01-01
    • 2019-11-05
    • 1970-01-01
    • 2015-05-14
    • 2011-11-11
    • 1970-01-01
    • 2021-02-25
    相关资源
    最近更新 更多