Testing Actors

testing the actors:)

Traits and Mixins

  • We’ve seen inheritance by extending abstract class
  • What if we want to extend multiple classes?
    • Example: You want an object class that extends PhysicalObject to have physics applied and extend Actor to run concurrently
    • This is not allowed
  • Can avoid this need by using composition
    • Make a class that extends actor and stores a PhysicalObject in a state variable

  • Traits
    • Similar to abstract classes
    • Cannot have a constructor
  • No limit to the number of traits that can be extended
    1
    2
    3
    4
    5
    6
    trait Database {
    def playerExists(username: String): Boolean
    def createPlayer(username: String): Unit
    def saveGameState(username: String, gameState: String): Unit
    def loadGameState(username: String): String
    }

  • Mixins
    • When extending multiple traits, we use the term mixin(ie. The traits are mixed into the class)
  • Extend any one class / abstract class / trait using “extends”
  • Add any number of Traits using “with”
  • Must implement all abstract methods from all inheritances
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    class ActorAndDatabase extends Actor with Database {
    override def receive: Receive = {
    case_ =>
    }
    override def playerExists(username: String): Boolean = {
    false
    }
    override def createPlayer(username: String): Unit = {}
    override def saveGameState(username: String, gameState: String): Unit = {}
    override def loadGameState(username: String): String = {
    ""
    }
    }

Testing Actors

  • We’ve seen our first actor system where multiple actors can run concurrently
  • But how would we test such a program?
  • Use a FunSuite like we have all semester?
    • FunSuite starts the actor system
    • Creates actors
    • Send messages
    • Runds some asserts that likely execuye before the first message is received by an actor
  • Can wait with Thread.sleep to wait for messages, but the FunSuite can’t receive messages from the actors
    • Now way of gainning infomation from the actors

Testing Actors - Library

  • Let’s pull in a new library to help us out
  • The testing library we’ll use is the Akka testkit
  • Make sure thr version number matches your version of the Akka actor library
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
     <dependency>
    <groupId>com.typesafe.akka</groupId>
    <artifactId>akka-actor_2.12</artifactId>
    <version>2.5.25</version>
    </dependency>

    <dependency>
    <groupId>com.typesafe.akka</groupId>
    <artifactId>akka-testkit_2.12</artifactId>
    <version>2.5.25</version>
    </dependency>

Testing Actors - Setup

  • Using this library, we’ll setup a new type of TestSuite using the TestKit class
  • This setup is directly from the test kit documentation and can be reused whenever you setup a test suite for actors
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    import akka.testkit.{ImplicitSender, TestKit}
    import org.scalatest.{BeforeAndAfterAll, Matchers, WordSpecLike}

    import scala.concurrent.duration._


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

    override def afterAll: Unit = {
    TestKit.shutdownActorSystem(system)
    }

    "A string actor" must {
    "track a string" in {
    // Test actors here
    }
    }
    }

  • Import classes / traits from the libraries
  • The test kit is built on top of scalatest so we’ll use both libraries
  • Import the duration package from Scala to use 100.millis syntax
    • Important: This muest be manually added. IntelliJ will not suggest importing the duration package and you will have an error on you millis

  • Extend the TestKit class which has a constructor that takes an ActorSystem
  • Name and Create a new ActorSystem directly in the constructor

  • Mixin traits for additional functionality
  • These traits contain defined methods
    • No abstract methods that need to be implemented

  • From tje BeforeAndAfterAll trait, we inherited the afterAll method which is called after all our tests complete
  • By default, afterAll is implement but does nothing
  • We override afterAll to properly shutdown the actor system

  • Finally, we can setup our tests
  • Instead of unit testing, the test kit uses behavioral tests
    • Name tests by the expect behavior of your code

  • Methods “must” and “in” are inherited from WordSpecLike and are called inline
    • Uses a string wrapper class (ie. “A value actor” is implicitly converted to the wrapper class which contains the must method)
  • All this to achieve more human readable syntax

Testing Actors

  • With the test suite setup, we’re ready to start testing our actors
  • We have access to the actor system we created in the constructor in the system variable
    • Use this system to start your actor(s)
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      "A string actor" must {
      "track a string" in {
      val valueActor = system.actorOf(Props(classOf[StringActor], "Hello"))

      valueActor ! Append(" CSE")
      valueActor ! Append("116")

      expectNoMessage(100.millis)

      valueActor ! GetValue
      val value: Value = expectMsgType[Value](1000.millis)

      assert(value == Value("Hello CSE116"))
      }
      }

  • Send messages to the actor using the ! method
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    "A string actor" must {
    "track a string" in {
    val valueActor = system.actorOf(Props(classOf[StringActor], "Hello"))

    valueActor ! Append(" CSE")
    expectNoMessage(100.millis)
    valueActor ! Append("116")
    expectNoMessage(100.millis)

    valueActor ! GetValue
    val value: Value = expectMsgType[Value](1000.millis)

    assert(value == Value("Hello CSE116"))
    }
    }

  • New: Use the expectNoMessage method (inherited from TestKit) to wait for messages to resolve before testing
  • The test suite will wait for the specified amount of time
    • If there’s an error on millis, don’t forget to import the duration package
      1
      expectNoMessage(100.millis)
  • In this example, each expectNoMessage will wait for 100 milliseconds before moving to the next line of code
    • This allows sample time for messages to be processed in a controlled order
  • If any message is received while waiting, the test will fail

  • Send a message to an actor that expects a response
  • The test suite is part of the actor system and can receive the response for testing
    1
    valueActor ! GetValue

  • New: When you expect to receive a message, call expectMsgType with the message type you expect
  • The time provided is the maximum amount to time to wait before failing the test
    • If we don’t receive a message of type Value within 1 second, this test fails
      1
      val value: Value = expectMsgType[Value](1000.millis)

  • expectMsgType does not wait for the full duration!
  • As soon as a message is received, the test moves on
  • This test will wait for 11.5dats, or until it receives a message
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    "A string actor" must {
    "track a string" in {
    val valueActor = system.actorOf(Props(classOf[StringActor], "Hello"))

    valueActor ! Append(" CSE")
    expectNoMessage(100.millis)
    valueActor ! Append("116")
    expectNoMessage(100.millis)

    valueActor ! GetValue
    val value: Value = expectMsgType[Value](1000000000.millis)

    assert(value == Value("Hello CSE116"))
    }
    }

  • When the message is received, use asserts to test it
    1
    assert(value == Value("Hello CSE116"))

  • Caution: When testing message types with underfined variable names do not access the variables for testing
    • For today’s lecture question, the names of the Int variables are not defined and it is unlikely that we will use the same names
    • If we use different names => error in AutoLab

  • Instead, create a new message of that type and check foor equality
    • Recall that case classes have an equals method that compares the values of its variables instead of references

Lecture Question

Task:
Create and test an actor class to track funds in a bank account

  • This question is similar to Monday’s in that your actor will only track a single value. Through this question you will practice writing a test suite for actors
  • In a package named bank create a class named BankAccount that extends Actor
  • Create the following case class/objects in the bank package that will be used as messages
    • A case class named Deposit that takes an Int in its constructor
    • A case class named Withdraw that takes an Int in its constructor
    • A case object named CheckBalance
    • A case class named Balance that takes an Int in its constructor
  • The BankAccount class must:
    • Initially contain no funds
    • When it receives a Deposit message, increases its funds by the amount in the message
    • When it receives a Withdraw message, decrease its funds by the amount in the message only if the account has enough money to cover the withdrawal. If it doesn’t have enough money for the full withdrawal, no funds are removed
    • When it receives a CheckBalance message, sends its current funds back to the sender in a Balance message

Testing:
In a package named tests, write a class named TestBankAccount as a test suite that tests the BankAccount functionality