Akka单元测试

80931836 2019-06-27

actor测试需了解scalatest,在多节点测试时,还需要使用sbt进行。

scalatest

scalatest是一个特别针对scala语言设计的单元测试框架,除了提供必要的基类和断言系统外,scalatest可以与IntelliJ IDEAmaven等IDE或构建工具集成。akka提供的测试框架就是在scalatest的基础上构建的,所以有必要先了解scalatest

scalatest提供了多种测试代码的书写风格,由于akka的例子大多是WordSpec风格的,所以建议优先研究和掌握WordSpec风格。

只要是安装有scala语言插件的IntelliJ IDEA,是默认可以继承scalatest的。这意味着你可以通过右键测试代码,就会弹出Run Test ...,甚至可以Debug Test ...。而且无需像JUnit那样为测试类或方法书写注解。这些注解在scalatest提供的trait中已经包含了。

在与maven集成时,需要在pom中引入插件,这样可以通过mvn test来运行测试

<!-- disable surefire -->
<plugin>
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-surefire-plugin</artifactId>
  <version>2.7</version>
  <configuration>
    <skipTests>true</skipTests>
  </configuration>
</plugin>
<!-- enable scalatest -->
<plugin>
  <groupId>org.scalatest</groupId>
  <artifactId>scalatest-maven-plugin</artifactId>
  <version>1.0</version>
  <configuration>
    <reportsDirectory>${project.build.directory}/surefire-reports</reportsDirectory>
    <junitxml>.</junitxml>
    <filereports>WDF TestSuite.txt</filereports>
  </configuration>
  <executions>
    <execution>
      <id>test</id>
      <goals>
        <goal>test</goal>
      </goals>
    </execution>
  </executions>
</plugin>

akka单节点测试

akka提供了一个特殊的测试框架akka-testkit

<dependency>
  <groupId>com.typesafe.akka</groupId>
  <artifactId>akka-testkit_2.12</artifactId>
  <version>2.5.13</version>
  <scope>test</scope>
</dependency>

官方例子:

import akka.actor.ActorSystem
import akka.testkit.{ ImplicitSender, TestActors, TestKit }
import org.scalatest.{ BeforeAndAfterAll, Matchers, WordSpecLike }

class MySpec() extends TestKit(ActorSystem("MySpec")) with ImplicitSender
  with WordSpecLike with Matchers with BeforeAndAfterAll {

  override def afterAll {
    TestKit.shutdownActorSystem(system)
  }

  "An Echo actor" must {

    "send back messages unchanged" in {
      val echo = system.actorOf(TestActors.echoActorProps)
      echo ! "hello world"
      expectMsg("hello world")
    }

  }
}
  • TestKit是akka提供的testkit类,每一个测试类需继承
  • WordSpecLike主要用于编写WordSpec风格的测试用例代码
  • BeforeAndAfterAll用于覆写afterAll
  • 测试程序启动时,会自动创建一个actorsystem,可以向上述代码那样,创建actor,发消息。通过expectXXX来断言收到消息。这里隐含一个叫testActor的实例,这个实例是作为发送消息的源,当书写expectXXX时,实际是期望testActor收到消息。
  • ImplicitSender用于将testActor映射到self

sbt构建

sbt是scala官方的构建系统。之所以要学习sbt,是因为想进行akka多节点测试的话,目前必须采用sbt构建。因为akka官方只为sbt创建了多节点测试的测试框架。

sbt的内容这里不详述,从下面多节点测试中体会。

akka多节点测试

官方文档描述了如何进行akka的多节点测试。

首先,应该引入akka的多节点测试框架:

libraryDependencies += "com.typesafe.akka" %% "akka-multi-node-testkit" % "2.5.13"

然后引入插件,在project/plugins.sbt中加入:

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

最终build.sbt应该看起来是这样的:

ThisBuild / version := "0.1.0"
ThisBuild / scalaVersion := "2.11.8"
ThisBuild / organization := "com.eoi.dc"

lazy val eoilib = (project in file("."))
  .enablePlugins(MultiJvmPlugin)
  .configs(MultiJvm)
  .settings(
    name := "eoilib",
    libraryDependencies ++= Seq(  
      "com.typesafe.akka" %% "akka-cluster" % "2.5.13",
      "org.scalactic" %% "scalactic" % "3.0.5",
      "org.scalatest" %% "scalatest" % "3.0.5",
      "com.typesafe.akka" %% "akka-multi-node-testkit" % "2.5.13",
    )
  )

测试代码如下:

package com.eoi.dc.lib.test

//#config
import akka.remote.testkit.MultiNodeConfig

object MultiNodeSampleConfig extends MultiNodeConfig {
  val node1 = role("node1")
  val node2 = role("node2")
}
//#config

//#spec
import akka.actor.{Actor, Props}
import akka.remote.testkit.MultiNodeSpec

class MultiNodeSampleSpecMultiJvmNode1 extends MultiNodeSample
class MultiNodeSampleSpecMultiJvmNode2 extends MultiNodeSample

object MultiNodeSample {
  class Ponger extends Actor {
    def receive = {
      case "ping" => sender() ! "pong"
    }
  }
}

class MultiNodeSample extends MultiNodeSpec(MultiNodeSampleConfig)
  with STMultiNodeSpec {

  import MultiNodeSample._
  import MultiNodeSampleConfig._

  def initialParticipants = roles.size

  "A MultiNodeSample" must {

    "wait for all nodes to enter a barrier" in {
      enterBarrier("startup")
    }

    "send to and receive from a remote node" in {
      runOn(node1) {
        enterBarrier("deployed")
        val ponger = system.actorSelection(node(node2) / "user" / "ponger")
        ponger ! "ping"
        import scala.concurrent.duration._
        expectMsg(10.seconds, "pong")
      }

      runOn(node2) {
        system.actorOf(Props[Ponger], "ponger")
        enterBarrier("deployed")
      }

      enterBarrier("finished")
    }
  }
}
//#spec

运行时:

> multi-jvm:testOnly MultiNodeSampleSpec

注意到代码中MultiNodeSampleSpecMultiJvmNode1MultiNodeSampleSpecMultiJvmNode2这两个类是有命名约定的,{TestName}MultiJvm{node},这里的TestName对应测试名,而node对应MultiNodeConfig

val node1 = role("node1")
val node2 = role("node2")

在具体的测试代码中,我们可以通过runOn来控制代码在哪个节点上运行。通过enterBarrier来同步多个节点的代码运行(简单的说就是,enterBarrier将当前节点放入某种状态,状态名随便定义,只有当所有的节点都进入这个状态后,代码才能运行下去,否则就block。

相关推荐