3.2.2 例子 SendingActor
返回第 1 章中售票的例子,我们需要测试,买了一张票后,可售票的总数减一了。 湖人对公牛的比赛要开始了,我们要能支持任意数量的买票需求。 因为设计TicketingAgent 的目的就是删除一张票,然后把票传给下一个 TicketingAgent。 我们要做的是创建一个 SendingActor,插入到链中作为下一个接收者,这样我们就能看到 所有票的状态,开始后就可以断言比原来少了一个。
Listing 3.8 Kiosk01 测试
"A Sending Actor" must {
"send a message to an actor when it has finished" in {
import Kiosk01Protocol._
val props = Props(new Kiosk01(testActor))
val sendingActor = system.actorOf(props, "kiosk1")
val tickets = Vector(Ticket(1), Ticket(2), Ticket(3))
val game = Game("Lakers vs Bulls", tickets)
sendingActor ! game
expectMsgPF() {
case Game(_, tickets) =>
tickets.size must be(game.tickets.size - 1)
}
}
}
- 下一个 TicketingAgent 传递给构造函数,测试中我们传入 testActor
- Event 消息创建时有三张票
- testActor 应该收到事件
- testActor 收到的消息中,票应该少了一张
比赛发送给名为 Agent01 的 actor,它处理 Event,拿走一张票,然后再把比赛发给下一个代理。 测试中我们传入 testActor 而不是另一个 Agent,这很容易做,因为 nextAgent 就是个 ActorRef。 因为我们不能确切知道哪张票被拿走,我们就不能使用 expetMsg(msg),因为不能精确地匹配。 本例中我们使用 expectMsgPF,它接受偏函数,就像 actor 的接收过程. 这里我们匹配发送给 testActor的消息,它应该是个票数少一的 Event。 当然,如果我们现在运行测试,它会失败,因为我们还没有在 Agent01 中实现消息协议。 我们现在就来做:
Listing 3.9 Agent01 实现
object Kiosk01Protocol {
case class Ticket(seat: Int)
case class Game(name: String, tickets: Seq[Ticket])
}
class Kiosk01(nextKiosk: ActorRef) extends Actor {
import Kiosk01Protocol._
def receive = {
case game @ Game(_, tickets) =>
nextKiosk ! game.copy(tickets = tickets.tail)
}
}
- 下一个 Agent 传递给构造函数,测试中我们传递的是 testActor
- 简化的 Ticket 消息
- Event 中包含着票
- 不可变的拷贝,由少了一张票的 Event 消息组成
我们再次创建一个把所有相关消息保存在一起的协议。 名为 Agent1 的 actor 匹配 Event 消息,从中取出票(在第一个字段中并不关心哪个是事件的名字), 然后给名为 event 的消息取一个别名。 下一步,使用每个 case 类都有的拷贝方法创建一个不可变的拷贝。 拷贝中只包含票列表的尾部,如果没票,这就是个空列表,否则就是列表中第一张票之外的所有票。 我们再次利用了 class 类的不可变属性。 事件发送给 nextAgent。
我们来看看 SendingActor 类型的变型。这里是一些常见的变型:
Table 3.1 SendingActor 类型
| Actor | 描述 |
| MutatingCopyActor | actor 创建可以可变的拷贝,发送给下一个 actor,这是我们刚看到的情形。 |
| ForwardingActor | actor 转发收到的消息,不做任何改变。 |
| TransformingActor | actor 根据收到的消息,创建一个不同类型的消息。 |
| SequancingActor | actor 根据收到的一个消息,创建多个消息,并将这些消息逐个发送给另一个 actor |
MutatingCopyActor, ForwardingActor, 和 TransformingActor 都可以以相同的方式测试。 我们可以传入 testActor 作为下一个接收消息的 actor,然后使用 expectMsg 或 expectMsgPF 来检查消息。 FilteringActor 有点不一样,它解决的问题是我们怎么断言某些消息没有通过测试。 SequencingActor 需要类似的方法(TODO:这是什么意思?之前没有?这不是已经实现的 actor 吗?还是本书中需要实现的?) 我们如何断言收到消息的数目正确?下一个测试会展示。 我们来写一个 FilteringActor 的测试。 我们即将构建的 FilteringActor 应该过滤出重复的事件。 它会保存最近收到的消息的列表,对着这个列表来检查新来的每一个消息,来找出是否重复。 (这类似于 mocking 框架中的典型元素,允许你对调用进行断言,调用次数,以及没有调用)
Listing 3.10 FilteringActor 测试
"filter out particular messages" in {
import FilteringActorProtocol._
val props = Props(new FilteringActor(testActor, 5))
val filter = system.actorOf(props, "filter-1")
filter ! Event(1)
filter ! Event(2)
filter ! Event(1)
filter ! Event(3)
filter ! Event(1)
filter ! Event(4)
filter ! Event(5)
filter ! Event(5)
filter ! Event(6)
val eventIds = receiveWhile() {
case Event(id) if id <= 5 => id
}
eventIds must be(List(1, 2, 3, 4, 5))
expectMsg(Event(6))
}
- 发送一些消息,包括重复的
- 接收消息,直到 case 语句不匹配任何东西
- 断言结果中没有重复消息
测试使用了 receiveWhile 方法来收集 testActor 接收的消息,直到 case 表达式匹配。 测试用 Event(6) 不匹配 case 表达式中的模式,case 表达式限定所有 id 小于等于 5 的 Events 才匹配,并从 while 循环中跳出。 receiveWhile 方法返回收集的条目,在偏函数中以列表的形式返回,不允许有任何重复。 现在我们来编写能保证规格这一部分的 FilteringActor:
Listing 3.11 FilteringActor 实现
object FilteringActorProtocol {
case class Event(id: Long)
}
class FilteringActor(nextActor: ActorRef,
bufferSize: Int) extends Actor {
import FilteringActorProtocol._
var lastMessages = Vector[Event]()
def receive = {
case msg: Event =>
if (!lastMessages.contains(msg)) {
lastMessages = lastMessages :+ msg
nextActor ! msg
if (lastMessages.size > bufferSize) {
// discard the oldest
lastMessages = lastMessages.tail
}
}
}
}
- 缓存的最大大小通过构造函数传入
- 最近消息以 Vector 保存
- 事件如果在缓存中没找到,就发送给下一个 actor
- 当达到最大大小,丢弃缓存中最早的事件
上述 FilteringActor 用 Vector 缓存最近收到的消息,如果在列表中不存在,就将其加入其中。 只有不在缓存中的消息才发送到 nextActor。 如果达到最大的 bufferSize, 丢弃最早的消息以防止 lastMessages 列表增长太大,可能导致空间不足。
receiveWhile 方法也可以用来测试 SequencingActor; 你可以断言特定事件引发的一组消息 正如预期中的一样。 当你需要对大量消息进行断言时,这两个对消息进行断言的方法, ignoreMsg 和 expectNoMsg 会很方便。 ignoreMsg 像 expectMsgPF 一样,以一个偏函数做参数,差别只在于,不是断言消息,而是忽略任何匹配的消息。 如果你不是关注所有消息,而只想断言发送给 testActor 的特定消息,这就很方便, expectNoMsg 断言一定时间内消息不能发送给 testActor,我们在之前的 FilteringActor 测试重复消息的发送中可以使用这个方法的。 3.12 中的测试展示了一个使用 expectNoMsg 的例子:
Listing 3.12 FilteringActor 实现
"filter out particular messages using expectNoMsg" in {
import FilteringActorProtocol._
val props = Props(new FilteringActor(testActor, 5))
val filter = system.actorOf(props, "filter-2")
filter ! Event(1)
filter ! Event(2)
expectMsg(Event(1))
expectMsg(Event(2))
filter ! Event(1)
expectNoMsg
filter ! Event(3)
expectMsg(Event(3))
filter ! Event(1)
expectNoMsg
filter ! Event(4)
filter ! Event(5)
filter ! Event(5)
expectMsg(Event(4))
expectMsg(Event(5))
expectNoMsg()
}
因为 expectNoMsg 得等到超时以保证没有收到消息,上边的测试运行的会比较慢。
如我们看到的,TestKit 提供了接收消息的 testActor,可以用 expectNoMsg 或其他方法来断言。 TestKit 只有一个 testActor,是需要你扩展的类,你如何测试给多个 actor 发送消息的 actor 呢?答案是 TestProbe 类。 TestProbe 类非常类似 TestKit 类,只是你可以直接用它,而不需要扩展它。 简单地用 TestProbe() 创建一个 TestProbe 实例,然后开始使用它。 在介绍负载平衡和路由的第 8 章中,我们会经常在测试中使用 TestProbe。