Akka中的单元测试

来源:互联网 发布:科比2000年西决数据 编辑:程序博客网 时间:2024/05/22 07:02

Akka中的单元测试 – TestKit

在任何一个系统中,测试代码都是至关重要的。当一个系统不断增大的时候,跟需要有每个关键功能的测试代码。在这片文章中,我将简要介绍一下Actor(Akka)模型中的测试方法和测试思想。

本文所有使用的代码来自于Akka的官方文档。

基本思想

ScalaTest

首先,Akka的测试是基于ScalaTest的,所以我们首先介绍一下Scala的测试方法。

Scala的测试使用的是WordSpecLike模式,使用3个关键字组织测试–must, should, can。这3个关键字可以创建一个测试,之后使用in关键字可以进行多个子测试(单元测试)。下面给一个例子:

  "An Echo actor" must{    "send back message unchanged" in {      val echo = system.actorOf(TestActors.echoActorProps)      echo ! "hello world"      expectMsg("hello world")    }    "another result" in {        // another case        ...    }  }

must关键字创建了一个测试—An Echo actor,之后有一个单元测试send back message unchanged,在之后还可以创建其他条件下的测试。使用WordSpecLike的测试模式,测试的可读性非常好。

TestKit

进行Actor实验,最为关键就是要扩展TestKit类。需要下面两个依赖

  "com.typesafe.akka" %% "akka-testkit" % akkaVersion,  "org.scalatest" %% "scalatest" % "3.0.1",

首先,我们需要知道TestKit中,有一个system变量 – ActorSystem。我们创建新的Actor的时候,都需要使用system

我们会产生一个疑问,如果我们向一个测试Actor – toTestActor 发送不同类型消息,查看toTestActor的处理结果,我们使用哪个Actor发送消息给toTestActor,并使用这个Actor接收来自toTestActor呢?
在TestKit中保存着一个testActor变量,这个变量会发送消息给toTestActor。
同时,testActor接收到的消息,会保存到一个queue中,我们调用expectMsg方法,检查queue中的是否有我们想要的消息。
举个例子:

  "An Echo actor" must{    "send back message unchanged" in {      val echo = system.actorOf(TestActors.echoActorProps)      echo ! "hello world"      expectMsg("hello world")    }  }

在这个例子中,我们使用system创建了一个Actor – echo进行测试。我们向echo发送消息(使用的是testActor),并使用expectMsg等待消息回复。

测试方法

断言方法

在之前的例子中,我们使用了expectMsg这个断言。在Akka中,有许多不同的断言方法,这里列出以下:
* expectMsg[T](d: Duration, msg:T):等待一段时间,检查消息
* expectNoMsg(d: Duration):无消息返回

等待时间

使用within([min, ]max{}设定一段时间来执行代码段中的代码。例子:

  "Time wait" should {    "not fail" in {      within(200 millis){        // expectNoMsg or receiveWhile, the final check for the within        // is skipped in order to avoid false positive due to wake-up latency.        expectNoMsg()        Thread.sleep(300)      }    }  }

在上面的例子中,我们等待200ms执行中间的代码。但是要注意如果代码段中有expectNoMsgreceiveWhile这样的代码,within检查会被省略。也就是,程序会使用expectNoMsgreceiveWhile的等待时间而不是within的等待时间。

Probe

当我们需要向多个Actor发送请求的时候,如果单纯使用testActor作为接受者的话,我们往往无法判断消息到来的顺序。所以我们需要又一个专门的接受者,准备接受指定的数据。这里我们介绍的TestProbe方法得到的结果就是一个靶接收器,等待来自测试Actor返回的结果。在之后我们将看到,这些probe甚至可以充当消息传递的中间人,在“暗中观察”消息的传递过程。

class MyDoubleEcho extends Actor{  var dest1: ActorRef = _  var dest2: ActorRef = _  override def receive = {    case (d1: ActorRef, d2: ActorRef) =>      dest1 = d1      dest2 = d2    case x =>      dest1 ! x      dest2 ! x  }}  "Test probe" should {    "return two echo" in {      val probe1 = TestProbe()      val probe2 = TestProbe()      val actor = system.actorOf(Props[MyDoubleEcho])      actor ! (probe1.ref, probe2.ref)      actor ! "Hello"      probe1.expectMsg("Hello")      probe2.expectMsg("Hello")    }  }

在上面的代码中,我们创建了两个probe来监听消息的接受。它们可以被看作一个普通的Actor,作为参数传递给我们测试的ActorMyDoubleEcho。我们通过等待probe接受的消息(不再是testActor)的消息,来测试相应的业务逻辑。

在Actor的文档中提到,我们不要对TestActor进行watch原因是,TestProbe使用的是CallingThreadDispatcher(一个线程调度管理类)来发送消息。而这个类会导致dead-lock出现。这个有一点疑问~

使用probewatch一个其他的Actor

val probe = TestProbe()probe watch targettarget ! PoisonPillprobe.expectTerminated(target)

使用probe来回复消息

val probe = TestProbe()val future = probe.ref ? "Hello"probe.expectMsg(0 millis, "hello")probe.reply("world")assert(future.isCompleted && future.value == Some(Success("world")))

在这个实验中需要注意,Success需要是scala.util.Success

使用probe作为消息中间人

class Source(target: ActorRef) extends Actor {  override def receive = {    case "start" => target ! "work"  }}class Destination extends Actor {  override def receive = {    case x => println(x)  }}class ForwardDemo extends TestKit(ActorSystem("testsystem"))with WordSpecLike with BeforeAndAfterAll{  override protected def afterAll(): Unit = {    shutdown(system)  }  // test probe act as a mid-one  "forward demo" should {    "receive message" in {      val probe = TestProbe()      val source = system.actorOf(Props(classOf[Source], probe.ref))      val dest = system.actorOf(Props[Destination])      source ! "start"      probe.expectMsg("work")      probe.forward(dest)    }  }}

在上面的实例中,我们创建了一个probe,接受来自source的消息回复。在probe.ref位置上应该是dest,我们放置probe用来监控消息。
probe之后会将消息forwarddest,仿佛probe没有接受过消息,source直接将消息发送给dest

AutoPilot

在上面的实例中,我们使用probe来实现对消息的监控,在这里我们将介绍一个更为方便的方法。

  "test auto pilot" should {    "forward message" in {      val probe = TestProbe()      probe.setAutoPilot(new testkit.TestActor.AutoPilot {        override def run(sender: ActorRef, msg: Any): TestActor.AutoPilot = msg match{          case "stop" => TestActor.NoAutoPilot          case x =>            println(s"receive message $x")            testActor.tell(x, sender); TestActor.KeepRunning        }      })      probe.ref ! "Hello"      expectMsg("Hello")      probe.ref ! "stop"      probe.ref ! "Hello2"      expectNoMsg    }  }

我们对probe设置auto-pilot。这个AutoPilot中,我们可以设置更为负责的传递逻辑。需要注意的是在run方法的partial function中, 必须返回probe下一步的状态,NoAutoPilot或者KeepRunning。在本例中,如果接受到来自sender的消息是”stop”,我们就停止运行Actor。否则就将消息返回给发送者testActor.tell(x, sender)

CallingThreadDispatcher

在进行单元测试的时候,我们将所有的测试Actor都运行在CallingThreadDispatcher之上。这带来的好处是,我们能够跟踪异常出现的堆栈。这回导致一个问题,就是如果一个Actor在运行的时候,阻塞了当前的线程,那么整个测试环境都将会被阻塞。

val latch = new CountDownLatch(1)actor ! startWorkerAfter(latch) // actor will call latch.await() before proceedingdoSomeSetupStuff()latch.countDown

在上面的实例中,代码将会无限期的阻塞,而且代码永远不会运行到第4行。原因是我们创建的worker需要等待latch,所以将当前的线程阻塞了。在实际的环境中,当前代码会在其他的dispatcher上执行,相当于在另一个线程中执行。而在测试当中,我们只有一个线程可以使用,所以一切都堵住了。

最后放上一段精彩的测试代码以供欣赏^ _ ^

package TestKitDemoimport akka.actor.{Actor, ActorRef, ActorSystem, Props}import akka.testkit.{DefaultTimeout, ImplicitSender, TestActors, TestKit}import com.typesafe.config.ConfigFactoryimport org.scalatest.{BeforeAndAfterAll, Matchers, WordSpecLike}import scala.collection.immutableimport scala.util.Randomimport scala.concurrent.duration._object TestKitUsageSpec {  val config =    """      |akka {      |  loglevel = "DEBUG"      |  actor {      |    debug {      |      receive = on      |      autoreceive = on      |    }      |  }      |}    """.stripMargin  class ForwardingActor(next: ActorRef) extends Actor {    override def receive: Receive = {      case msg => next ! msg    }  }  class FilteringActor(next: ActorRef) extends Actor {    override def receive: Receive = {      case msg: String => next ! msg      case _ => None    }  }  class SequencingActor(next: ActorRef, head: immutable.Seq[String],                        tail: immutable.Seq[String]) extends Actor{    override def receive: Receive = {      case msg => {        head foreach {next ! _}        next ! msg        tail foreach {next ! _}      }    }  }}class TestKitUsageSpec  extends TestKit(ActorSystem("TestKitUsageSpec", ConfigFactory.parseString(TestKitUsageSpec.config)))  with DefaultTimeout with ImplicitSender  with WordSpecLike with Matchers with BeforeAndAfterAll {  import TestKitUsageSpec._  val echoRef = system.actorOf(TestActors.echoActorProps)  val forwardRef = system.actorOf(Props(classOf[ForwardingActor], testActor))  val filterRef = system.actorOf(Props(classOf[FilteringActor], testActor))  val randomHead = Random.nextInt(6)  val randomTail = Random.nextInt(10)  val headList = immutable.Seq().padTo(randomHead, "0")  val tailList = immutable.Seq().padTo(randomTail, "1")  val seqRef = system.actorOf(Props(classOf[SequencingActor], testActor, headList, tailList))  override protected def afterAll(): Unit = {    shutdown()  }  "An EchoActor" should {    "Respond with the same message" in {      within(500 millis){        echoRef ! "Hello"        expectMsg("Hello")      }    }  }  "A ForwardingActor" should {    "Forward a message it receives" in {      within(500 millis) {        forwardRef ! "test"        expectMsg("test")      }    }  }  "A FilteringActor" should {    "Filter all messages, except expected messagetypes it receive" in {      var messages = Seq[String]()      within(500 millis) {        filterRef ! "test"        expectMsg("test")        filterRef ! 1        expectNoMsg        filterRef ! "Some"        filterRef ! "more"        filterRef ! 1        filterRef ! "text"        filterRef ! 1        receiveWhile(500 millis){          case msg: String => messages = msg +: messages        }      }      messages.length should be(3)      messages.reverse should be(Seq("Some", "more", "text"))    }  }  "A Sequencing Actor" should {    "receive an interesting message at some point" in {      within(500 millis) {        ignoreMsg {          case msg: String => msg != "something"        }        seqRef ! "something"        expectMsg("something")        ignoreMsg {          case msg : String => msg == "1"        }        expectNoMsg        ignoreNoMsg      }    }  }}
原创粉丝点击