问题
I'm writing F# code and tests in xUnit 1.9.
For normal sync stuff, I simply return unit
and all is well; but now, I'm migrating the sync innards to be async
workflows.
In other words, my simple AAA is getting explicit Async
usage pushed out into it as I refactor the system:
let [<Fact>] ``Can consume using NEventStore InMemory`` () =
let store = NesGateway.createInMemory ()
let finalDirection = playCircuit store |> Async.RunSynchronously // <----- YUCK
test <@ CounterClockWise = finalDirection @>
To remedy this, I want to make the body of the test be async
. However, to the best of my knowledge, xUnit.net only manages methods returning Task
-derived types, and hence I need a safe way of having my async
test body be wrapped suitably for xUnit.net's runner to pick it up and handle it appropriately.
What's the best way to express my above test ?
回答1:
As part of xUnit 2.2 Beta 3, @Brad Wilson has done the necessary to make it work natively directly. Hence one can now simply write:
let [<Fact>] ``Can consume NEventStore InMemory`` () = async {
let store = NesGateway.createInMemory ()
let! finalDirection = playCircuit store
test <@ CounterClockWise = finalDirection @> }
回答2:
Unfortunately (to the best of my knowledge [which is not saying a lot, hence the question]), the cleanest that's possible is to use the Async.StartAsTask <| async
applelation thusly:
let [<Fact>] ``Can consume NEventStore InMemory`` () = Async.StartAsTask <| async {
let store = NesGateway.createInMemory ()
let! finalDirection = playCircuit store
test <@ CounterClockWise = finalDirection @> }
Or, for a little more safety (an async [Fact]
will not be recognized as sync by xUnit.net if the return type is not [derived from] Task
) and [arguably] cleanliness (reads better):
// Ensure we match the return type xUnit.net is looking for
let toFact computation : Task = Async.StartAsTask computation :> _
let [<Fact>] ``Can play a circuit using GES``() = toFact <| async {
use! store = createStore()
let! finalDirection = playCircuit store
CounterClockWise =! finalDirection }
回答3:
You can also create your own computation expression to encapsulate the Async.RunSynchronously
call:
type AsyncTestBuilder() =
member this.Bind (v: Async<'a>, c: 'a -> 'b) = v |> Async.RunSynchronously |> c;
member this.Zero () = ()
let asyncTest = AsyncTestBuilder()
let [<Fact>] ``Can consume using NEventStore InMemory`` () = asyncTest {
let store = NesGateway.createInMemory ()
let! finalDirection = playCircuit store // <-- use let! instead of let
test <@ CounterClockWise = finalDirection @> }
回答4:
Pre xUnit 2.2, you can use a TaskBuilder to yield a Task<T>
using the same style as async
:-
let [<Fact>] ``Can consume NEventStore InMemory`` () = task {
let store = NesGateway.createInMemory ()
let! finalDirection = playCircuit store
test <@ CounterClockWise = finalDirection @> }
Excerpt (don't take it, take the full source as you'll find it relevant for all kinds of Task
-munging):
open System.Threading
open System.Threading.Tasks
[<AbstractClass>]
type AsyncBuilderAbstract() =
// AsyncBuilder is marked sealed, so we need this wrapper
member __.Zero() = async.Zero()
member __.Return t = async.Return t
member __.ReturnFrom t = async.ReturnFrom t
member __.Bind(f,g) = async.Bind(f,g)
member __.Combine(f,g) = async.Combine(f,g)
member __.Delay f = async.Delay f
member __.While(c,b) = async.While(c,b)
member __.For(xs,b) = async.For(xs,b)
member __.TryWith(b,e) = async.TryWith(b,e)
type TaskBuilder(?ct : CancellationToken) =
inherit AsyncBuilderAbstract()
member __.Run f : Task<'T> = Async.StartAsTask(f, ?cancellationToken = ct)
type UntypedTaskBuilder(?ct : CancellationToken) =
inherit AsyncBuilderAbstract()
member __.Run f : Task = Async.StartAsTask(f, ?cancellationToken = ct) :> Task
let task = new TaskBuilder()
来源:https://stackoverflow.com/questions/25605130/testing-f-async-workflows-with-xunit-nets-task-support