在前两篇笔记中(第一篇第二篇),我们简单地介绍了一下Actor以及它的消息传递是如何工作的。在本篇中,我们将看下如何解决TeacherActor的日志打印及测试的问题。

简单回顾

前面我们的Actor是这样的:

  1. class TeacherActor extends Actor {
  2. val quotes = List(
  3. "Moderation is for cowards",
  4. "Anything worth doing is worth overdoing",
  5. "The trouble is you think you have time",
  6. "You never gonna know if you never even try")
  7. def receive = {
  8. case QuoteRequest => {
  9. import util.Random
  10. //Get a random Quote from the list and construct a response
  11. val quoteResponse=QuoteResponse(quotes(Random.nextInt(quotes.size)))
  12. println (quoteResponse)
  13. }
  14. }
  15. }

在Akka中使用slf4j来打印日志

你应该也看到了,在上面的代码中我们将QuoteResponse打印到了控制台上,你一定会觉得这种方式不太好。我们将使用slf4j接口来解决日志打印的问题。

1. 修改类以支持日志打印

Akka通过一个叫做ActorLogging的特质(trait)来实现的这一功能。我们将这个trait混入(mixin)到类里边:

  1. class TeacherLogActor extends Actor with ActorLogging {
  2. val quotes = List(
  3. "Moderation is for cowards",
  4. "Anything worth doing is worth overdoing",
  5. "The trouble is you think you have time",
  6. "You never gonna know if you never even try")
  7. def receive = {
  8. case QuoteRequest => {
  9. import util.Random
  10. //get a random element (for now)
  11. val quoteResponse=QuoteResponse(quotes(Random.nextInt(quotes.size)))
  12. log.info(quoteResponse.toString())
  13. }
  14. }
  15. //We'll cover the purpose of this method in the Testing section
  16. def quoteList=quotes
  17. }

说几句题外话:
当我们要打印一条消息的时候,ActorLogging中的日志方法会将日志信息发布到一个EventStream流中。没错,我的确说的是发布。那么EventStream到底是何方神圣?

EventStream和日志

EventStream就像是一个我们用来发布及接收消息的消息代理。它与常见的消息中间件的根本区别在于EventStream的订阅者只能是一个Actor。
打印消息日志的时候,所有的日志信息都会发布到EventStream里面。DefaultLogger默认是订阅了这些消息的,它只是简单地将消息打印到了标准输出上。

  1. class DefaultLogger extends Actor with StdOutLogger {
  2. override def receive: Receive = {
  3. ...
  4. case event: LogEvent print(event)
  5. }
  6. }

因此,这就是为什么我们在启动了StudentSimulatorApp之后,消息日志会打印到控制台上的原因。
也就是说,EventStream不光能用来记录日志。它是Actor在同一个虚拟机内的一个通用的发布-订阅机制。
再回头来说下如何配置slf4j:

2. 配置Akka以支持slf4j

  1. akka{
  2. loggers = ["akka.event.slf4j.Slf4jLogger"]
  3. loglevel = "DEBUG"
  4. logging-filter = "akka.event.slf4j.Slf4jLoggingFilter"
  5. }

我们把这类信息存储到classpath路径中的一个叫做application.conf的文件里。在我们sbt的目录结构中,它是放在了/main/resources目录下。
从配置信息中我们可以看出:
1. loggers属性指定的是订阅日志事件的Actor。Slf4jLogger要做的就是去消费日志消息,并委托给slf4j日志接口去处理。
2. logLevel属性配置的是日志打印的最小级别
3. loggeing-filter会将配置的logLevel和传进来的日志消息的级别进行比较,把低于logLevel的日志都给过滤掉,然后再发布到EventStream中。
但为什么前面这个例子我们没有用到application.conf呢?
这是因为Akka提供了一些默认值,因此在我们真正使用它之前不用去整一个配置文件。后面我们还会频繁使用到这个文件来定制各式各样的东西。在application.conf中除了日志参数,还有许多很棒的参数以供使用。这里是一个详细的说明

3. 配置logback.xml

现在我们来配置一个通过logback来打印日志的slf4j的logger。

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <configuration>
  3. <appender name="FILE"
  4. class="ch.qos.logback.core.rolling.RollingFileAppender">
  5. <encoder>
  6. <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
  7. </encoder>
  8. <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
  9. <fileNamePattern>logs\akka.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
  10. <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
  11. <maxFileSize>50MB</maxFileSize>
  12. </timeBasedFileNamingAndTriggeringPolicy>
  13. </rollingPolicy>
  14. </appender>
  15. <root level="DEBUG">
  16. <appender-ref ref="FILE" />
  17. </root>
  18. </configuration>

我把它跟application.conf一道放在了main/resources目录里。请确保main/resources已经在eclipse或者别的IDE的classpath路径中。同时还要在build.sbt中把logback和slf4j-api给包含进来。
当我们再次启动StudentSimulatorApp并发送消息到新的TeacherLogActor的时候,我们所配置的akkaxxxxx.log文件里的内容会是这样的。
Akka笔记之日志及测试 - 图1

测试Akka

请注意这绝对不是要覆盖到Akka测试中的所有细节。接下面的部分我们将根据各个用例所对应的主题尽量地测试更多的特性。这些用例旨在覆盖我们前面所写的各个Actor。
既然StudentSimulatorApp已经按需求实现好了,现在是时候给它测试一下了。
Akka提供了一套出色的测试工具来减轻测试时的痛苦,我们可以通过它实现许多不可思议的事情,比如可以查看Actor的内部实现。
讲得差不多了,我们来看下这些测试用例吧。
我们先给StudentSimulatorApp写一个测试用例。
Akka笔记之日志及测试 - 图2
先看下声明部分。

  1. TestKit trait接收一个ActorSystem参数,这个是用来创建Actor的。在TestKit的内部实现中,它会对ActorSystem进行封装,并替换掉默认的分发器。
    2. 我们使用WordSpec来编写测试用例,这是进行Scala测试的一种很有意思的方式。
    3. MustMatcher提供了一些很便利的方法,能让测试用例看起来更像是自然语言。
    4. 我们还将BeforeAndAfterAll混入了进来,以便在测试结束时能将ActorSystem关闭掉。trait提供的这个afterAll方法很像是JUnit中的tearDown。

    1,2-将消息发送给Actor

  2. 第一个测试用例只是把一条消息发送给了PrintActor。它并没有做断言:-(
    2. 第二个用例将消息发送给日志Actor并使用ActorLogging里的log对象将消息发布给EventStream。它还是没有进行断言:-(
  1. //1. Sends message to the Print Actor. Not even a testcase actually
  2. "A teacher" must {
  3. "print a quote when a QuoteRequest message is sent" in {
  4. val teacherRef = TestActorRef[TeacherActor]
  5. teacherRef ! QuoteRequest
  6. }
  7. }
  8. //2. Sends message to the Log Actor. Again, not a testcase per se
  9. "A teacher with ActorLogging" must {
  10. "log a quote when a QuoteRequest message is sent" in {
  11. val teacherRef = TestActorRef[TeacherLogActor]
  12. teacherRef ! QuoteRequest
  13. }

3 -对Actor的内部状态进行断言判断

第三个用例会使用TestActorRef里的underlyingActor方法并调用TeacherActor内部的quoteList方法。这个方法会返回一个名言的列表。我们会对这个列表的大小进行断言。
如果quoteList失败了,看一下前面提到的TeacherLogActor的代码,找一下这行

  1. //From TeacherLogActor
  2. //We'll cover the purpose of this method in the Testing section
  3. def quoteList=quotes
  4. //3. Asserts the internal State of the Log Actor.
  5. "have a quote list of size 4" in {
  6. val teacherRef = TestActorRef[TeacherLogActor]
  7. teacherRef.underlyingActor.quoteList must have size (4)
  8. teacherRef.underlyingActor.quoteList must have size (4)
  9. }

4 – 日志消息的断言

我们在前面的EventStream和日志一节已经提到过了,所有的日志消息都会发送给EventStream,SLF4JLogger会订阅这些消息并使用自己的appender将日志写入到日志文件或者控制台中。不过在测试用例里直接从EventStream中订阅并对日志消息本身进行断言不是会更好一点么?看起来貌似是可行的。
要实现这点需要做两件事情:
1. 你需要给TestKit中添加一个额外的配置:

  1. class TeacherTest extends TestKit(ActorSystem("UniversityMessageSystem", ConfigFactory.parseString("""akka.loggers = ["akka.testkit.TestEventListener"]""")))
  2. with WordSpecLike
  3. with MustMatchers
  4. with BeforeAndAfterAll {
  1. 既然已经订阅到EventStream中了,现在我们可以在测试用例中对它进行断言了:
    1. //4. Verifying log messages from eventStream
    2. "be verifiable via EventFilter in response to a QuoteRequest that is sent" in {
    3. val teacherRef = TestActorRef[TeacherLogActor]
    4. EventFilter.info(pattern = "QuoteResponse*", occurrences = 1) intercept {
    5. teacherRef ! QuoteRequest
    6. }
    7. }

EventFilter.info块只会拦截以QuoteResponse开头的一条日志消息(pattern=’QuoteResponse*)。(或者写成start=’QuoteResponse’也可以。如果没有日志消息发送给TeacherLogActor,这条测试用例就会失败)。

5 – 对带构造参数的Actor进行测试

请注意在测试用例中我们是通过TestActorRef[TeacherLogActor]而非syste.actorOf来创建Actor的。这么做是因为我们可以通过TeacherLogAcotr的underlyingActor方法来访问Actor的内部属性。而正常情况在运行时通过ActorRef是无法实现这点的。(不过这可不是在生产代码中使用TestActorRef的借口。你会被揍死的)
如果Actor是接受参数的话,那么我们可以这样来创建TestActorRef:

  1. val teacherRef = TestActorRef(new TeacherLogParameterActor(quotes))

完整的测试用例是这样的:

  1. //5. have a quote list of the same size as the input parameter
  2. " have a quote list of the same size as the input parameter" in {
  3. val quotes = List(
  4. "Moderation is for cowards",
  5. "Anything worth doing is worth overdoing",
  6. "The trouble is you think you have time",
  7. "You never gonna know if you never even try")
  8. val teacherRef = TestActorRef(new TeacherLogParameterActor(quotes))
  9. //val teacherRef = TestActorRef(Props(new TeacherLogParameterActor(quotes)))
  10. teacherRef.underlyingActor.quoteList must have size (4)
  11. EventFilter.info(pattern = "QuoteResponse*", occurrences = 1) intercept {
  12. teacherRef ! QuoteRequest
  13. }
  14. }

关闭ActorSystem

最后,到了afterAll方法

  1. override def afterAll() {
  2. super.afterAll()
  3. system.shutdown()
  4. }

代码

同样的,项目的完整代码可以从Github中进行下载。