zella
zella

Reputation: 4685

How to test ask pattern with fallback

I have an actor, that asks other actor and future can fallbacks to other:

class Subject(db: ActorRef) extends Actor with ActorLogging {

  implicit val timeout = Timeout(1 seconds)
  implicit val ec = context.system.dispatcher

  override def preStart(): Unit = {
    db ? "auth" fallbackTo Future {
      "auth:timeout_error"
    } pipeTo self
  }

  def receive: Receive = {
    case msg: String => log.info(msg)
  }

}

I need to test fallback behavior, but don't know how to do it:

class ActorSpec extends TestKit(ActorSystem("MySpec"))
  with ImplicitSender with WordSpecLike with BeforeAndAfterAll with BeforeAndAfterEach with Matchers {

  val db = TestProbe()

  db.ref ! PoisonPill
  //db not exist anymore
  val subject = system.actorOf(Props(new Subject(db.ref)))

  //Something like: subject should receive "auth:timeout_error"

}

How to carry out this task properly?

Upvotes: 1

Views: 158

Answers (2)

The easiest way to perform the testing would be to refactor your Subject class to add a level of abstraction to the db parameter. There is nothing in Subject that inherently depends on the fact that db is an ActorRef; Subject simply needs something to send a query String to and receive a Future[String] response. Therefore you can make the constructor more generic by taking in a function:

object Subject {
  type Query = String
  type DBResult = Future[String]
  type DB : (Query) => DBResult

  val defaultAuth : DBResult = Future.successful("auth:timeout_error")

  val authQuery : Query = "auth"

  def queryWithDefault(db : DB, default : DBResult = defaultAuth) : DB = 
    (query : Query) => db(query) fallbackTo default     
}//end object Subject

class Subject(db : Subject.DB) extends Actor with ActorLogging {

  override def preStart() : Unit = {
    db(Subject.authQuery) pipeTo self
  }

  override def receive : Receive = {
    case msg : String => log info msg
  }
}//end class Subject

You can now test the queryWithDefault function without having to use akka at all:

import org.scalatest.{Matchers, WordSpecLike}
import org.scalatest.concurrent.ScalaFutures

class SubjectSpec 
  extends Matchers 
  with WorkSpecLike 
  with ScalaFutures {

  val alwaysFail : DB = 
    (query : Query) => Future.failed(new Exception("always fails"))

  import Subject.{defaultAuth, queryWithDefault, authQuery}

  "queryWithDefault" should {
    "always return default when db fails" in {
      val db = queryWithDefault(alwaysFail, defaultAuth)

      whenReady(
        for {
          authQueryRes <- db(authQuery)
          fooQueryRes  <- db("foo")
          defaultRes   <- defaultAuth
        }) {
          authQueryRes shouldEqual defaultRes
          fooQueryRes shouldEqual defaultRes 
        }
    }//end "always return..."
  }//end "queryWithDefault" should
}//end class SubjectSpec

You can then use the refactored, and unit tested, Subject.queryWithDefault function in production:

val actorDB : DB = (query : Query) => (db.ref ? query).mapTo[String]

val subject = system.actorOf(Props(queryWithDefault(actorDB, defaultAuth)))

Upvotes: 1

N. Melnikov
N. Melnikov

Reputation: 11

You can override receive method and send message to your TestProbe()

val probe = TestProbe()

db.ref ! PoisonPill

val actor = system.actorOf(Props(new Subject(db.ref) {
    override def receive: Received = {
      case message => probe.ref ! message
    }
}))

probe.expectMsg("auth:timeout_error")

Upvotes: 1

Related Questions