閱讀208 返回首頁    go 阿裏雲 go 技術社區[雲棲]


Akka筆記之日誌及測試

在前兩篇筆記中(第一篇第二篇),我們簡單地介紹了一下Actor以及它的消息傳遞是如何工作的。在本篇中,我們將看下如何解決TeacherActor的日誌打印及測試的問題。

簡單回顧

前麵我們的Actor是這樣的:

class TeacherActor extends Actor {

  val quotes = List(
    "Moderation is for cowards",
    "Anything worth doing is worth overdoing",
    "The trouble is you think you have time",
    "You never gonna know if you never even try")

  def receive = {

    case QuoteRequest => {

      import util.Random

      //Get a random Quote from the list and construct a response
      val quoteResponse=QuoteResponse(quotes(Random.nextInt(quotes.size)))

      println (quoteResponse)

    }
  }
}

在Akka中使用slf4j來打印日誌

你應該也看到了,在上麵的代碼中我們將QuoteResponse打印到了控製台上,你一定會覺得這種方式不太好。我們將使用slf4j接口來解決日誌打印的問題。

1. 修改類以支持日誌打印

Akka通過一個叫做ActorLogging的特質(trait)來實現的這一功能。我們將這個trait混入(mixin)到類裏邊:

class TeacherLogActor extends Actor with ActorLogging {

   val quotes = List(
    "Moderation is for cowards",
    "Anything worth doing is worth overdoing",
    "The trouble is you think you have time",
    "You never gonna know if you never even try")

  def receive = {

    case QuoteRequest => {

      import util.Random

      //get a random element (for now)
      val quoteResponse=QuoteResponse(quotes(Random.nextInt(quotes.size)))
      log.info(quoteResponse.toString())
    }
  }

  //We'll cover the purpose of this method in the Testing section
  def quoteList=quotes

}

說幾句題外話:

當我們要打印一條消息的時候,ActorLogging中的日誌方法會將日誌信息發布到一個EventStream流中。沒錯,我的確說的是發布。那麼EventStream到底是何方神聖?

EventStream和日誌

EventStream就像是一個我們用來發布及接收消息的消息代理。它與常見的消息中間件的根本區別在於EventStream的訂閱者隻能是一個Actor。

打印消息日誌的時候,所有的日誌信息都會發布到EventStream裏麵。DefaultLogger默認是訂閱了這些消息的,它隻是簡單地將消息打印到了標準輸出上。

class DefaultLogger extends Actor with StdOutLogger { 
    override def receive: Receive = {
        ...
        case event: LogEvent ⇒ print(event)
    }
}

因此,這就是為什麼我們在啟動了StudentSimulatorApp之後,消息日誌會打印到控製台上的原因。

也就是說,EventStream不光能用來記錄日誌。它是Actor在同一個虛擬機內的一個通用的發布-訂閱機製。

再回頭來說下如何配置slf4j:

2. 配置Akka以支持slf4j

akka{ 
    loggers = ["akka.event.slf4j.Slf4jLogger"]
    loglevel = "DEBUG"
    logging-filter = "akka.event.slf4j.Slf4jLoggingFilter"
}

我們把這類信息存儲到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。

<?xml version="1.0" encoding="UTF-8"?> 
<configuration> 
    <appender name="FILE"
        >
        <encoder>
            <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>

        <rollingPolicy >
            <fileNamePattern>logs\akka.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
            <timeBasedFileNamingAndTriggeringPolicy >
                <maxFileSize>50MB</maxFileSize>
            </timeBasedFileNamingAndTriggeringPolicy>
        </rollingPolicy>
    </appender>

    <root level="DEBUG">
        <appender-ref ref="FILE" />
    </root>
</configuration> 

我把它跟application.conf一道放在了main/resources目錄裏。請確保main/resources已經在eclipse或者別的IDE的classpath路徑中。同時還要在build.sbt中把logback和slf4j-api給包含進來。

當我們再次啟動StudentSimulatorApp並發送消息到新的TeacherLogActor的時候,我們所配置的akkaxxxxx.log文件裏的內容會是這樣的。

image

測試Akka

請注意這絕對不是要覆蓋到Akka測試中的所有細節。接下麵的部分我們將根據各個用例所對應的主題盡量地測試更多的特性。這些用例旨在覆蓋我們前麵所寫的各個Actor。

既然StudentSimulatorApp已經按需求實現好了,現在是時候給它測試一下了。

Akka提供了一套出色的測試工具來減輕測試時的痛苦,我們可以通過它實現許多不可思議的事情,比如可以查看Actor的內部實現。

講得差不多了,我們來看下這些測試用例吧。

我們先給StudentSimulatorApp寫一個測試用例。

image

先看下聲明部分。

class TeacherPreTest extends TestKit(ActorSystem("UniversityMessageSystem")) 
  with WordSpecLike
  with MustMatchers
  with BeforeAndAfterAll {
 

從TestCase的定義中可以看出:

1. TestKit trait接收一個ActorSystem參數,這個是用來創建Actor的。在TestKit的內部實現中,它會對ActorSystem進行封裝,並替換掉默認的分發器。
2. 我們使用WordSpec來編寫測試用例,這是進行Scala測試的一種很有意思的方式。
3. MustMatcher提供了一些很便利的方法,能讓測試用例看起來更像是自然語言。
4. 我們還將BeforeAndAfterAll混入了進來,以便在測試結束時能將ActorSystem關閉掉。trait提供的這個afterAll方法很像是JUnit中的tearDown。

1,2-將消息發送給Actor

1. 第一個測試用例隻是把一條消息發送給了PrintActor。它並沒有做斷言:-(

2. 第二個用例將消息發送給日誌Actor並使用ActorLogging裏的log對象將消息發布給EventStream。它還是沒有進行斷言:-(

//1. Sends message to the Print Actor. Not even a testcase actually
  "A teacher" must {

    "print a quote when a QuoteRequest message is sent" in {

      val teacherRef = TestActorRef[TeacherActor]
      teacherRef ! QuoteRequest
    }
  }

  //2. Sends message to the Log Actor. Again, not a testcase per se
  "A teacher with ActorLogging" must {

    "log a quote when a QuoteRequest message is sent" in {

      val teacherRef = TestActorRef[TeacherLogActor]
      teacherRef ! QuoteRequest
    }
   

3 -對Actor的內部狀態進行斷言判斷

第三個用例會使用TestActorRef裏的underlyingActor方法並調用TeacherActor內部的quoteList方法。這個方法會返回一個名言的列表。我們會對這個列表的大小進行斷言。

如果quoteList失敗了,看一下前麵提到的TeacherLogActor的代碼,找一下這行

//From TeacherLogActor
//We'll cover the purpose of this method in the Testing section
  def quoteList=quotes
 
//3. Asserts the internal State of the Log Actor.
    "have a quote list of size 4" in {

      val teacherRef = TestActorRef[TeacherLogActor]
      teacherRef.underlyingActor.quoteList must have size (4)
      teacherRef.underlyingActor.quoteList must have size (4)
    }
   

4 – 日誌消息的斷言

我們在前麵的EventStream和日誌一節已經提到過了,所有的日誌消息都會發送給EventStream,SLF4JLogger會訂閱這些消息並使用自己的appender將日誌寫入到日誌文件或者控製台中。不過在測試用例裏直接從EventStream中訂閱並對日誌消息本身進行斷言不是會更好一點麼?看起來貌似是可行的。

要實現這點需要做兩件事情:

1. 你需要給TestKit中添加一個額外的配置:

class TeacherTest extends TestKit(ActorSystem("UniversityMessageSystem", ConfigFactory.parseString("""akka.loggers = ["akka.testkit.TestEventListener"]"""))) 
  with WordSpecLike
  with MustMatchers
  with BeforeAndAfterAll {
 

2. 既然已經訂閱到EventStream中了,現在我們可以在測試用例中對它進行斷言了:

//4. Verifying log messages from eventStream
    "be verifiable via EventFilter in response to a QuoteRequest that is sent" in {

      val teacherRef = TestActorRef[TeacherLogActor]
      EventFilter.info(pattern = "QuoteResponse*", occurrences = 1) intercept {
        teacherRef ! QuoteRequest
      }
    }
   

EventFilter.info塊隻會攔截以QuoteResponse開頭的一條日誌消息(pattern=’QuoteResponse*)。(或者寫成start=’QuoteResponse’也可以。如果沒有日誌消息發送給TeacherLogActor,這條測試用例就會失敗)。

5 – 對帶構造參數的Actor進行測試

請注意在測試用例中我們是通過TestActorRef[TeacherLogActor]而非syste.actorOf來創建Actor的。這麼做是因為我們可以通過TeacherLogAcotr的underlyingActor方法來訪問Actor的內部屬性。而正常情況在運行時通過ActorRef是無法實現這點的。(不過這可不是在生產代碼中使用TestActorRef的借口。你會被揍死的)

如果Actor是接受參數的話,那麼我們可以這樣來創建TestActorRef:

val teacherRef = TestActorRef(new TeacherLogParameterActor(quotes))

完整的測試用例是這樣的:

//5. have a quote list of the same size as the input parameter
    " have a quote list of the same size as the input parameter" in {

      val quotes = List(
        "Moderation is for cowards",
        "Anything worth doing is worth overdoing",
        "The trouble is you think you have time",
        "You never gonna know if you never even try")

      val teacherRef = TestActorRef(new TeacherLogParameterActor(quotes))
      //val teacherRef = TestActorRef(Props(new TeacherLogParameterActor(quotes)))

      teacherRef.underlyingActor.quoteList must have size (4)
      EventFilter.info(pattern = "QuoteResponse*", occurrences = 1) intercept {
        teacherRef ! QuoteRequest
      }
    }
   

關閉ActorSystem

最後,到了afterAll方法

override def afterAll() { 
    super.afterAll()
    system.shutdown()
  }
 

代碼

同樣的,項目的完整代碼可以從Github中進行下載。

本文最早發布於我的個人博客: Java譯站

最後更新:2017-05-23 14:33:24

  上一篇:go  Akka筆記之消息傳遞
  下一篇:go  《Java特種兵》1.1 String的例子,見證下我們的功底