【问题标题】:Unit testing an agent对代理进行单元测试
【发布时间】:2017-01-15 15:46:04
【问题描述】:

我正在尝试在 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

此代码有效,但对我来说看起来不太好。

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

2) 我在 waitingFor 函数中使用了第二个参数来约束它,我知道我可以使用类型 MyType() 来做到这一点,还有其他选择吗?我宁愿不使用我觉得很麻烦的新 MyType。

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

【问题讨论】:

    标签: unit-testing f# mailboxprocessor


    【解决方案1】:

    这是一个艰难的问题,我也一直在努力解决这个问题。这是我到目前为止发现的内容,评论太长了,但我也不愿将其称为完整答案......

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

    您的解决方案可能没问题

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

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

    还有一点也适用于响应的其他部分:我通常使用取消令牌启动代理,它使生产和测试生命周期更容易。

    使用代理回复渠道

    将AsyncReplyChannel&lt;'reply&gt; 添加到消息类型并使用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)
    

    这似乎是对代理“接口”的人为要求,但模拟方法调用很方便,测试也很简单 - 等待 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
    

    代理在内部维护Map&lt;ClientInfo, MessageConsumer&gt;。

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

    type ConsumerType =
        | SucceedingConsumer
        | FailingConsumer
        | ExceptionThrowingConsumer
    
    type SubscriptionState =
        | Subscribed of ConsumerType
        | Unsubscribed
    
    type AgentModel = Map<ClientInfo, SubscriptionState>
    

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

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

    使用演员

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

    【讨论】:

    • 很好的答案,谢谢。非常彻底……正是我想要的。我将看看 fscheck 实验性以及我可以用它做什么。
    猜你喜欢
    • 2016-02-02
    • 1970-01-01
    • 1970-01-01
    • 2012-01-08
    • 2012-05-14
    • 2023-03-21
    • 1970-01-01
    • 1970-01-01
    • 2019-11-02
    相关资源
    最近更新 更多