1. 程式人生 > >Akka筆記之日誌及測試

Akka筆記之日誌及測試

英文原文連結譯文連結,原文作者:Arun Manivannan ,譯者:有孚

在前兩篇筆記中(第一篇第二篇),我們簡單地介紹了一下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"
        class="ch.qos.logback.core.rolling.RollingFileAppender">
        <encoder>
            <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>

        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>logs\akka.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
            <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                <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譯站