This article corresponds to chapter 3 of Akka in Action.
TDD, short for Test-driven Development, is a core practice and technology in agile Development, as well as a design methodology. TDD works by writing unit test case code before developing functional code, then writing code to get those use cases passed, and then adding functionality in a loop until all functionality is developed. This agile style of development has many advantages when developing lightweight projects:
- The system can be shipped with a detailed test set.
- Developers can clearly recognize that a phase is over.
- Most of the time the code is in a high quality state.
In Akka, actors are built on top of messages, so it’s very easy to test. Developers just need to send messages to simulate behavior, which is very much in line with TDD thinking.
In addition to TDD, we can extend our thinking to BDD (Behavior-Driven Development) : It drives software Development by writing behaviors and specifications. The root of BDD is a common language that is semantically simple and easy to understand, or a DSL. Teams using BDD can provide extensive functional documentation in the form of user stories with only test cases. The following code snippets are taken from this chapter. Each use case uses text and keywords such as must and should to build specifications. This is very easy to interpret, so it allows non-technical people, customers, to participate in the validation and acceptance of requirements.
"A Echo Actor" must {
"Reply with the same message after sending the message to it" in {
// TODO implementation}}Copy the code
We used scalaTest as the unit testing framework, which is an XUnit-style testing framework. The above code block is written in WordSpec. For more details, see ScalaTest.
Testing for actors is more difficult than normal code because:
- Timing: Messages are sent asynchronously and it is difficult to know when to assert expectations in unit tests.
- Asynchronicity: Actors execute in parallel on multiple threads. Multithreaded tests are harder to verify than single-threaded tests and require concurrent primitives such as locks, latches, and barriers. In Akka, this is something we want to avoid.
- Statelessness: In testing, we want to be able to access the state of actors, but the design of actors prevents that.
- Collaboration/Integration: If you are testing Integration against several actors, you need to eavesdrop on the Actor and assert that it is the same as expected.
As you can see, you can’t directly test Akka projects using ScalaTest alone, but Akka itself provides the Akka-TestKit module. This module provides several testing tools that make Actor testing much easier.
- Single-threaded unit tests – Test tools are provided
TestActorRef
To allow access to the underlying Actor state under single-threaded testing, see SilentActor below. - Multithreaded unit testing – testing tools are provided
TestKit
和TestProbe
To receive responses from other actors, examine messages, and set the time when a particular message arrives.Actors run in a multithreaded environment through regular message distributors. - Multi-jvm testing – This is very useful for testing remote JVMS, as we will cover in future distributed applications.
Test Driven development vs. Behavior Driven Development -CSDN Blog
Based on the SBT build, save all the test cases in the test/scala/aia testdriven. To avoid unnecessary duplication, declare a feature that automatically exits the system at the end of the test.
import akka.testkit.TestKit
import org.scalatest.{BeforeAndAfterAll.Suite}
Only TestKit with Suite can use it.
trait StopSystemAfterAll extends BeforeAndAfterAll{
this : TestKit with Suite= >override protected def afterAll() :Unit = {
super.afterAll()
system.terminate()
}
}
Copy the code
You can test all use cases in the test/ directory at once by typing the test command into the SBT shell, or you can test specific use cases using testOnly
. The following is a common structure for the various test cases:
// Pass a test ActorSystem to TestKit (${name})
class FilteringActor01Test extends TestKit(ActorSystem("testSystem"))
with WordSpecLike // Write use cases in WordSpec style
with MustMatchers / / must assert
with StopSystemAfterAll { // Exit the AKka system after the test
"An Actor" must {
"finish the goal-1" in {
fail("not implemented yet")}"finish the goal-2" in {
fail("not implemented yet")}}}Copy the code
We refined the test code step-by-step in a red-green-refactor style:
- First make sure the test fails before the test is implemented (red)
- Improve code to pass test cases (green)
- Refactor the code to make it look good.
Code Refactoring Guide for Engineers – Zhihu (zhihu.com)
A one-way message
A standard testing procedure in send-and-discard messaging is:
- Send a message.
- Verify that the Actor has done its job in the appropriate time slice.
In this process, we don’t pay attention to how the message is delivered or who is delivering it. Not all actors will return a message to the sender() after completing the task, for example some actors will only “quietly” change their internal state after receiving the message. To recap, there are three variants of actors:
- SilentActor: Does not reply directly to the message, but changes the state inside the Actor itself.
- SendingActor: Sends the message to other actors (s) after receiving the message and completing the task.
- SideEffectingActor: Produces some side effect (log, console printing, database interaction, etc.).
SilentActor
The first simple test case starts with the SilentActor: it unpacks the SilentMessage from the outside world and adds the data to the internal internalState property.
class SilentActor extends Actor {
// The internal state of SilentActor.
private var internalState: Vector[String] = Vector[String] ()override def receive: Receive = {
// For single-threaded tests
case SilentMessage(data) => internalState = internalState :+ data
// for multithreading tests
case GetState(ref) => ref ! this.internalState
}
// Return status to the outside world
def state: Vector[String] = internalState
}
object SilentActor {
case class SilentMessage(str: String)
case class GetState(ref: ActorRef)
}
Copy the code
Since this type of Actor does not send messages back, other means are needed to observe its internal details. The first test is performed in a single-threaded environment, where TestActorRef can be used to create ActorRef instead of ActorSystem. The TestActorRef created from it exposes the inner details of an Actor by calling underlyingActor.
"A Silent Actor" must{
"change state when it receives a message, single-thread." in {
import aia.driven.SilentActor. _val silentActor: TestActorRef[SilentActor] = TestActorRef[SilentActor]
silentActor ! SilentMessage("whisper")
silentActor.underlyingActor.state must contain ("whisper")}}Copy the code
Note that actors are protected from multithreaded access and only handle one message at a time, so setting/changing internalState is thread-safe. The internal state is best replaced by a combination of var + immutable data structures instead of the value val (value) + mutable data structures. This prevents actors from accidentally sharing mutable state even when they share their internal state with other actors through the GET method.
The second test was run in a multithreaded environment (the Akka code we normally write). Another Actor must be introduced to receive the state of the SilentActor, using testActor provided by TestKit.
// "A Silent Actor" must ...
"change state when it receives a message, multi-thread." in {
import aia.driven.SilentActor. _// System refers to the ActorSystem passed in TestKit ("testSystem")
val silentActor: ActorRef = system.actorOf(Props[SilentActor])
silentActor ! SilentMessage("whisper1")
silentActor ! SilentMessage("whisper2")
// testActor is provided by TestKit.
silentActor ! GetState(testActor)
expectMsg(Vector("whisper1"."whisper2"))}Copy the code
Notice that it is called directly in the test code! To send a message, the default sender is noSender (which points to NULL in the source code). SilentActor cannot find the sender of such a message through sender(), and the message will be sent back as a dead letter. This results in test cases failing to capture messages, so testActor is indispensable for now. However, I’ll show you how to bind testActor to the test code itself using the ImplicitSender attribute later.
To avoid the dead-letter problem, the test code gets the internal state of a SilentActor by passing a testActor reference along with the message, and the SilentActor returns the internal state to testActor through the state method. Later, the test code can verify the messages received by testActor using methods such as expectMsg or expectMsgPF. The wait is synchronous; if the expectMsg method asserts success, the test case passes; otherwise, it fails.
The timeout setting has a default value of 3s, which can be configured by akka.test.single-expect-default, and the dilation factor can be used to accommodate different performance machines in multiple environments, such as low configuration machines, Wait times are also longer.
SendingActor
SendingActor is a more common type of Actor that holds another ActorRef through the props method to maintain two-way message interaction (the two actors are flat between each other). Note that the Props (…). The ActorRef () method returns a configuration (property) that creates an ActorRef. It is recommended that props be declared in an Actor companion object, effectively avoiding access to the internal state of the Actor. If properties inside the Actor are used when creating Props, a data race is caused. See: factory method – CSDN for providing Props inside an Actor associated object
The Props argument is lazy-loaded, which means that the associated ActorRef will only be created when the Akka system needs it. For lazy loading in Scala, see exploring Scala’s Non-strict evaluation and Streaming Data Structures — Nuggets (juejin. Cn).
The following SendingActor receives a list of chaotic sequences and passes back its ordered arrangement:
class SendingActor(receiver: ActorRef) extends Actor {
override def receive: Receive = {
case SortEvents(unsorted) =>receiver ! SortedEvents(unsorted.sortBy(_.id)); }}object SendingActor {
def props(ref: ActorRef) = Props(new SendingActor(ref))
case class Event(id: Long)
case class SortEvents(unsorted: Vector[Event])
case class SortedEvents(sortedEvents: Vector[Event])
}
Copy the code
The logic is not hard to understand: send a Vector[Event] out of order and assert that the result will be an ordered Vector[Event]. This time use expectMsgPF to receive a partial function: it can receive multiple message types and make assertions. Note that the assertion here uses be(…) Method, which follows must, is equivalent to mustEqual.
"A Sending Actor " must {
"send a message to another actor when it's finished processing." in {
val props: Props = SendingActor.props(testActor)
val sendingActor: ActorRef = system.actorOf(props, "sendActor")
val size = 1000
val maxInclusive = 10000
def randomEvents() :Vector[Event] = {
for{_ < -0 until size} yield Event(Random.nextInt(maxInclusive))
}.toVector
// ---- create the Event vector with out-of-order id ---- //
val unsorted: Vector[Event] = randomEvents()
val sortEvents: SortEvents = SortEvents(unsorted)
sendingActor ! sortEvents
expectMsgPF() {
case SortedEvents(events) =>
// must( events.size == size)
events.size must be(size)
// must(unsorted.sortBy(_.id) == events)
unsorted.sortBy(_.id) must be(events)
}
}
}
Copy the code
SendingActor is a ubiquitous Actor type in the ubiquitous Akka System, so much so that it can be subdivided into some variations:
Actor | description |
---|---|
MutatingCopyActor | The Actor receives the message and passes the modified copy to the next Actor. |
ForwardActor | An Actor that simply forwards messages. |
TransformingActor | An Actor that converts a message to another type. |
FilteringActor | Select the Actor that receives the message based on the Settings. |
SequencingActor | An Actor that creates and sends multiple messages based on the message received. |
This example belongs to a MutatingCopyActor. Both ForwardActor and TransformingActor can be tested in a similar way. FilteringActor is slightly different because the test case needs to continuously send and receive messages to it. Here’s an example: This FilteringActor filters out duplicate message ids with a buffer and returns messages to testActor only if it receives a non-duplicate message.
class FilterActor(nexus : ActorRef,bufferSize : Int) extends Actor {
private var buffer : Vector[String] = Vector[String] ()override def receive: Receive = {
case UniqueEvent(id) =>
if(! buffer.contains(id)){ buffer = buffer :+ id nexus ! idif(buffer.size > bufferSize) {buffer = buffer.tail}
}
}
}
object FilterActor {
def props(nexus : ActorRef,bufferSize : Int) :Props = Props(new FilterActor(nexus, bufferSize))
case class UniqueEvent(id : String)
}
Copy the code
During the test, the valid message ID ranges from 1 to 5, but the number of messages is greater than 5. The test expects the filtered message ID to be unique. If the ids are still remembered in increasing order, the List(“1”, “2”, “3”, “4”, “5”) should be the result of the final.tolist processing.
The current test case uses the receiveWhile method to fetch consecutive messages received by testActor and then asserts when a message with ID 6 is received.
"A Filtering Actor" must {
Get List(1,2,3,4,5) after sending the sequential and repetitive MSG in {
val ref: ActorRef = system.actorOf(FilterActor.props(testActor, 5))
val idList = List(1.2.3.3.2.1.4.5.6)
for{i <- idList}{ref ! UniqueEvent(s"${i}")}
// Used to receive multiple messages. It ends at 6 o 'clock
val gets : List[String] = receiveWhile(){
case id : String if id.toInt <= 5 => id
}.toList
gets must be(List("1"."2"."3"."4"."5"))}}Copy the code
The receiveWhile method can also be used to test SequencingActor, which can assert that a series of messages triggered by an activity are expected.
Two more methods are available: ignoreMsg and expectNoMsg. IgnoreMsg accepts a partial function of type PartionalFunction[Any,Boolean] declared before receiveWhile allowing testActor to further filter out specific information while receiving the message.
// PF receives [Any,Boolean] messages that return true are ignored.
ReceiveWhile () before calling.
ignoreMsg {
case id : String if id.toInt == 3= >true
case_ = >false
}
// Used to receive multiple messages.
val gets : List[String] = receiveWhile(){ case id : String if id.toInt <= 5 => id}.toList
// Message "3" ignored by testActor.
gets must be(List("1"."2"."4"."5"))
Copy the code
ExpectNoMsg says that testActor should not receive any messages during the set interval, otherwise the assertion will fail.
// Set to ignore all messages.
ignoreMsg { case_ = >true }
// Start receiving messages
receiveWhile(){ case id : String if id.toInt <= 5 => id}.toList
// No message arrived.
expectNoMsg()
Copy the code
Sometimes we need an Actor that can simultaneously send and receive messages in groups, in which case using TestProbe() is more convenient. Instead of binding testActor for every tested Actor, the Sender () of EchoActor always points to TestProbe’s ActorRef.
class EchoActor extends Actor {
override def receive: Receive = {
case msg => sender() ! msg
}
}
val probe: TestProbe = TestProbe(a)val probeRef: ActorRef = probe.ref
val rec1: ActorRef = system.actorOf(Props[EchoActor]."echo-actor-01")
val rec2: ActorRef = system.actorOf(Props[EchoActor]."echo-actor-02")
probeRef.tell("Hello",rec1)
probeRef.tell("World",rec2)
probe.receiveN(2).toList must be(List("Hello"."World"))
Copy the code
Akka uses Series 2: Test – Pekkle – Blogpark (CNblogs.com)
SideEffectingActor
Here is a demonstration of the SideEffectingActor: it simply logs messages to the log (ActorLogging is introduced here).
// Extend the logging function
class Greeter extends Actor with ActorLogging {
override def receive: Receive = {case Greeting(msg) => log.info("Hello {}",msg)}
}
object Greeter {case class Greeting(message : String)}
Copy the code
In order to develop logging, need from one contains event listener akka. The testkit. TestEventListener configuration created in the system.
// ActorSystem(SideEffecting)
object SideEffectActor01Test {
// Create a system from a configuration that contains test event listeners
val testSystem: ActorSystem = {
val config: Config = ConfigFactory.parseString(
""" |akka.loggers = [akka.testkit.TestEventListener] |""".stripMargin
)
ActorSystem("testSystem",config)
}
}
Copy the code
This example is being tested in a single thread, so you might want to bind its configuration Props to the specified dispenser of. The EventFilter object can be used to detect and filter log information. The following code block requires the log to be recorded twice: Hello World! Information. The test code is written inside the Intercept closure.
"The Greeter" must {
"say Hello World! when a Greeting(\"World! \")is sent to it" in {
// Specify the message dispenser
val dispatcherId: String = CallingThreadDispatcher.Id
val greeter: Props = Props(new Greeter()).withDispatcher(dispatcherId)
// system is the testSystem passed in
val greetRef: ActorRef = system.actorOf(greeter)
// Listen for events.
/ / log information by akka. Testkit. TestEventListener acquisition.
// The test fails if the number of occurrences is not met.
EventFilter.info(message = "Hello World!",occurrences = 2).intercept {
greetRef ! Greeting("World!")
greetRef ! Greeting("World!")}}}Copy the code
A convenient form of two-way messaging
The example of bidirectional message sending was introduced in SendingActor type message testing. The usual method is to pass ActorRef when creating Actor Props. In this test, the ImplicitSender attribute is used. This property replaces the implicit message sender with a testActor reference.
class EchoActorTest extends TestKit(ActorSystem("testSystem"))
with WordSpecLike
with MustMatchers
with ImplicitSender
with StopSystemAfterAll {
"A Echo Actor" must {
"Reply with the same message after sending the message to it" in {
val ref: ActorRef = system.actorOf(Props[EchoActor])
ref ! "Hello?"
expectMsg("Hello?")}}}class EchoActor extends Actor { override def receive: Receive = { case msg => sender() ! msg } }
Copy the code
EchoActor still does simple postback of messages. The difference is that instead of binding a listener ActorRef through the constructor, it just needs the sender() to send the message to the test code.