[英]Unit testing an agent
我正在尝试在 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/
这是一个艰难的问题,我也一直在努力解决这个问题。 这是我到目前为止发现的,评论太长了,但我也不愿称其为完整的答案......
从最简单到最复杂,实际上取决于您要测试的彻底程度以及代理逻辑的复杂程度。
您所拥有的对于小型代理来说很好,其唯一作用是序列化对异步资源的访问,很少或没有内部状态处理。 如果您在示例中提供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 函数的结果”和“发送到未订阅的客户端不应调用任何消息消费者”。 因此,我可以定义诸如此类的类型来为代理建模。
type ConsumerType =
| SucceedingConsumer
| FailingConsumer
| ExceptionThrowingConsumer
type SubscriptionState =
| Subscribed of ConsumerType
| Unsubscribed
type AgentModel = Map<ClientInfo, SubscriptionState>
然后使用 FsCheck.Experimental 定义添加和删除具有不同成功消费者的客户端并尝试向它们发送数据的操作。 FsCheck 然后生成随机的操作序列,并在每个步骤之间根据模型验证代理实现。
这确实需要一些额外的“仅测试”代码,并且在开始时有很大的心理开销,但可以让您测试相对复杂的有状态逻辑。 我特别喜欢这一点的是,它帮助我测试整个合约,而不仅仅是单个函数/方法/消息,就像基于属性/生成测试帮助测试不仅仅是单个值一样。
我还没有走那么远,但我还听说作为替代方案,例如使用Akka.NET来提供成熟的 actor 模型支持,并使用它的测试工具,让您可以在特殊的测试上下文中运行代理,验证预期的消息等。 正如我所说,我没有第一手经验,但对于更复杂的有状态逻辑(即使在单台机器上,而不是在分布式多节点参与者系统中),这似乎是一个可行的选择。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.