Статьи

Akka Notes — Актерское ведение журнала и тестирование

В первых двух частях ( одна , две ) мы кратко говорили об актерах и о том, как работает обмен сообщениями. В этой части давайте рассмотрим исправление регистрации и тестирования нашего TeacherActor .

резюмировать

Вот как выглядел наш актер из предыдущей части:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
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 черту ActorLogging для достижения этой цели. Давайте смешаем это в:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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 . Да, я сказал publish . Итак, что на самом деле является EventStream?

EventStream и Logging

EventStream ведет себя так же, как брокер сообщений, которому мы можем публиковать и получать сообщения. Одно тонкое отличие от обычной MOM заключается в том, что подписчиками EventStream может быть только Актер.

В случае регистрации сообщений все сообщения журнала будут опубликованы в EventStream. По умолчанию подписчик на эти сообщения — это DefaultLogger, который просто печатает сообщение в стандартный вывод.

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

Вот почему, когда мы пытаемся запустить StudentSimulatorApp , мы видим сообщение журнала, записанное на консоль.

Тем не менее, EventStream не подходит только для регистрации. Это универсальный механизм публикации-подписки, доступный внутри ActorWorld внутри виртуальной машины (подробнее об этом позже).

Вернуться к настройке SLF4J:

2. Настройте Akka для использования SLF4J

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

Мы храним эту информацию в файле с именем application.conf который должен находиться в вашем пути к классам. В нашей структуре папок sbt мы добавили бы это в вашу директорию main/resources .

Из конфигурации мы можем вывести, что:

  1. свойство loggers указывает Актера, который собирается подписаться на события журнала. Что делает Slf4jLogger , так это просто использует сообщения журнала и передает их фасаду SLF4J Logger.
  2. свойство loglevel просто указывает минимальный уровень, который следует учитывать при ведении журнала.
  3. logging-filter сравнивает loglevel настроенный уровень журнала и уровень входящего сообщения журнала и отбрасывает любое сообщение журнала ниже настроенного уровня журнала перед публикацией в EventStream.

Но почему у нас не было application.conf для предыдущего примера?

Просто потому, что Akka предоставляет некоторые нормальные значения по умолчанию, так что нам не нужно создавать файл конфигурации, прежде чем мы начнем с ним играть. Здесь мы будем слишком часто возвращаться к этому файлу для настройки различных вещей. Существует целый ряд потрясающих параметров, которые вы можете использовать внутри application.conf для самостоятельной регистрации. Они подробно объяснены здесь .

3. Добавьте logback.xml

Сейчас мы будем настраивать регистратор SLF4J с поддержкой logback .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
<?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>

Я бросил это в папку main/resources вместе с application.conf . Убедитесь, что main/resources теперь находится в вашем затмении или в другом пути к классам IDE. Также добавьте logback и slf4j-api в ваш build.sbt .

И когда мы TeacherLogActor akkaxxxxx.log и отправляем сообщение нашему новому TeacherLogActor , akkaxxxxx.log файл akkaxxxxx.log выглядит следующим образом.

SLF4JLogging

Тестирование Акки

Обратите внимание, что это ни в коем случае не является исчерпывающим охватом Testing Akka. Мы построим наши тесты на дополнительных возможностях тестирования в следующих частях под заголовками соответствующих тем. Эти тестовые сценарии нацелены на актеров, которых мы написали ранее.

В то время как StudentSimulatorApp делает то, что нам нужно, вы согласитесь с тем, что его следует вытеснить из тестовых случаев.

Чтобы облегчить задачу тестирования, Akka разработала удивительный инструментарий тестирования, с помощью которого мы могли бы выполнять некоторые магические действия, например, исследовать внутреннюю часть реализации Actor.

Хватит разговоров, посмотрим на тестовые случаи.

Давайте сначала попробуем отобразить StudentSimulatorApp в Testcase.

Mapping.001

Давайте теперь посмотрим на одну декларацию.

1
2
3
4
class TeacherPreTest extends TestKit(ActorSystem("UniversityMessageSystem")) 
  with WordSpecLike
  with MustMatchers
  with BeforeAndAfterAll {

Итак, из определения класса TestCase мы видим, что:

  1. Черта TestKit принимает ActorSystem через которую мы будем создавать Actors. Внутри TestKit украшает ActorSystem и заменяет диспетчер по умолчанию.
  2. Мы используем WordSpec, который является одним из многих забавных способов написания тестовых случаев с ScalaTest.
  3. MustMatchers предоставляют удобные методы, чтобы сделать тестовый пример похожим на естественный язык
  4. Мы BeforeAndAfterAll чтобы BeforeAndAfterAll работу ActorSystem после завершения тестовых случаев. Метод afterAll который обеспечивает эта черта, больше похож на наш tearDown в JUnit

1, 2 — отправка сообщения актерам

  1. Первый тестовый пример просто отправляет сообщение в PrintActor. Это ничего не утверждает!
  2. Во втором случае сообщение отправляется субъекту Log, который использует поле журнала ActorLogging для публикации сообщения в EventStream. Это тоже ничего не утверждает!
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
//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 — Утверждение внутреннего состояния актеров

В третьем случае используется метод TestActorRef и вызывается метод quoteList . Метод quoteList возвращает список цитат обратно. Мы используем этот список, чтобы утверждать его размер.

Если ссылка на quoteList отбрасывает вас назад, обратитесь к указанному выше коду TeacherLogActor и найдите:

1
2
3
//From TeacherLogActor
//We'll cover the purpose of this method in the Testing section
  def quoteList=quotes
1
2
3
4
5
6
7
//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 and Logging (выше), все сообщения журнала отправляются на EventStream и SLF4JLogger подписывается на него и использует свои приложения для записи в файл / консоль журнала и т. Д. Не было бы неплохо подписаться на EventStream прямо в нашем тестовом примере и утверждать наличие самого сообщения журнала? Похоже, мы тоже можем это сделать.

Это включает в себя два этапа:

  1. Вам нужно добавить дополнительную конфигурацию в ваш TestKit например, так:
    1
    2
    3
    4
    class TeacherTest extends TestKit(ActorSystem("UniversityMessageSystem", ConfigFactory.parseString("""akka.loggers = ["akka.testkit.TestEventListener"]"""))) 
      with WordSpecLike
      with MustMatchers
      with BeforeAndAfterAll {
  2. Теперь, когда у нас есть подписка на EventStream, мы можем утверждать ее из нашего тестового примера как:
    1
    2
    3
    4
    5
    6
    7
    8
    //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 просто перехватывает 1 лог-сообщение, которое начинается с QuoteResponse ( pattern='QuoteResponse* ). (Вы также можете достичь этого, используя start='QuoteResponse' . Если в результате отправки сообщения в TeacherLogActor нет сообщения журнала, тестовый случай завершится неудачно.

5 — Актеры тестирования с параметрами конструктора

Обратите внимание, что способ создания Actors в TestActorRef[TeacherLogActor] — это TestActorRef[TeacherLogActor] а не system.actorOf . Это просто для того, чтобы мы могли получить доступ к внутренним объектам актера через метод basicActor в TeacherActorRef. Мы не сможем достичь этого через ActorRef которому у нас есть доступ во время обычной среды выполнения. (Это не дает нам никаких оправданий для использования TestActorRef в производстве. Вы будете охотиться).

Если Actor принимает параметры, то способ создания TestActorRef будет таким:

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

Весь тестовый пример будет выглядеть примерно так:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
//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 жизненного цикла afterAll :

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

КОД

  • Как всегда, весь проект можно скачать с github здесь .
Ссылка: Заметки Акки — регистрация актеров и тестирование от нашего партнера JCG Аруна Маниваннана в блоге Rerun.me .