我正在尝试在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&lt;&#39; a&gt;()来做它,还有另一种选择吗?我宁愿不使用我觉得麻烦的新MyType。
3)测试我的代理商是否还有其他选择?到目前为止我发现的关于这个主题的唯一帖子是2009年http://www.markhneedham.com/blog/2009/05/30/f-testing-asynchronous-calls-to-mailboxprocessor/
的博文答案 0 :(得分:3)
这是一个艰难的问题,我一直试图解决这个问题。这是我到目前为止所发现的,评论时间太长了,但我还是毫不犹豫地称之为完整答案......
从最简单到最复杂,取决于您想要测试的彻底程度,以及代理逻辑的复杂程度。
对于小型代理来说,您所拥有的只是一件好事,它的唯一作用是序列化对异步资源的访问,很少或没有内部状态处理。如果您像示例中那样提供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)
这似乎是对代理&#34;接口&#34;的一个人为要求,但是模拟方法调用很方便,并且测试它是微不足道的 - 等待{{1} (超时),你可以摆脱大多数测试助手代码。
由于您对提供的函数和PostAndAsyncReply
进行了单独调用,因此响应也可以反映代理状态,而不仅仅是函数结果。
我认为这是我最常见的内容。
如果代理程序封装了更复杂的行为,我发现跳过测试单个消息并使用基于模型的测试来验证针对预期外部行为模型的整个操作序列是很方便的。我正在使用FsCheck.Experimental API:
在你的情况下,这是可行的,但由于没有内部状态来建模,因此没有多大意义。为了举例说明在我的特定情况下的情况,请考虑维护客户端WebSocket连接以将消息推送到客户端的代理。我无法共享整个代码,但界面看起来像这样
replyChannel.Reply
代理人在内部维持/// 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
。
现在为了测试这个,我可以根据非正式规范来模拟外部行为,例如:&#34;根据调用MessageConsumer函数的结果,发送到订阅的客户端可能成功或失败。并且&#34;发送给未订阅的客户端不应该调用任何MessageConsumer&#34;。所以我可以像这样定义类型来为代理建模。
Map<ClientInfo, MessageConsumer>
然后使用FsCheck.Experimental定义添加和删除具有不同成功消费者的客户端并尝试向其发送数据的操作。然后,FsCheck生成随机操作序列,并在每个步骤之间根据模型验证代理实现。
这确实需要一些额外的&#34;仅测试&#34;代码并且在开始时具有显着的心理开销,但允许您测试相对复杂的有状态逻辑。我特别喜欢的是它帮助我测试整个合同,而不仅仅是测试单个函数/方法/消息,就像基于属性的/生成测试有助于测试的不仅仅是单个值一样。
我还没有走得那么远,但我作为替代方案也听说过Akka.NET用于成熟的演员模型支持,并使用其测试设施您在特殊测试上下文中运行代理,验证预期的消息等。正如我所说,我没有第一手经验,但似乎是更复杂的有状态逻辑的可行选择(即使在一台机器上,也不在分布式多节点演员系统中)。