Describe how to write cluster test, see #2437

This commit is contained in:
Patrik Nordwall 2012-09-21 11:47:50 +02:00
parent b88f8e4b85
commit 7bb125385b
5 changed files with 160 additions and 41 deletions

View file

@ -141,6 +141,8 @@ Be aware of that using auto-down implies that two separate clusters will
automatically be formed in case of network partition. That might be
desired by some applications but not by others.
.. _cluster_subscriber:
Subscribe to Cluster Events
^^^^^^^^^^^^^^^^^^^^^^^^^^^
@ -339,6 +341,64 @@ service nodes and 1 client::
.. note:: The above example, especially the last part, will be simplified when the cluster handles automatic actor partitioning.
How to Test
^^^^^^^^^^^
:ref:`multi_node_testing` is useful for testing cluster applications.
Set up your project according to the instructions in :ref:`multi_node_testing` and :ref:`multi_jvm_testing`, i.e.
add the ``sbt-multi-jvm`` plugin and the dependency to ``akka-remote-tests-experimental``.
First, as described in :ref:`multi_node_testing`, we need some scaffolding to configure the ``MultiNodeSpec``.
Define the participating roles and their :ref:`cluster_configuration` in an object extending ``MultiNodeConfig``:
.. includecode:: ../../akka-samples/akka-sample-cluster/src/multi-jvm/scala/sample/cluster/stats/StatsSampleSpec.scala
:include: MultiNodeConfig
:exclude: router-lookup-config
Define one concrete test class for each role/node. These will be instantiated on the different nodes (JVMs). They can be
implemented differently, but often they are the same and extend an abstract test class, as illustrated here.
.. includecode:: ../../akka-samples/akka-sample-cluster/src/multi-jvm/scala/sample/cluster/stats/StatsSampleSpec.scala#concrete-tests
Note the naming convention of these classes. The name of the classes must end with ``MultiJvmNode1``, ``MultiJvmNode2``
and so on. It's possible to define another suffix to be used by the ``sbt-multi-jvm``, but the default should be
fine in most cases.
Then the abstract ``MultiNodeSpec``, which takes the ``MultiNodeConfig`` as constructor parameter.
.. includecode:: ../../akka-samples/akka-sample-cluster/src/multi-jvm/scala/sample/cluster/stats/StatsSampleSpec.scala#abstract-test
Most of this can of course be extracted to a separate trait to avoid repeating this in all your tests. This example
is using `Scalatest <http://www.scalatest.org/>`_, but similar can be done with other testing frameworks.
Typically you begin your test by starting up the cluster and let the members join, and create some actors.
That can be done like this:
.. includecode:: ../../akka-samples/akka-sample-cluster/src/multi-jvm/scala/sample/cluster/stats/StatsSampleSpec.scala#startup-cluster
From the test you interact with the cluster using the ``Cluster`` extension, e.g. ``join``.
.. includecode:: ../../akka-samples/akka-sample-cluster/src/multi-jvm/scala/sample/cluster/stats/StatsSampleSpec.scala#join
Notice how the `testActor` from :ref:`testkit <akka-testkit>` is added as :ref:`subscriber <cluster_subscriber>`
to cluster changes and then waiting for certain events, such as in this case all members becoming 'Up'.
The above code was running for all roles (JVMs). ``runOn`` is a convenient utility to declare that a certain block
of code should only run for a specific role.
.. includecode:: ../../akka-samples/akka-sample-cluster/src/multi-jvm/scala/sample/cluster/stats/StatsSampleSpec.scala#test-statsService
Once again we take advantage of the facilities in :ref:`testkit <akka-testkit>` to verify expected behavior.
Here using ``testActor`` as sender (via ``ImplicitSender``) and verifing the reply with ``expectMsgPF``.
In the above code you can see ``node(third)``, which is useful facility to get the root actor reference of
the actor system for a specific role. This can also be used to grab the ``akka.actor.Address`` of that node.
.. includecode:: ../../akka-samples/akka-sample-cluster/src/multi-jvm/scala/sample/cluster/stats/StatsSampleSpec.scala#addresses
.. _cluster_jmx:
JMX

View file

@ -5,8 +5,10 @@ import scala.concurrent.util.duration._
import com.typesafe.config.ConfigFactory
import StatsSampleSpec.first
import StatsSampleSpec.third
import org.scalatest.BeforeAndAfterAll
import org.scalatest.WordSpec
import org.scalatest.matchers.MustMatchers
import akka.actor.Props
import akka.actor.RootActorPath
import akka.cluster.Cluster
@ -16,10 +18,9 @@ import akka.cluster.ClusterEvent.CurrentClusterState
import akka.cluster.ClusterEvent.MemberUp
import akka.remote.testkit.MultiNodeConfig
import akka.remote.testkit.MultiNodeSpec
import akka.remote.testkit.STMultiNodeSpec
import akka.testkit.ImplicitSender
object StatsSampleSingleMasterSpec extends MultiNodeConfig {
object StatsSampleSingleMasterSpecConfig extends MultiNodeConfig {
// register the named roles (nodes) of the test
val first = role("first")
val second = role("second")
@ -50,17 +51,21 @@ object StatsSampleSingleMasterSpec extends MultiNodeConfig {
}
// need one concrete test class per node
class StatsSampleSingleMasterMultiJvmNode1 extends StatsSampleSingleMasterSpec
class StatsSampleSingleMasterMultiJvmNode2 extends StatsSampleSingleMasterSpec
class StatsSampleSingleMasterMultiJvmNode3 extends StatsSampleSingleMasterSpec
class StatsSampleSingleMasterSpecMultiJvmNode1 extends StatsSampleSingleMasterSpec
class StatsSampleSingleMasterSpecMultiJvmNode2 extends StatsSampleSingleMasterSpec
class StatsSampleSingleMasterSpecMultiJvmNode3 extends StatsSampleSingleMasterSpec
abstract class StatsSampleSingleMasterSpec extends MultiNodeSpec(StatsSampleSingleMasterSpec)
with STMultiNodeSpec with ImplicitSender {
abstract class StatsSampleSingleMasterSpec extends MultiNodeSpec(StatsSampleSingleMasterSpecConfig)
with WordSpec with MustMatchers with BeforeAndAfterAll with ImplicitSender {
import StatsSampleSpec._
import StatsSampleSingleMasterSpecConfig._
override def initialParticipants = roles.size
override def beforeAll() = multiNodeSpecBeforeAll()
override def afterAll() = multiNodeSpecAfterAll()
"The stats sample with single master" must {
"illustrate how to startup cluster" in within(10 seconds) {
Cluster(system).subscribe(testActor, classOf[MemberUp])

View file

@ -2,7 +2,6 @@ package sample.cluster.stats
import language.postfixOps
import scala.concurrent.util.duration._
import com.typesafe.config.ConfigFactory
import akka.actor.Props
import akka.actor.RootActorPath
@ -11,12 +10,12 @@ import akka.cluster.Member
import akka.cluster.MemberStatus
import akka.cluster.ClusterEvent.CurrentClusterState
import akka.cluster.ClusterEvent.MemberUp
import akka.remote.testkit.MultiNodeConfig
import akka.remote.testkit.MultiNodeSpec
import akka.remote.testkit.STMultiNodeSpec
import akka.testkit.ImplicitSender
object StatsSampleSpec extends MultiNodeConfig {
//#MultiNodeConfig
import akka.remote.testkit.MultiNodeConfig
import com.typesafe.config.ConfigFactory
object StatsSampleSpecConfig extends MultiNodeConfig {
// register the named roles (nodes) of the test
val first = role("first")
val second = role("second")
@ -44,49 +43,95 @@ object StatsSampleSpec extends MultiNodeConfig {
"""))
}
//#MultiNodeConfig
//#concrete-tests
// need one concrete test class per node
class StatsSampleMultiJvmNode1 extends StatsSampleSpec
class StatsSampleMultiJvmNode2 extends StatsSampleSpec
class StatsSampleMultiJvmNode3 extends StatsSampleSpec
class StatsSampleSpecMultiJvmNode1 extends StatsSampleSpec
class StatsSampleSpecMultiJvmNode2 extends StatsSampleSpec
class StatsSampleSpecMultiJvmNode3 extends StatsSampleSpec
//#concrete-tests
abstract class StatsSampleSpec extends MultiNodeSpec(StatsSampleSpec)
with STMultiNodeSpec with ImplicitSender {
//#abstract-test
import org.scalatest.BeforeAndAfterAll
import org.scalatest.WordSpec
import org.scalatest.matchers.MustMatchers
import akka.remote.testkit.MultiNodeSpec
import akka.testkit.ImplicitSender
import StatsSampleSpec._
abstract class StatsSampleSpec extends MultiNodeSpec(StatsSampleSpecConfig)
with WordSpec with MustMatchers with BeforeAndAfterAll
with ImplicitSender {
import StatsSampleSpecConfig._
override def initialParticipants = roles.size
override def beforeAll() = multiNodeSpecBeforeAll()
override def afterAll() = multiNodeSpecAfterAll()
//#abstract-test
"The stats sample" must {
//#startup-cluster
"illustrate how to startup cluster" in within(10 seconds) {
Cluster(system).subscribe(testActor, classOf[MemberUp])
expectMsgClass(classOf[CurrentClusterState])
Cluster(system) join node(first).address
//#addresses
val firstAddress = node(first).address
val secondAddress = node(second).address
val thirdAddress = node(third).address
//#addresses
//#join
Cluster(system) join firstAddress
//#join
system.actorOf(Props[StatsWorker], "statsWorker")
system.actorOf(Props[StatsService], "statsService")
expectMsgAllOf(
MemberUp(Member(node(first).address, MemberStatus.Up)),
MemberUp(Member(node(second).address, MemberStatus.Up)),
MemberUp(Member(node(third).address, MemberStatus.Up)))
MemberUp(Member(firstAddress, MemberStatus.Up)),
MemberUp(Member(secondAddress, MemberStatus.Up)),
MemberUp(Member(thirdAddress, MemberStatus.Up)))
Cluster(system).unsubscribe(testActor)
testConductor.enter("all-up")
}
//#startup-cluster
"show usage of the statsService" in within(5 seconds) {
val service = system.actorFor(RootActorPath(node(third).address) / "user" / "statsService")
//#test-statsService
"show usage of the statsService from one node" in within(5 seconds) {
runOn(second) {
val service = system.actorFor(node(third) / "user" / "statsService")
service ! StatsJob("this is the text that will be analyzed")
val meanWordLength = expectMsgPF() {
case StatsResult(meanWordLength) meanWordLength
}
meanWordLength must be(3.875 plusOrMinus 0.001)
}
testConductor.enter("done-2")
}
//#test-statsService
"show usage of the statsService from all nodes" in within(5 seconds) {
val service = system.actorFor(node(third) / "user" / "statsService")
service ! StatsJob("this is the text that will be analyzed")
val meanWordLength = expectMsgPF() {
case StatsResult(meanWordLength) meanWordLength
}
meanWordLength must be(3.875 plusOrMinus 0.001)
testConductor.enter("done")
testConductor.enter("done-2")
}
}
}

View file

@ -5,14 +5,17 @@ import scala.concurrent.util.duration._
import com.typesafe.config.ConfigFactory
import org.scalatest.BeforeAndAfterAll
import org.scalatest.WordSpec
import org.scalatest.matchers.MustMatchers
import akka.actor.Props
import akka.cluster.Cluster
import akka.remote.testkit.MultiNodeConfig
import akka.remote.testkit.MultiNodeSpec
import akka.remote.testkit.STMultiNodeSpec
import akka.testkit.ImplicitSender
object TransformationSampleSpec extends MultiNodeConfig {
object TransformationSampleSpecConfig extends MultiNodeConfig {
// register the named roles (nodes) of the test
val frontend1 = role("frontend1")
val frontend2 = role("frontend2")
@ -31,19 +34,23 @@ object TransformationSampleSpec extends MultiNodeConfig {
}
// need one concrete test class per node
class TransformationSampleMultiJvmNode1 extends TransformationSampleSpec
class TransformationSampleMultiJvmNode2 extends TransformationSampleSpec
class TransformationSampleMultiJvmNode3 extends TransformationSampleSpec
class TransformationSampleMultiJvmNode4 extends TransformationSampleSpec
class TransformationSampleMultiJvmNode5 extends TransformationSampleSpec
class TransformationSampleSpecMultiJvmNode1 extends TransformationSampleSpec
class TransformationSampleSpecMultiJvmNode2 extends TransformationSampleSpec
class TransformationSampleSpecMultiJvmNode3 extends TransformationSampleSpec
class TransformationSampleSpecMultiJvmNode4 extends TransformationSampleSpec
class TransformationSampleSpecMultiJvmNode5 extends TransformationSampleSpec
abstract class TransformationSampleSpec extends MultiNodeSpec(TransformationSampleSpec)
with STMultiNodeSpec with ImplicitSender {
abstract class TransformationSampleSpec extends MultiNodeSpec(TransformationSampleSpecConfig)
with WordSpec with MustMatchers with BeforeAndAfterAll with ImplicitSender {
import TransformationSampleSpec._
import TransformationSampleSpecConfig._
override def initialParticipants = roles.size
override def beforeAll() = multiNodeSpecBeforeAll()
override def afterAll() = multiNodeSpecAfterAll()
"The transformation sample" must {
"illustrate how to start first frontend" in {
runOn(frontend1) {

View file

@ -330,14 +330,14 @@ object AkkaBuild extends Build {
lazy val clusterSample = Project(
id = "akka-sample-cluster-experimental",
base = file("akka-samples/akka-sample-cluster"),
dependencies = Seq(cluster, remoteTests % "compile;test->test;multi-jvm->multi-jvm", testkit % "test->test"),
dependencies = Seq(cluster, remoteTests % "test", testkit % "test"),
settings = defaultSettings ++ multiJvmSettings ++ Seq(
libraryDependencies ++= Dependencies.clusterSample,
// disable parallel tests
parallelExecution in Test := false,
extraOptions in MultiJvm <<= (sourceDirectory in MultiJvm) { src =>
(name: String) => (src ** (name + ".conf")).get.headOption.map("-Dakka.config=" + _.absolutePath).toSeq
},
scalatestOptions in MultiJvm := defaultMultiJvmScalatestOptions,
jvmOptions in MultiJvm := defaultMultiJvmOptions,
publishArtifact in Compile := false
)
@ -594,6 +594,8 @@ object Dependencies {
val docs = Seq(Test.scalatest, Test.junit, Test.junitIntf)
val zeroMQ = Seq(protobuf, zeroMQClient, Test.scalatest, Test.junit)
val clusterSample = Seq(Test.scalatest)
}
object Dependency {