测试代理的单元测试

9

我想测试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<'a>()来实现,还有其他选项吗?我不想使用我认为笨重的新MyType。

3)除了这种方法之外,还有其他测试我的代理的选择吗?迄今为止我找到的唯一关于这个主题的文章是2009年的这篇博客文章http://www.markhneedham.com/blog/2009/05/30/f-testing-asynchronous-calls-to-mailboxprocessor/

1个回答

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)

这似乎对代理“接口”来说是一个人为的要求,但模拟方法调用非常方便,并且易于测试-等待 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<ClientInfo, MessageConsumer>

现在为了测试这个代理,我可以用非正式规范来模拟外部行为,例如:“向已订阅的客户端发送可能成功也可能失败,取决于调用MessageConsumer函数的结果”和“向未订阅的客户端发送不应调用任何MessageConsumer”。因此,我可以定义像这样的类型来模拟代理。

type ConsumerType =
    | SucceedingConsumer
    | FailingConsumer
    | ExceptionThrowingConsumer

type SubscriptionState =
    | Subscribed of ConsumerType
    | Unsubscribed

type AgentModel = Map<ClientInfo, SubscriptionState>

然后使用FsCheck.Experimental定义添加和删除具有不同成功消费者并尝试向它们发送数据的操作。FsCheck随机生成操作序列,并在每个步骤之间针对模型验证代理实现。
这确实需要一些额外的“仅测试”代码,并且在开始时需要承担相当大的心理负担,但可以让您测试相对复杂的有状态逻辑。我特别喜欢的是它帮助我测试整个合同,而不仅仅是单个函数/方法/消息,就像基于属性/生成的测试帮助测试超过单个值一样。
使用演员
我还没有走得那么远,但我听到的另一种选择是例如使用Akka.NET进行完整的演员模型支持,并使用其测试设施,在特殊的测试上下文中运行代理,验证预期的消息等等。正如我所说,我没有第一手经验,但似乎是更复杂的有状态逻辑(甚至在单台计算机上,而不是在分布式多节点演员系统中)的可行选项。

非常感谢您的出色回答。非常详尽,正是我所需要的。我会研究一下fscheck实验和我能够做些什么。 - Yoann

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接