├── .gitignore ├── .travis.yml ├── README.md ├── build.sbt ├── project ├── build.properties └── plugins.sbt └── src ├── main ├── protobuf │ └── models.proto ├── resources │ └── application.conf └── scala │ └── com │ └── experiments │ └── calculator │ ├── Calculator.scala │ ├── Main.scala │ ├── PersistenceQueryExample.scala │ ├── models │ └── Models.scala │ └── serialization │ └── CalculatorEventProtoBufSerializer.scala └── test ├── resources └── application.conf └── scala └── com └── experiments ├── PersistenceCleanup.scala ├── PersistenceSpec.scala └── calculator └── PersistentCalculatorSpec.scala /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | *.log 3 | 4 | # sbt specific 5 | dist/* 6 | target/ 7 | lib_managed/ 8 | src_managed/ 9 | project/boot/ 10 | project/plugins/project/ 11 | 12 | # IDE specific 13 | .scala_dependencies 14 | .classpath 15 | .idea/ 16 | .idea_modules/ 17 | .project 18 | .settings/ 19 | *.sublime-project 20 | *.sublime-workspace 21 | /.env 22 | atlassian-ide-plugin.xml 23 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: scala 2 | jdk: oraclejdk8 3 | scala: 4 | - 2.11.8 5 | sudo: false 6 | cache: 7 | directories: 8 | - $HOME/.ivy2/cache 9 | - $HOME/.sbt/boot/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Akka Persistence example with Protocol Buffers serialization 2 | 3 | ### Build Status 4 | [![Build Status](https://travis-ci.org/calvinlfer/play-framework-validation-example.svg?branch=master)](https://travis-ci.org/calvinlfer/play-framework-validation-example) 5 | 6 | ### Overview 7 | This example uses [ScalaPB](https://trueaccord.github.io/ScalaPB/) in order to obtain case class support for Scala when 8 | generating code from `*.proto` files. ScalaPB uses the `protoc-jar` internally to compile your protocol buffer files. 9 | 10 | Execute `sbt run` in order to run the `Main` application. The `Calculator` actor is used to persist events across runs 11 | using Event Sourcing. We define a custom serializer that makes use of the generated class' (from proto) 12 | serialization mechanisms to easily serialize and deserialize events. 13 | 14 | The `Main` application fires events at the `Calculator` actor. Since `Calculator` actor is Persistent, it makes use of 15 | the Serializer (set up in `application.conf`) when reading and persistent events to/from the event journal. 16 | 17 | We also have a [test](https://github.com/referentiallytransparent/Akka-Persistence-example-with-Protocol-Buffers-serialization/blob/master/src/test/scala/com/experiments/calculator/PersistentCalculatorSpec.scala) that exercises the Persistent `Calculator` Actor by sending it events, killing it and making sure it uses Event Sourcing to bring the `Calculator` up to the current state. You can use `sbt test` to run the test. 18 | 19 | If you are using IntelliJ and want syntax highlighting, then make sure your directory setup looks like this: 20 | ![image](https://cloud.githubusercontent.com/assets/14280155/14578746/e1a8e258-035e-11e6-86af-5a74669930d5.png) 21 | 22 | ### External Libraries ### 23 | - [ScalaPB](https://trueaccord.github.io/ScalaPB/) is used to generate `Scala case classes` from `proto` files. These case classes have the ability to convert to and from binary 24 | - [Akka](http://akka.io/) for its actor framework, persistence module and test kit 25 | 26 | **Warning: Make sure you have Python 2.7 installed (accessible via `python`)** otherwise the protocol buffers compiler will give you errors and will not compile your project 27 | -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | import com.trueaccord.scalapb.compiler.Version.scalapbVersion 2 | 3 | name := "akka-persistence-example-with-protocol-buffers" 4 | version := "1.1" 5 | scalaVersion := "2.12.1" 6 | 7 | // Disable parallel execution of tests 8 | parallelExecution in Test := false 9 | 10 | // Fork tests in case native LevelDB database is used 11 | fork := true 12 | 13 | libraryDependencies ++= { 14 | val akkaVersion = "2.4.16" 15 | Seq( 16 | // Akka 17 | "com.typesafe.akka" %% "akka-actor" % akkaVersion, 18 | "com.typesafe.akka" %% "akka-persistence" % akkaVersion, 19 | "com.typesafe.akka" %% "akka-testkit" % akkaVersion % "test", 20 | "com.typesafe.akka" %% "akka-slf4j" % akkaVersion, 21 | "com.typesafe.akka" %% "akka-persistence-query-experimental" % akkaVersion, 22 | 23 | // Local LevelDB journal (Akka Persistence) 24 | // http://doc.akka.io/docs/akka/current/scala/persistence.html#Local_LevelDB_journal 25 | "org.iq80.leveldb" % "leveldb" % "0.9", 26 | "org.fusesource.leveldbjni" % "leveldbjni-all" % "1.8", 27 | 28 | // Commons IO is needed for cleaning up data when testing persistent actors 29 | "commons-io" % "commons-io" % "2.4", 30 | "ch.qos.logback" % "logback-classic" % "1.1.3", 31 | "org.scalatest" %% "scalatest" % "3.0.1" % "test", 32 | 33 | // allows ScalaPB proto customizations (scalapb/scalapb.proto) 34 | "com.trueaccord.scalapb" %% "scalapb-runtime" % scalapbVersion % "protobuf" 35 | ) 36 | } 37 | 38 | PB.targets in Compile := Seq( 39 | scalapb.gen() -> (sourceManaged in Compile).value 40 | ) 41 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version = 0.13.13 -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | logLevel := Level.Warn 2 | 3 | // Informative Scala compiler errors 4 | addSbtPlugin("com.softwaremill.clippy" % "plugin-sbt" % "0.4.1") 5 | 6 | // Scala Protocol Buffers Compiler 7 | addSbtPlugin("com.thesamet" % "sbt-protoc" % "0.99.3") 8 | 9 | libraryDependencies += "com.trueaccord.scalapb" %% "compilerplugin" % "0.5.47" 10 | -------------------------------------------------------------------------------- /src/main/protobuf/models.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | // Brought in from scalapb-runtime 4 | import "scalapb/scalapb.proto"; 5 | import "google/protobuf/wrappers.proto"; 6 | 7 | package com.experiments.calculator; 8 | 9 | message Added { 10 | option (scalapb.message).extends = "com.experiments.calculator.models.Models.Event"; 11 | double value = 1; 12 | } 13 | 14 | message Subtracted { 15 | option (scalapb.message).extends = "com.experiments.calculator.models.Models.Event"; 16 | double value = 1; 17 | } 18 | 19 | message Multiplied { 20 | option (scalapb.message).extends = "com.experiments.calculator.models.Models.Event"; 21 | double value = 1; 22 | } 23 | 24 | message Divided { 25 | option (scalapb.message).extends = "com.experiments.calculator.models.Models.Event"; 26 | double value = 1; 27 | } 28 | 29 | message Reset { 30 | option (scalapb.message).extends = "com.experiments.calculator.models.Models.Event"; 31 | } 32 | 33 | // note that models comes from the name of this proto file 34 | // creates com.experiments.calculator.models.CalculatorModel.Event 35 | // check the target folder to see the code -------------------------------------------------------------------------------- /src/main/resources/application.conf: -------------------------------------------------------------------------------- 1 | akka { 2 | loglevel = INFO 3 | stdout-loglevel = INFO 4 | event-handlers = ["akka.event.Logging$DefaultLogger"] 5 | 6 | // configure Akka persistence to use the LevelDB journal (local machine persistence) 7 | persistence { 8 | journal { 9 | plugin = "akka.persistence.journal.leveldb" 10 | // Place persisted events into the targets/journal folder 11 | leveldb { 12 | dir = "target/journal" 13 | native = false 14 | } 15 | } 16 | snapshot-store { 17 | plugin = "akka.persistence.snapshot-store.local" 18 | local { 19 | dir = "target/snapshots" 20 | } 21 | } 22 | } 23 | 24 | actor { 25 | serializers { 26 | calculatorEvent = "com.experiments.calculator.serialization.CalculatorEventProtoBufSerializer" 27 | } 28 | 29 | serialization-bindings { 30 | "com.experiments.calculator.models.Added" = calculatorEvent 31 | "com.experiments.calculator.models.Subtracted" = calculatorEvent 32 | "com.experiments.calculator.models.Multiplied" = calculatorEvent 33 | "com.experiments.calculator.models.Divided" = calculatorEvent 34 | "com.experiments.calculator.models.Reset" = calculatorEvent 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /src/main/scala/com/experiments/calculator/Calculator.scala: -------------------------------------------------------------------------------- 1 | package com.experiments.calculator 2 | 3 | import akka.actor.ActorLogging 4 | import akka.persistence.{PersistentActor, RecoveryCompleted, SnapshotOffer} 5 | import com.experiments.calculator.models.Models._ 6 | import com.experiments.calculator.models._ 7 | 8 | /** 9 | * The persistent event-sourced calculator actor 10 | * The calculator actor receives commands, converts them 11 | * to events and persists them before dealing with them 12 | */ 13 | class Calculator extends PersistentActor with ActorLogging { 14 | 15 | // Persistence ID must be unique for persistent actors as Akka Persistence uses this ID to recover state 16 | override def persistenceId: String = "calculator-persistent-actor" 17 | 18 | // Internal state of the actor 19 | var state = CalculationResult(result = 0) 20 | 21 | // General updateState method for re-use 22 | // Used by both receiveRecover and receiveCommand 23 | // This updates the internal state using an event 24 | val updateState: Event => Unit = { 25 | case Reset() => state = state.reset 26 | case Added(value) => state = state.add(value) 27 | case Subtracted(value) => state = state.subtract(value) 28 | case Divided(value) => state = state.divide(value) 29 | case Multiplied(value) => state = state.multiply(value) 30 | } 31 | 32 | // Events come here (recovery phase) from database (snapshot and event) 33 | override def receiveRecover: Receive = { 34 | // comes from the event database journal 35 | case event: Event => updateState(event) 36 | 37 | // comes from the snapshot journal 38 | case SnapshotOffer(metadata, resetEvent: Reset) => updateState(resetEvent) 39 | 40 | // this message is sent once recovery has completed 41 | case RecoveryCompleted => log.info(s"Recovery has completed for $persistenceId") 42 | } 43 | 44 | // Commands come here (active phase) 45 | override def receiveCommand: Receive = { 46 | // Add this number to the result 47 | case Add(value) => 48 | // Convert Command to Event 49 | // We generate an Added Event (as in `I have added this`) 50 | val event = Added(value) 51 | // Persist the event to the database and then call update state with the persisted event 52 | persist(event)(updateState) 53 | 54 | case Subtract(value) => persist(Subtracted(value))(updateState) 55 | 56 | // We see validation in the case of division 57 | case Divide(value) => 58 | if (value > 0) persist(Divided(value))(updateState) 59 | else log.error("Cannot divide by 0, Ignoring command") 60 | 61 | case Multiply(value) => persist(Multiplied(value))(updateState) 62 | 63 | case PrintResult => println(s"the result is: ${state.result}") 64 | 65 | case GetResult => sender() ! state.result 66 | 67 | case Clear => 68 | persist(Reset())(updateState) 69 | // tell the snapshot database to persist a Reset event, we would usually tell it to persist our internal state 70 | // but I'm lazy and I don't want to make our internal state (CalculationResult) a part of the proto because then 71 | // I'd have to serialize that 72 | saveSnapshot(Reset()) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/main/scala/com/experiments/calculator/Main.scala: -------------------------------------------------------------------------------- 1 | package com.experiments.calculator 2 | 3 | import akka.actor.{ActorSystem, Props} 4 | import com.experiments.calculator.models.Models._ 5 | 6 | import scala.language.postfixOps 7 | 8 | /** 9 | * Created on 2016-02-16. 10 | */ 11 | object Main extends App { 12 | val system = ActorSystem("calculator-actor-system") 13 | val persistentCalculatorActor = system.actorOf(Props[Calculator]) 14 | 15 | // Send messages 16 | persistentCalculatorActor ! PrintResult 17 | persistentCalculatorActor ! Add(2) 18 | persistentCalculatorActor ! Multiply(2) 19 | persistentCalculatorActor ! PrintResult 20 | 21 | /* 22 | // Simple example of using the built in serializer as Multiplied is a generated class 23 | println { 24 | "The result is going to be!!!! " + 25 | // deserialize 26 | Multiplied.parseFrom { 27 | // serialize 28 | Multiplied(1).toByteArray 29 | } 30 | } 31 | */ 32 | 33 | // Wait for 2 seconds before terminating 34 | // Just an example, you wouldn't do this normally 35 | Thread sleep 2000 36 | system terminate 37 | } 38 | -------------------------------------------------------------------------------- /src/main/scala/com/experiments/calculator/PersistenceQueryExample.scala: -------------------------------------------------------------------------------- 1 | package com.experiments.calculator 2 | 3 | import akka.Done 4 | import akka.actor.ActorSystem 5 | import akka.persistence.query.PersistenceQuery 6 | import akka.persistence.query.journal.leveldb.scaladsl.LeveldbReadJournal 7 | import akka.stream.ActorMaterializer 8 | import akka.stream.scaladsl.Sink 9 | import com.experiments.calculator.models._ 10 | 11 | import scala.concurrent.{ExecutionContext, Future} 12 | import scala.language.postfixOps 13 | import scala.util.{Failure, Success} 14 | 15 | /** 16 | * A simple example that uses Persistence Query to stream out the currents events from all the current persistent 17 | * actors (in our case, it's calculator-persistent-actor) from the LevelDB journal 18 | */ 19 | object PersistenceQueryExample extends App { 20 | implicit val system = ActorSystem("calculator-actor-system") 21 | implicit val materializer = ActorMaterializer() 22 | implicit val ec: ExecutionContext = system.dispatcher 23 | 24 | // Configure Persistence Query 25 | val queries = PersistenceQuery(system).readJournalFor[LeveldbReadJournal](LeveldbReadJournal.Identifier) 26 | 27 | val nonInvasiveLog = (x: String) => { 28 | println(x) 29 | x 30 | } 31 | 32 | // Grab a list of all the persistenceIds as of this moment 33 | val events = queries.currentPersistenceIds() 34 | .map(nonInvasiveLog) 35 | // currentEventsByPersistenceId will grab all the events as of this moment for a supplied persistence id 36 | .flatMapConcat(eachPersistentId => queries.currentEventsByPersistenceId(eachPersistentId)) 37 | .map(eventEnvelope => eventEnvelope.event) 38 | .map({ 39 | case Added(value) => s"Added $value" 40 | case Subtracted(value) => s"Subtracted $value" 41 | case Divided(value) => s"Divided $value" 42 | case Multiplied(value) => s"Multiplied $value" 43 | case Reset() => s"Value reset" 44 | }) 45 | 46 | val matValue: Future[Done] = events.runWith(Sink.foreach(println)) 47 | 48 | matValue.onComplete { 49 | case Success(done) => 50 | Console println "Query completed successfully" 51 | system terminate() 52 | 53 | case Failure(e) => 54 | Console println e 55 | system terminate() 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/main/scala/com/experiments/calculator/models/Models.scala: -------------------------------------------------------------------------------- 1 | package com.experiments.calculator.models 2 | 3 | object Models { 4 | 5 | // Commands: Do this action (potentially harmful) 6 | sealed trait Command 7 | case object Clear extends Command 8 | case class Add(value: Double) extends Command 9 | case class Subtract(value: Double) extends Command 10 | case class Divide(value: Double) extends Command 11 | case class Multiply(value: Double) extends Command 12 | case object PrintResult extends Command 13 | case object GetResult extends Command 14 | 15 | // Marker trait for subclasses generated by Protobuf 16 | // The rest of the events are present in target/scala-2.*/src_managed/... 17 | // The Events are generated by ScalaPB from the models.proto file 18 | // Events: I have done this action (not harmful) 19 | trait Event 20 | 21 | // Internal state for the calculator actor 22 | case class CalculationResult(result: Double = 0) { 23 | def reset = copy(result = 0) 24 | def add(value: Double) = copy(result = result + value) 25 | def subtract(value: Double) = copy(result = result - value) 26 | def multiply(value: Double) = copy(result = result * value) 27 | def divide(value: Double) = copy(result = result / value) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/scala/com/experiments/calculator/serialization/CalculatorEventProtoBufSerializer.scala: -------------------------------------------------------------------------------- 1 | package com.experiments.calculator.serialization 2 | 3 | import akka.serialization.SerializerWithStringManifest 4 | import com.experiments.calculator.models._ 5 | 6 | /** 7 | * This class is responsible for serializing Calculator Events before they make their way to the event journal 8 | * or snapshot journal because we don't want to use Java Serialization. Thanks to ScalaPB generating our classes from 9 | * protos, we can easily convert to and from the binary format that we use in the journals. 10 | * 11 | * ________________ 12 | * When asked to persist an event | | 13 | * Calculator Event (Multiplied(2)) -> Serializer -> Serialized form of Multiplied(2) -> | Journal | 14 | * | | 15 | * When asked to recover | | 16 | * Calculator Event (Multiplied(2)) <- Deserializer <- Serialized form of Multiplied(2) <- | | 17 | * ------------------ 18 | * 19 | * Based off http://doc.akka.io/docs/akka/2.4.4/scala/persistence-schema-evolution.html 20 | */ 21 | class CalculatorEventProtoBufSerializer extends SerializerWithStringManifest { 22 | override def identifier: Int = 9001 23 | 24 | // Event <- **Deserializer** <- Serialized(Event) <- Journal 25 | override def fromBinary(bytes: Array[Byte], manifest: String): AnyRef = 26 | manifest match { 27 | case AddedManifest => Added.parseFrom(bytes) 28 | case SubtractedManifest => Subtracted.parseFrom(bytes) 29 | case MultipliedManifest => Multiplied.parseFrom(bytes) 30 | case DividedManifest => Divided.parseFrom(bytes) 31 | case ResetManifest => Reset.parseFrom(bytes) 32 | } 33 | 34 | // We use the manifest to determine the event (it is called for us during serializing) 35 | // Akka will call manifest and attach it to the message in the event journal/snapshot database 36 | // when toBinary is being invoked 37 | override def manifest(o: AnyRef): String = o.getClass.getName 38 | final val AddedManifest = classOf[Added].getName 39 | final val SubtractedManifest = classOf[Subtracted].getName 40 | final val MultipliedManifest = classOf[Multiplied].getName 41 | final val DividedManifest = classOf[Divided].getName 42 | final val ResetManifest = classOf[Reset].getName 43 | 44 | // Event -> **Serializer** -> Serialized(Event) -> Journal 45 | override def toBinary(o: AnyRef): Array[Byte] = { 46 | o match { 47 | case a: Added => a.toByteArray 48 | case s: Subtracted => s.toByteArray 49 | case m: Multiplied => m.toByteArray 50 | case d: Divided => d.toByteArray 51 | case r: Reset => r.toByteArray 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/test/resources/application.conf: -------------------------------------------------------------------------------- 1 | akka { 2 | loglevel = INFO 3 | stdout-loglevel = INFO 4 | event-handlers = ["akka.event.Logging$DefaultLogger"] 5 | 6 | // configure Akka persistence to use the LevelDB journal (local machine persistence) 7 | persistence { 8 | journal { 9 | plugin = "akka.persistence.journal.leveldb" 10 | // Place persisted events into the targets/journal folder 11 | leveldb { 12 | dir = "target/journal" 13 | native = false 14 | } 15 | } 16 | snapshot-store { 17 | plugin = "akka.persistence.snapshot-store.local" 18 | local { 19 | dir = "target/snapshots" 20 | } 21 | } 22 | } 23 | 24 | actor { 25 | serializers { 26 | calculatorEvent = "com.experiments.calculator.serialization.CalculatorEventProtoBufSerializer" 27 | } 28 | 29 | serialization-bindings { 30 | "com.experiments.calculator.models.Added" = calculatorEvent 31 | "com.experiments.calculator.models.Subtracted" = calculatorEvent 32 | "com.experiments.calculator.models.Multiplied" = calculatorEvent 33 | "com.experiments.calculator.models.Divided" = calculatorEvent 34 | "com.experiments.calculator.models.Reset" = calculatorEvent 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /src/test/scala/com/experiments/PersistenceCleanup.scala: -------------------------------------------------------------------------------- 1 | package com.experiments 2 | 3 | import java.io.File 4 | 5 | import akka.actor.ActorSystem 6 | import org.apache.commons.io.FileUtils 7 | 8 | import scala.util.Try 9 | 10 | /** 11 | * The PersistenceCleanup trait defines a deleteStorageLocations method that removes directories created by the 12 | * leveldb journal (as well as the default snapshot journal). It gets the configured directories from the Akka 13 | * configuration. 14 | */ 15 | trait PersistenceCleanup { 16 | def system: ActorSystem 17 | 18 | // Obtain information about the event journal and snapshot journal from the Akka configuration 19 | val storageLocations = 20 | List( 21 | "akka.persistence.journal.leveldb.dir", 22 | "akka.persistence.journal.leveldb-shared.store.dir", 23 | "akka.persistence.snapshot-store.local.dir").map { s => 24 | new File(system.settings.config.getString(s)) 25 | } 26 | 27 | def deleteStorageLocations(): Unit = { 28 | storageLocations.foreach(dir => Try(FileUtils.deleteDirectory(dir))) 29 | } 30 | } -------------------------------------------------------------------------------- /src/test/scala/com/experiments/PersistenceSpec.scala: -------------------------------------------------------------------------------- 1 | package com.experiments 2 | 3 | import akka.actor.{ActorSystem, ActorRef} 4 | import akka.testkit.{ImplicitSender, TestKit} 5 | import com.typesafe.config.Config 6 | import org.scalatest.{WordSpecLike, Matchers, BeforeAndAfterAll} 7 | 8 | /** 9 | * The PersistenceSpec deletes any leftover directories before the unit test starts, and deletes the directories 10 | * after all specifications and shuts down the actor system used during the test. 11 | * @param system the test actor system 12 | */ 13 | abstract class PersistenceSpec(system: ActorSystem) extends TestKit(system) 14 | with ImplicitSender 15 | with WordSpecLike 16 | with Matchers 17 | with BeforeAndAfterAll 18 | with PersistenceCleanup { 19 | 20 | def this(name: String, config: Config) = this(ActorSystem(name, config)) 21 | override protected def beforeAll() = deleteStorageLocations() 22 | 23 | override protected def afterAll() = { 24 | deleteStorageLocations() 25 | TestKit.shutdownActorSystem(system) 26 | } 27 | 28 | def killActors(actors: ActorRef*) = { 29 | actors.foreach { actor => 30 | watch(actor) 31 | system.stop(actor) 32 | expectTerminated(actor) 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/test/scala/com/experiments/calculator/PersistentCalculatorSpec.scala: -------------------------------------------------------------------------------- 1 | package com.experiments.calculator 2 | 3 | import akka.actor.{ActorSystem, Props} 4 | import com.experiments.PersistenceSpec 5 | import org.scalatest.{BeforeAndAfterAll, WordSpecLike} 6 | 7 | /** 8 | * Note that we use the PersistenceSpec we defined instead of the usual TestKit which takes care of cleaning up the 9 | * left over files from previous tests 10 | */ 11 | class PersistentCalculatorSpec extends PersistenceSpec(ActorSystem("calculator-actor-test-system")) 12 | with WordSpecLike 13 | with BeforeAndAfterAll { 14 | "The Calculator" should { 15 | import com.experiments.calculator.models.Models._ 16 | "recover last known result after crash" in { 17 | val calc = system.actorOf(Props[Calculator], "test-calculator") 18 | calc ! Add(1) 19 | calc ! GetResult 20 | // Note that PersistenceSpec mixes in ImplicitSender which is why the test itself is an actor 21 | expectMsg(1) 22 | 23 | calc ! Subtract(0.5) 24 | calc ! GetResult 25 | expectMsg(0.5) 26 | 27 | killActors(calc) 28 | 29 | val calcResurrected = system.actorOf(Props[Calculator], "test-calculator") 30 | calcResurrected ! GetResult 31 | expectMsg(0.5) 32 | 33 | calcResurrected ! Add(1) 34 | calcResurrected ! GetResult 35 | expectMsg(1.5) 36 | } 37 | } 38 | } 39 | --------------------------------------------------------------------------------