SilentActor Examples

我们从 SilentActor 开始。这是我们的第一个测试,我们通过使用 ScalaTest 来简要看看:

Listing 3.2 First test for the silent actor type

class SilentActor01Test extends TestKit(ActorSystem("testsystem"))
  with WordSpec
  with MustMatchers
  with StopSystemAfterAll {

    "A Silent Actor" must {
      "change state when it receives a message, single threaded" in {
        //Write the test, first fail
        fail("not implemented yet")
      }
      "change state when it receives a message, multi-threaded" in {
        //Write the test, first fail
        fail("not implemented yet")
      }
    }
  }
  1. 从 TestKit 扩展,提供用来测试的 actor 系统,
  2. WordSpec 提供了易于阅读的 DSL 来以 BDD 方式测试,
  3. MustMatchers 提供了易于阅读的断言,
  4. 确保系统在所有测试完成后停止,
  5. 以文字说明的方式写测试,
  6. 每个 'in' 描述一个特定的测试

上述代码是我们用来启动静默的 actor 测试的基本框架。

我们使用 WordSpec 方式的测试,因为这允许以文本说明的方式来写测试,这也会在测试运行时看到。 在上述代码中,我们用一个声称“当收到消息时,改变内部状态”的测试,为静默 actor 类型创建了说明。 现在因为还没有实现,它总是失败,如在”红-绿-重构“风格中预期的一样。以”红-绿-重构“风格中,你先让 测试失败(红),然后实现代码,让它通过(绿),之后你再重构代码,令其更优雅。 首先我们在单线程环境中测试静默 actor。 既然我们想以后测试在多线程环境中是否工作正常,我们就包含了 TestKit。如果你只想使用 TestActorRef,这不是必需的。 下边我们定义了一个空的 Actor,会让测试失败:

Listing 3.3 第一个静默 actor 类型的会失败的实现

class SilentActor extends Actor {
  def receive = {
    case msg => ()
  }
}
  • 压制消息,不保存任何内部状态

现在首先来写一个测试,给静默 actor 发送一个消息,检查是不是改变了内部状态。 我们得写 SilentActor actor 来让测试能通过,还需要写叫做 SilentActorProtocol 的对象。 对象包含所有的 SilentActor 支持的消息,像后边所看到的一样,这是一个把相关的消息归类的很好的方法。

Listing 3.4 单线程测试内部状态

"change internal state when it receives a message, single" in {
  import SilentActorProtocol._
  val silentActor = TestActorRef[SilentActor]
  silentActor ! SilentMessage("whisper")
silentActor.underlyingActor.state must (contain("whisper"))
}
  • 引入消息
  • 创建单线程测试用的 TestActorRef
  • 拿到底层的 actor, 检查其状态

这是典型 TDD 场景的最简版:触发,然后检查状态改变。 现在来写 SilentActor actor:

Listing 3.5 SilentActor 实现

    object SilentActorProtocol {
    case class SilentMessage(data: String)
    case class GetState(receiver: ActorRef)
    }
    class SilentActor extends Actor {
    import SilentActorProtocol._
    var internalState = Vector[String]()
    def receive = {
    case SilentMessage(data) =>
    internalState = internalState :+ data
    }
    def state = internalState
    }
    }
  1. 'protocol' 把相关信息保存在一起
  2. SilentActor 可以处理的消息类型
  3. 状态保存在一个 Vector 中,每个消息都加到这个 Vector 中
  4. 状态方法返回创建的 Vector

因为返回的列表是不可变的,测试不能改变列表,检查预期结果的时候就会有问题。 设置/更新变量 internalState 完全安全,因为 Actor 防止被多线程访问。 一般优先选择 vars 配合不可变数据结构是好习惯,而不是 vals 配合可变数据结构。

现在我们来看看这个测试的多线程版,你会看到我们需要改变的代码很少。 就像单线程版中,我们添加状态方法来使得可以测试 actor,我们也需要增加一些代码 让多线程版本也能测试。

Listing 3.6 内部状态的多线程测试

    "change internal state when it receives a message, multi" in {
    import SilentActorProtocol._
    val silentActor = system.actorOf(Props[SilentActor], "s3")
    silentActor ! SilentMessage("whisper1")
    silentActor ! SilentMessage("whisper2")
    silentActor ! GetState(testActor)
    expectMsg(Vector("whisper1", "whisper2"))
    }
  1. 把相关消息保存在一起的 'protocol'
  2. 测试系统用来创建 actor
  3. 把消息加到协议中来获取状态
  4. 用来检查发送给了 testActor 什么消息

多线程测试使用名为 "testsystem" 的 ActorSystem 来创建 SilentActor actor。 ActorSystem 是 TestKit 的一部分。 因为当使用多线程 actor 系统时,我们不能访问 actor 实例,我们需要用另一种方法来查看状态改变。 因为这个原因,GetState 消息加了进来,它引用 ActorRef。 TestKit 有一个 testActor,你可以用来接收期望的消息。 我们添加的 GetState 方法如此(TODO:简单?还是什么),我们可以直接让 SilentActor 发送内部状态。 那样我们可以调用 expectMsg 方法,它期待一个消息会发送到 testActor,判断这个消息,在这里,消息是一个包含所有数据字段的 Vector。

侧边栏: 方法 expectMsg* 的时间设置

TestKit 有多个版本的 expectMsg 和其他方法来判断消息。 所有这些方法在一定时间内等待一个消息,否则就超时并抛出异常。 超时有默认值,可以通过配置中的 "akka.test.single-expect-default" 来修改。 有一个伸缩系数可以计算超时的实际时间(正常设为 1,表示超时没有伸缩)。 它的目的是为计算能力各不相同的计算机提供各种分级方法,使得在较慢的机器上, 我们需要等的时间长一点(开发者在速度很快的工作站上测试后,提交到较难的持续集成服务器上, 然后测试失败了,这很常见)。 每个机器可以配置需要达到的参数(配置的更多细节参见第4章)。 最大超时时间也可以通过函数方法直接设置,但是最好只使用配置的值,如果必要时,通过修改配置来 改变这个影响所有测试的值。

现在我们需要的是还能处理 GetState 消息的静默的 actor 的代码:

Listing 3.7 SilentActor 实现

    object SilentActorProtocol {
    case class SilentMessage(data: String)
    case class GetState(receiver: ActorRef)
    }
    class SilentActor extends Actor {
    import SilentActorProtocol._
    var internalState = Vector[String]()
    def receive = {
    case SilentMessage(data) =>
    internalState = internalState :+ data
    case GetState(receiver) => receiver ! internalState
    }
    }
  1. 为了测试目的,加了 GetState 消息
  2. 内部状态发送给 GetStatef 中的 ActorRef

内部状态发送回 GetState 消息中的 ActorRef,本例中的 ActorRef 会是 testActor。 因为内部状态是个不变的 Vector,这是完全安全的。 这就是 SilentActor 类型:单线程和多线程变型。 使用这些方法,我们可以构建大多数程序员都非常熟悉的测试:可以检查状态变化, 变化时可以运用 TestKit 中的一些工具来判断。