diff --git a/akka-docs/cluster/cluster-usage.rst b/akka-docs/cluster/cluster-usage.rst
index b0ec8f08b7..d370f8b7ac 100644
--- a/akka-docs/cluster/cluster-usage.rst
+++ b/akka-docs/cluster/cluster-usage.rst
@@ -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 `_, 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 ` is added as :ref:`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 ` 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
diff --git a/akka-samples/akka-sample-cluster/src/multi-jvm/scala/sample/cluster/stats/StatsSampleSingleMasterSpec.scala b/akka-samples/akka-sample-cluster/src/multi-jvm/scala/sample/cluster/stats/StatsSampleSingleMasterSpec.scala
index b1d27cd7a3..9f7010f7cd 100644
--- a/akka-samples/akka-sample-cluster/src/multi-jvm/scala/sample/cluster/stats/StatsSampleSingleMasterSpec.scala
+++ b/akka-samples/akka-sample-cluster/src/multi-jvm/scala/sample/cluster/stats/StatsSampleSingleMasterSpec.scala
@@ -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])
diff --git a/akka-samples/akka-sample-cluster/src/multi-jvm/scala/sample/cluster/stats/StatsSampleSpec.scala b/akka-samples/akka-sample-cluster/src/multi-jvm/scala/sample/cluster/stats/StatsSampleSpec.scala
index 9f88597051..b1141e587f 100644
--- a/akka-samples/akka-sample-cluster/src/multi-jvm/scala/sample/cluster/stats/StatsSampleSpec.scala
+++ b/akka-samples/akka-sample-cluster/src/multi-jvm/scala/sample/cluster/stats/StatsSampleSpec.scala
@@ -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")
}
+
+
}
}
\ No newline at end of file
diff --git a/akka-samples/akka-sample-cluster/src/multi-jvm/scala/sample/cluster/transformation/TransformationSampleSpec.scala b/akka-samples/akka-sample-cluster/src/multi-jvm/scala/sample/cluster/transformation/TransformationSampleSpec.scala
index 98b4cb6526..1c3176ee16 100644
--- a/akka-samples/akka-sample-cluster/src/multi-jvm/scala/sample/cluster/transformation/TransformationSampleSpec.scala
+++ b/akka-samples/akka-sample-cluster/src/multi-jvm/scala/sample/cluster/transformation/TransformationSampleSpec.scala
@@ -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) {
diff --git a/project/AkkaBuild.scala b/project/AkkaBuild.scala
index 8972ed5cc8..9a11c119c8 100644
--- a/project/AkkaBuild.scala
+++ b/project/AkkaBuild.scala
@@ -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 {