单元测试代理

Wat*_*son 8 f# unit-testing mailboxprocessor

我试图在F#中测试一个MailboxProcessor.我想测试我发布的函数f实际上是在发布消息时执行的.

原始代码使用的是Xunit,但我创建了一个fsx,我可以使用fsharpi执行它.

到目前为止我这样做:

open System 
open FSharp
open System.Threading
open System.Threading.Tasks



module MyModule =

    type Agent<'a> = MailboxProcessor<'a>
    let waitingFor timeOut (v:'a)= 
        let cts = new CancellationTokenSource(timeOut|> int)
        let tcs = new TaskCompletionSource<'a>()
        cts.Token.Register(fun (_) ->  tcs.SetCanceled()) |> ignore
        tcs ,Async.AwaitTask tcs.Task

    type MyProcessor<'a>(f:'a->unit) =
        let agent = Agent<'a>.Start(fun inbox -> 
             let rec loop() = async {

                let! msg = inbox.Receive()
                // some more complex should be used here
                f msg
                return! loop() 
             }
             loop()
        )

        member this.Post(msg:'a) = 
            agent.Post msg


open MyModule

let myTest =
    async {

        let (tcs,waitingFor) = waitingFor 5000 0

        let doThatWhenMessagepostedWithinAgent msg =
            tcs.SetResult(msg)

        let p = new MyProcessor<int>(doThatWhenMessagepostedWithinAgent)

        p.Post 3

        let! result = waitingFor

        return result

    }

myTest 
|> Async.RunSynchronously
|> System.Console.WriteLine 

//display 3 as expected
Run Code Online (Sandbox Code Playgroud)

这段代码有效,但对我来说看起来并不好.

1)在f#中是否正常使用TaskCompletionSource,还是有一些专门的东西可以让我等待完成?

2)我在waitingFor函数中使用第二个参数来约束它,我知道我可以使用类型MyType <'a>()来做它,还有另一种选择吗?我宁愿不使用我觉得麻烦的新MyType.

3)有没有其他选择来测试我的代理而不是这样做?到目前为止我发现的关于这个主题的唯一帖子是2009年的这篇博文http://www.markhneedham.com/blog/2009/05/30/f-testing-asynchronous-calls-to-mailboxprocessor/

Hon*_*tan 4

这是一个艰难的问题,我也已经尝试解决这个问题有一段时间了。这是我到目前为止发现的,对于评论来说太长了,但我也犹豫是否将其称为完整答案......

从最简单到最复杂,实际上取决于您想要测试的彻底程度以及代理逻辑的复杂程度。

你的解决方案可能没问题

对于小型代理来说,您拥有的功能很好,其唯一作用是序列化对异步资源的访问,而很少或没有内部状态处理。如果您f按照示例中的方式提供,则可以非常确定它将在几百毫秒的相对较短的超时内被调用。当然,它看起来很笨重,而且所有包装器和助手的代码大小都是两倍,但是如果您测试更多代理和/或更多场景,这些可以重复使用,因此成本很快就能摊销。

我看到的问题是,如果您还想验证调用的函数以外的内容,那么它不是很有用 - 例如调用它后的内部代理状态。

需要注意的是,这也适用于响应的其他部分:我通常使用取消令牌启动代理,它使生产和测试生命周期变得更容易。

使用代理回复渠道

添加AsyncReplyChannel<'reply>到消息类型并使用代理上的方法PostAndAsyncReply来发布消息。Post它会将您的代理更改为如下所示:

type MyMessage<'a, 'b> = 'a * AsyncReplyChannel<'b>

type MyProcessor<'a, 'b>(f:'a->'b) =
    // Using the MyMessage type here to simplify the signature
    let agent = Agent<MyMessage<'a, 'b>>.Start(fun inbox -> 
         let rec loop() = async {
            let! msg, replyChannel = inbox.Receive()
            let! result = f msg
            // Sending the result back to the original poster
            replyChannel.Reply result
            return! loop()
         }
         loop()
    )

    // Notice the type change, may be handled differently, depends on you
    member this.Post(msg:'a): Async<'b> = 
        agent.PostAndAsyncReply(fun channel -> msg, channel)
Run Code Online (Sandbox Code Playgroud)

这可能看起来像是对代理“接口”的人为要求,但是模拟方法调用很方便,并且测试也很简单 - 等待(PostAndAsyncReply超时)并且您可以摆脱大部分测试帮助程序代码。

由于您对所提供的函数 和 进行了单独的调用replyChannel.Reply,因此响应还可以反映代理状态,而不仅仅是函数结果。

基于黑盒模型的测试

这是我将最详细地讨论的内容,因为我认为这是最普遍的。

如果代理封装了更复杂的行为,我发现跳过测试单个消息并使用基于模型的测试来根据预期外部行为模型验证整个操作序列很方便。我为此使用FsCheck.Experimental API:

在您的情况下,这是可行的,但没有多大意义,因为没有可建模的内部状态。为了向您展示我的特定情况下的情况,请考虑一个维护客户端 WebSocket 连接以将消息推送到客户端的代理。我无法分享整个代码,但界面如下所示

/// For simplicity, this adapts to the socket.Send method and makes it easy to mock
type MessageConsumer = ArraySegment<byte> -> Async<bool>

type Message =
    /// Send payload to client and expect a result of the operation
    | Send of ClientInfo * ArraySegment<byte> * AsyncReplyChannel<Result>
    /// Client connects, remember it for future Send operations
    | Subscribe of ClientInfo * MessageConsumer
    /// Client disconnects
    | Unsubscribe of ClientInfo
Run Code Online (Sandbox Code Playgroud)

代理在内部维护一个Map<ClientInfo, MessageConsumer>.

现在为了测试这一点,我可以根据非正式规范对外部行为进行建模,例如:“发送到订阅的客户端可能会成功或失败,具体取决于调用 MessageConsumer 函数的结果”和“发送到未订阅的客户端不应调用任何消息消费者”。因此,我可以定义类似的类型来对代理进行建模。

type ConsumerType =
    | SucceedingConsumer
    | FailingConsumer
    | ExceptionThrowingConsumer

type SubscriptionState =
    | Subscribed of ConsumerType
    | Unsubscribed

type AgentModel = Map<ClientInfo, SubscriptionState>
Run Code Online (Sandbox Code Playgroud)

然后使用 FsCheck.Experimental 定义添加和删除具有不同成功消费者的客户端并尝试向他们发送数据的操作。然后,FsCheck 生成随机操作序列,并根据每个步骤之间的模型验证代理实现。

这确实需要一些额外的“仅测试”代码,并且在开始时会产生很大的心理开销,但可以让您测试相对复杂的状态逻辑。我特别喜欢这一点的是,它可以帮助我测试整个合约,而不仅仅是单个函数/方法/消息,就像基于属性/生成测试有助于测试多个值一样。

使用演员

我还没有走那么远,但我也听说作为替代方案是使用Akka.NET来提供成熟的 Actor 模型支持,并使用它的测试工具让您在特殊的测试上下文中运行代理,验证预期的消息等等。正如我所说,我没有第一手经验,但对于更复杂的状态逻辑来说似乎是一个可行的选择(即使在单台机器上,而不是在分布式多节点参与者系统中)。