6.2.5 多JVM 测试

sbt 多 jvm 插件使得可以在多个 JVM 间运行测试,既然应用成了分布式的,这正是我们想做的。 sbt 多 jvm 插件需要 sbt 在 project/plugins.sbt 文件中注册:

addSbtPlugin("com.typesafe.sbt" % "sbt-multi-jvm" % "0.3.5")

我们还需要增加一个使用它的 sbt 构建文件。 多 JVM 插件只支持 SBT 项目文件的 scala DSL 版本,所以我们需要在 chapter6/project 目录下增加 GoTicksBuild.scala 文件。 SBT 会自动合并 build.sbt 和下面的文件,这意味着列表 6.12 中的依赖关系不需要复制。

Listing 6.12 多 JVM 配置

import sbt._
import Keys._
import com.typesafe.sbt.SbtMultiJvm
import com.typesafe.sbt.SbtMultiJvm.MultiJvmKeys.{ MultiJvm }

object GoTicksBuild extends Build {
    lazy val buildSettings = Defaults.defaultSettings ++
                             multiJvmSettings ++
                             Seq(
        crossPaths   := false
    )

    lazy val goticks = Project(
        id = "goticks",
        base = file("."),
        settings = buildSettings ++ Project.defaultSettings
    ) configs(MultiJvm)

    lazy val multiJvmSettings = SbtMultiJvm.multiJvmSettings ++
    Seq(
        compile in MultiJvm <<=
            (compile in MultiJvm) triggeredBy (compile in Test),
        parallelExecution in Test := false,
        executeTests in Test <<=
        ((executeTests in Test), (executeTests in MultiJvm)) map {
            case ((_, testResults), (_, multiJvmResults))  =>
            val results = testResults ++ multiJvmResults
            (Tests.overall(results.values), results)
        }
    )
}
  • 保证我们的测试在默认测试编译中
  • 关闭并行执行
  • 保证作为默认测试目标的一部分

如果你不是 SBT 专家,不用担心这里的构建文件的细节。 上述代码基本配置了多 JVM 插件,保证多 jvm 测试随着正常的单元测试执行。 SBT 实战( http://www.manning.com/suereth2/ ) 在解释 SBT 细节方面做的非常好,如果你想知道更多,可以参考这本书。

默认情况下,多 JVM 测试需要添加到 src/multi-jvm/scala 目录下。 既然我们的工程已经针对多 jvm 测试正确建立起来了,我们可以启动 goticks.com 应用的前台和后台的单元测试。 首先需要定义 MultiNodeConfig,其描述了测试节点的角色。 下面的列表展示了针对客户-服务器(前台和后台)配置的多节点配置:

object ClientServerConfig extends MultiNodeConfig {
    val frontend = role("frontend")
    val backend = role("backend")
}
  • 前台角色
  • 后台角色

如你预期的,需要定义两个角色,前台和后台。 角色用来识别单元测试的节点,在每个用来测试的节点上执行指定代码。 开始写测试前,我们需要写一些基础设施代码来将测试连进 scalatest

import akka.remote.testkit.MultiNodeSpecCallbacks
import org.scalatest.{BeforeAndAfterAll, WordSpec}
import org.scalatest.matchers.MustMatchers

trait STMultiNodeSpec extends MultiNodeSpecCallbacks
    with WordSpec with MustMatchers with BeforeAndAfterAll {
        override def beforeAll() = multiNodeSpecBeforeAll()
        override def afterAll() = multiNodeSpecAfterAll()
    }
  • 通过集成 TestKit 中的类来获得调用
  • 获得我们需要的测试 trait 其他内容
  • 让我们所有的测试使用我们的 before 和 after 方法

这个 trait 用来启动和关闭多个节点的测试,你可以对你所有的多节点测试重用它。 它被混入了单元测试描述。 现在来看看下边展示的测试。 我们首先需要做的是,创建 MultiNodeSpec,其混入了我们刚定义的 STMultiNodeSpec。 两个版本的 ClientServerSpec 需要运行在两个不同的 JVM 上。 6.13 中的代码展示了为了这个目的两个 ClientServerSpec 类是如何定义的。

Listing 6.13 多节点测试的 Spec 类

class ClientServerSpecMultiJvmFrontend extends ClientServerSpec
class ClientServerSpecMultiJvmBackend extends ClientServerSpec

class ClientServerSpec extends MultiNodeSpec(ClientServerConfig)
with STMultiNodeSpec with ImplicitSender {
    def initialParticipants = roles.size
  • 在前台 JVM 上运行的 Spec
  • 在后台 JVM 上运行的 Spec
  • 参与近测试的节点数
  • 描述了两个节点应该做什么的 Spec

ClientServerSpec 使用了 STMultiNodeSpec 和 ImplicitSender trait。 ImplicitSender 特质设定 testActor 作为所有消息默认的发送者,这使得可以只调用 expectMsg 和其他断言函数,不需要每次都设置 testActor 作为消息的发送者。 列表 6.14 中的代码展示了我们如何做到的。

Listing 6.14 配置 TestActor

import ClientServerConfig._

trait TestRemoteBoxOfficeCreator
    extends RemoteBoxOfficeCreator { this:Actor =>
    override def createPath: String = {
        val actorPath = node(backend) / "user" /"boxOffice"
        actorPath.toString
    }
}
  • 引入配置,这样我们就可以访问后台角色。
  • TestRemoteBoxOfficeCreator 会被用在测试中,而不是 RemoteBoxOfficeCreator。
  • 重载 createPath 方法,这样它可以返回一个路径给后台节点上的测试系统用来测试。
  • node() 方法测试期间返回后台角色节点的地址。

这个表达式创建了一个 ActorPath。 后台和前台角色节点默认在随机的节点上运行。 TestRemoteBoxOfficeCreator 在测试中代替了 RemoteBoxOfficeCreator,因为它根据 frontend.conf 文件中配置的主机,端口和 actor 名来创建路径。 这里我们在测试过程中想使用后台角色节点的地址,在那个节点上查询到 boxOffice actor 的引用。 上面的代码做到了这一点。 列表 6.15 展示了我们分布式架构的测试:

Listing 6.15 测试分布式架构

"A Client Server configured app" must {
"wait for all nodes to enter a barrier" in {
    enterBarrier("startup")
}

"be able to create an event and sell a ticket" in {
    runOn(frontend) {
        enterBarrier("deployed")
        val restInterface = system.actorOf(
            Props(new RestInterfaceMock
                with TestRemoteBoxOfficeCreator))
        val path = node(backend) / "user" / "boxOffice"
        val actorSelection = system.actorSelection(path)
        actorSelection.tell(Identify(path), testActor)
        val actorRef = expectMsgPF() {
            case ActorIdentity(`path`, ref) => ref
        }
        restInterface ! Event("RHCP", 1)
        expectMsg(EventCreated)
        restInterface ! TicketRequest("RHCP")
        expectMsg(Ticket("RHCP", 1))
    }

    runOn(backend) {
        system.actorOf(Props[BoxOffice], "boxOffice")
        enterBarrier("deployed")
    }

    enterBarrier("finished")
  }
}
  • 启动所有节点
  • 前台和后台节点的测试场景
  • 在前台 JVM 上运行块中的代码
  • 等待后台节点部署
  • 创建打桩的 Rest 接口
  • 获取一个到远程票房的 actor selection(TODO:what)
  • 发送认证信息给 actor selection
  • 等待 boxOffice 报告其可用。RemoteLookup 类会走一遍获取到 boxOffice 的 ActorRef 的流程。
  • 用 TestKit 来等待消息,和平常一样
  • 在后台 JVM 中运行块中的代码
  • 用名字 boxOffice 创建 boxOffice,使得 RemoteLookup 能找到它
  • 发信号称后台已经部署
  • 表明测试完成

这里实际做了很多工作。 单元测试可以分解成四块。 首先,它等待所有节点启动,通过使用 enterBarrier("startup") 调用,其在两个节点上都执行。 然后实际的测试继续运行,指定在前台和后台节点上应该运行什么代码。 前台等待后台发信号称已经部署好,然后执行测试。

后台节点只启动 boxOffice,这样它可以在前台节点中使用。 因为如果要使用真实地 RestInterface,我们就得增加 HTTP 客户端请求,现在我们使用 RestInterfaceMock 类。 这个 actor 混入了 TestRemoteBoxOfficeCreator trait,其行为几乎与 RemoteBoxOfficeCreator trait 一致,除了在测试用从后台节点获取路径。 因为仍然使用 RemoteLookup actor(createBoxOffice 方法没有重载),我们需要等待远程 actorRef 完成认证。 actorSelection 用来做这个,在我们开始发送消息给远程的 boxOffice 开始测试前,我们等待一个 ActorIdentity 消息。

这之后我们终于可以测试前台和后台节点间的交互了。 我们可以使用第二章中的相同的方法来等待消息。 多 JVM 测试可以通过在 sbt 中运行 multi-jvm:test 命令来执行,试一试。

图 6.6 展示了实际的流程。 注意多个合作者之间的合作,以及他们的运行时,multi-jvm 测试工具将其做的相当地自动化。 亲手做这个会有相当大的工作量。

Figure 6.6 Multi-JVM 测试流程

chapter6 工程也有一个单节点版本的应用的单元测试,出了一些基础设施的安装,测试基本是一样的。 这里 multi-jvm 测试的例子展示了一个最初是单节点的应用如何修改地运行在两个节点上。 单节点与客户服务器安装之间的巨大差别在于,如何找到对远程系统的 actor 引用,是查询,还是远程部署。 在 RestInterface 和 boxOffice 安排一个 Remote Lookup 增加了灵活性,能在崩溃中存活。 在例子中的单元测试中,有一个有趣的问题要解决:我们如何等着对 boxOffice 的远程 ActorRef 可用,答案就是 actorSelection 和 Identity 消息。

这总结了我们对 multi-node-testkit 模块的第一印象。 我们在后面的章节会看到更多。 上面的测试展示了 goticks.com 应用可以在分布式环境下进行单元测试。 本例中它运行在单台机器的两个 JVM 中。 如我们在第十三章中看到的, multi-node-testkit 也可以用在多个服务器的单元测试中。