Akka基础-测试方法

基本写法

基本写法如下,继承三个trait,重写afterAll方法。每个should结构用于测试一个actor。其中包含多个in结构,每个测试该actor的一种行为。通过expectMsgAllOf()方法来指定预期的返回值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
class  BasicSpec extends TestKit(ActorSystem("BasicSpec"))
with ImplicitSender
with WordSpecLike
with BeforeAndAfterAll {
// setup
override def afterAll(): Unit = {
TestKit.shutdownActorSystem(system)
}
import BasicSpec._
// message assertions
"A lab test actor" should {
val labTestActor = system.actorOf(Props[LabTestActor])
"turn a string into uppercase" in {
labTestActor ! "I love Akka"
val reply = expectMsgType[String]
assert(reply == "I LOVE AKKA")
}
"reply to a greeting" in {
labTestActor ! "greeting"
expectMsgAnyOf("hi", "hello")
}
"reply with favorite tech" in {
labTestActor ! "favoriteTech"
expectMsgAllOf("Scala", "Akka")
}
"reply with cool tech in a different way" in {
labTestActor ! "favoriteTech"
val messages = receiveN(2) // Seq[Any]
// free to do more complicated assertions
}
"reply with cool tech in a fancy way" in {
labTestActor ! "favoriteTech"
expectMsgPF() {
case "Scala" => // only care that the PF is defined
case "Akka" =>
}
}
}
}

testProbe (测试探针)

A test probe is essentially a queryable mailbox which can be used in place of an actor and the received messages can then be asserted

翻译一下,testProbe就是在测试中用来代替其他actor的。

在真实场景下,actor内的逻辑通常会包含和其他actor或者第三方的接口交互的过程,此时如果想测试这个actor,就需要依赖外界的反馈。这肯定是不行的,由于环境的变化等因素,无法保证外界的反馈永远一致,同时这也不符合单测的定义。因此,通过testProbe的形式,保证只测试需要的actor,剥离外界干扰。

在下面的例子中,我们需要测试的是master actor,slave actor不在测试范围内。因此,通过第4行的TestProbe(“slave”)方法mock出了一个slave actor。之后,我们可以定义如果master正常的情况下,slave应该收到什么信息(16 slave.expectMsg),同时可以指定slave应该回应master什么信息(17 slave.reply)在此情况下,最终应该拿到什么返回(18 expectMsg)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
"A master actor" should {
"register a slave" in {
val master = system.actorOf(Props[Master])
val slave = TestProbe("slave")
master ! Register(slave.ref)
expectMsg(RegistrationAck)
}
"send the work to the slave actor" in {
val master = system.actorOf(Props[Master])
val slave = TestProbe("slave")
master ! Register(slave.ref)
expectMsg(RegistrationAck)
val workloadString = "I love Akka"
master ! Work(workloadString)
// the interaction between the master and the slave actor
slave.expectMsg(SlaveWork(workloadString, testActor))
slave.reply(WorkCompleted(3, testActor))
expectMsg(Report(3)) // testActor receives the Report(3)
}
"aggregate data correctly" in {
val master = system.actorOf(Props[Master])
val slave = TestProbe("slave")
master ! Register(slave.ref)
expectMsg(RegistrationAck)
val workloadString = "I love Akka"
master ! Work(workloadString)
master ! Work(workloadString)
// in the meantime I don't have a slave actor
slave.receiveWhile() {
case SlaveWork(`workloadString`, `testActor`) => slave.reply(WorkCompleted(3, testActor))
}
expectMsg(Report(3))
expectMsg(Report(6))
}
}

Timed assertions(基于时间的断言)

actor的响应时间是一个重要的指标,有时我们需要确保actor在指定时间内能够返回结果。因此,引入基于时间的断言。即在测试的时候,可以测试某个actor能否在指定时间内返回结果,如果不行,则测试失败。

如下面的例子,通过 within(1 second) {}的形式进行断言(4),这表明该代码块中的操作需要在指定时间内返回。

也可以传入两个参数,表示应该在时间范围a,b内返回(4 within(500 millis, 1 second) )

还可以进行多次请求(12 receiveWhile[Int](max=2 seconds, idle=500 millis, messages=4) )表示在2s内,需要收到4次结果,每次间隔最大500ms,这个方法返回一个list int,表示每次请求的耗时。继续增加断言,总耗时不能超过5s(15)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
"A worker actor" should {
val workerActor = system.actorOf(Props[WorkerActor])
"reply with the meaning of life in a timely manner" in {
within(500 millis, 1 second) {
workerActor ! "work"
expectMsg(WorkResult(42))
}
}
"reply with valid work at a reasonable cadence" in {
within(1 second) {
workerActor ! "workSequence"
val results: Seq[Int] = receiveWhile[Int](max=2 seconds, idle=500 millis, messages=4) {
case WorkResult(result) => result
}
assert(results.sum > 5)
}
}
"reply to a test probe in a timely manner" in {
within(1 second) {
val probe = TestProbe()
probe.send(workerActor, "work")
probe.expectMsg(WorkResult(42)) // timeout of 0.3 seconds
}
}
}

intercepting logs(断点日志)

测试中可以通过判断特定的日志是否打印来判断整个交互流程是否正常。这就是断点日志的作用。具体实现如下,在22行,actor中进行了日志打印,这个日志表示整个支付流程最终的成功。在上方的测试代码中,第3行,通过eventFilter intercept的形式进行特定日志的拦截,作用对象是intercept的代码块。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
"A checkout flow" should {
"correctly log the dispatch of an order" in {
EventFilter.info(pattern = s"Order [0-9]+ for item $item has been dispatched.", occurrences = 1) intercept {
// our test code
val checkoutRef = system.actorOf(Props[CheckoutActor])
checkoutRef ! Checkout(item, creditCard)
}
}
"freak out if the payment is denied" in {
EventFilter[RuntimeException](occurrences = 1) intercept {
val checkoutRef = system.actorOf(Props[CheckoutActor])
checkoutRef ! Checkout(item, invalidCreditCard)
}
}
}

class FulfillmentManager extends Actor with ActorLogging {
var orderId = 43
override def receive: Receive = {
case DispatchOrder(item: String) =>
orderId += 1
log.info(s"Order $orderId for item $item has been dispatched.")
sender() ! OrderConfirmed
}
}

可以控制日志的级别,还可以对异常进行拦截

1
2
3
4
5
6
"freak out if the payment is denied" in {
EventFilter[RuntimeException](occurrences = 1) intercept {
val checkoutRef = system.actorOf(Props[CheckoutActor])
checkoutRef ! Checkout(item, invalidCreditCard)
}
}

synchronized test(同步测试)

akka整体逻辑是基于消息系统进行相互的调用,这整套系统全部都是异步的。但在测试的时候,希望能够进行同步的测试,即在执行测试流程时,确保某个步骤一定获取到了返回值,再进行后续的步骤。通过同步测试来实现这个效果。

具体如下,通过第3行,TestActorRef的形式构建actor,这样的actor可以保证同步获取消息。即第4行,一定能保证counter收到消息。这里可以加第5行的断言

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
"A counter" should {
"synchronously increase its counter" in {
val counter = TestActorRef[Counter](Props[Counter])
counter ! Inc // counter has ALREADY received the message
assert(counter.underlyingActor.count == 1)
}
"synchronously increase its counter at the call of the receive function" in {
val counter = TestActorRef[Counter](Props[Counter])
counter.receive(Inc)
assert(counter.underlyingActor.count == 1)
}
"work on the calling thread dispatcher" in {
val counter = system.actorOf(Props[Counter])
val probe = TestProbe()
probe.send(counter, Read)
probe.expectMsg(Duration.Zero, 0) // probe has ALREADY received the message 0
}
}

同步测试的原理是用同一个线程来处理所有actor逻辑,单线程处理保证了顺序和同步性。


Akka基础-测试方法
http://www.bake-data.com/2024/07/06/Akka基础-测试方法/
Author
shuchen
Posted on
July 6, 2024
Licensed under