Сбой самого масштабного, когда akka actor создает исключение за пределами тестового потока

Я несколько раз сталкивался с ситуацией, когда я проверял Актера, и Актер неожиданно выдает исключение (из-за ошибки), но тест все равно проходит. Теперь в большинстве случаев исключение в Actor означает, что то, что проверяет тест, не будет работать должным образом, поэтому тест не пройден, но в редких случаях это не так. Исключение возникает в другом потоке, чем у тестера, поэтому тестер ничего не знает об этом.

Один из примеров - когда я использую макет для проверки вызова некоторой зависимости, и из-за ошибки в коде Actor я вызываю неожиданный метод в макете. Это заставляет насмешку выдавать исключение, которое взрывает актера, но не тест. Иногда это может даже вызвать загадочные тесты из-за того, как взорвался Актер. Например:

// using scala 2.10, akka 2.1.1, scalatest 1.9.1, easymock 3.1
// (FunSpec and TestKit)
class SomeAPI {
  def foo(x: String) = println(x)
  def bar(y: String) = println(y)
}

class SomeActor(someApi: SomeAPI) extends Actor {
  def receive = {
    case x:String  =>
      someApi.foo(x)
      someApi.bar(x)
  }
}

describe("problem example") {
  it("calls foo only when it receives a message") {
    val mockAPI = mock[SomeAPI]
    val ref = TestActorRef(new SomeActor(mockAPI))

    expecting {
      mockAPI.foo("Hi").once()
    }

    whenExecuting(mockAPI) {
      ref.tell("Hi", testActor)
    }
  }

  it("ok actor") {
    val ref = TestActorRef(new Actor {
      def receive = {
        case "Hi"  => sender ! "Hello"
      }
    })
    ref.tell("Hi", testActor)
    expectMsg("Hello")
  }
}

"problemExample" проходит, но затем не работает "ok actor" по какой-то причине, по какой-то причине я не совсем понимаю... с этим исключением:

cannot reserve actor name '$$b': already terminated
java.lang.IllegalStateException: cannot reserve actor name '$$b': already terminated
at       akka.actor.dungeon.ChildrenContainer$TerminatedChildrenContainer$.reserve(ChildrenContainer.scala:86)
at akka.actor.dungeon.Children$class.reserveChild(Children.scala:78)
at akka.actor.ActorCell.reserveChild(ActorCell.scala:306)
at akka.testkit.TestActorRef.<init>(TestActorRef.scala:29)

Итак, я могу увидеть способы поймать такого рода вещи, изучив выходные данные регистратора в обработчиках afterEach. Определенно выполнимо, хотя немного сложнее в тех случаях, когда я действительно ожидаю исключения, и это то, что я пытаюсь проверить. Но есть ли более прямой способ справиться с этим и сделать тест неудачным?

Приложение: Я посмотрел на TestEventListener и подозреваю, что, возможно, есть что-то, что могло бы помочь, но я не вижу этого. Единственная документация, которую я смог найти, касалась использования ее для проверки ожидаемых, а не неожиданных исключений.

3 ответа

Решение

Хорошо, у меня было немного времени, чтобы поиграть с этим. У меня есть хорошее решение, которое использует прослушиватель событий и фильтр для обнаружения ошибок. (Проверка isTermination или использование TestProbes, вероятно, хороши в более сфокусированных случаях, но кажутся неловкими, когда пытаются что-то смешать с любым старым тестом.)

import akka.actor.{Props, Actor, ActorSystem}
import akka.event.Logging.Error
import akka.testkit._
import com.typesafe.config.Config
import org.scalatest._
import org.scalatest.matchers.ShouldMatchers
import org.scalatest.mock.EasyMockSugar
import scala.collection.mutable

trait AkkaErrorChecking extends ShouldMatchers {
  val system:ActorSystem
  val errors:mutable.MutableList[Error] = new mutable.MutableList[Error]
  val errorCaptureFilter = EventFilter.custom {
    case e: Error =>
      errors += e
      false // don't actually filter out this event - it's nice to see the full output in console.
  }

  lazy val testListener = system.actorOf(Props(new akka.testkit.TestEventListener {
    addFilter(errorCaptureFilter)
  }))

  def withErrorChecking[T](block: => T) = {
    try {
      system.eventStream.subscribe(testListener, classOf[Error])
      filterEvents(errorCaptureFilter)(block)(system)
      withClue(errors.mkString("Akka error(s):\n", "\n", ""))(errors should be('empty))
    } finally {
      system.eventStream.unsubscribe(testListener)
      errors.clear()
    }
  }
}

Вы можете просто использовать withErrorChecking встроенный в определенных местах, или смешайте его в Suite и используйте withFixture сделать это глобально во всех тестах, например так:

trait AkkaErrorCheckingSuite extends AkkaErrorChecking with FunSpec {
  override protected def withFixture(test: NoArgTest) {
    withErrorChecking(test())
  }
}

Если вы используете это в моем исходном примере, то первый тест "вызовет foo только при получении сообщения" будет неудачным, и это хорошо, потому что именно в этом и заключается настоящая ошибка. Но последующий тест все равно будет неудачным из-за взрыва системы. Чтобы это исправить, я пошел еще дальше и использовал fixture.Suite например, отдельный TestKit за каждый тест. Это решает множество других потенциальных проблем изоляции тестов, когда у вас шумные актеры. Требуется немного больше церемонии объявления каждого теста, но я думаю, что оно того стоит. Используя эту черту с моим исходным примером, я получаю первый тест не пройденным, а второй - пройденный, и это то, чего я хочу!

trait IsolatedTestKit extends ShouldMatchers { this: fixture.Suite =>
  type FixtureParam = TestKit
  // override this if you want to pass a Config to the actor system instead of using default reference configuration
  val actorSystemConfig: Option[Config] = None

  private val systemNameRegex = "[^a-zA-Z0-9]".r

  override protected def withFixture(test: OneArgTest) {
    val fixtureSystem = actorSystemConfig.map(config => ActorSystem(systemNameRegex.replaceAllIn(test.name, "-"), config))
                                         .getOrElse    (ActorSystem (systemNameRegex.replaceAllIn(test.name, "-")))
    try {
      val errorCheck = new AkkaErrorChecking {
        val system = fixtureSystem
      }
      errorCheck.withErrorChecking {
        test(new TestKit(fixtureSystem))
      }
    }
    finally {
      fixtureSystem.shutdown()
    }
  }
}

Размышляя об актерах, есть и другое решение: сбои отправляются к супервайзеру, так что это идеальное место, чтобы поймать их и включить в процедуру тестирования:

val failures = TestProbe()
val props = ... // description for the actor under test
val failureParent = system.actorOf(Props(new Actor {
  val child = context.actorOf(props, "child")
  override val supervisorStrategy = OneForOneStrategy() {
    case f => failures.ref ! f; Stop // or whichever directive is appropriate
  }
  def receive = {
    case msg => child forward msg
  }
}))

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

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

  • Убедитесь, что Прекращенное сообщение не получено
  • Проверьте свойство TestActorRef.isTeridity

Последний вариант устарел, поэтому я его проигнорирую.

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

val probe = TestProbe()
probe watch ref

// Actual test goes here ...

probe.expectNoMessage()

Если актер умирает из-за исключения, он генерирует сообщение "Прекращено". Если это произойдет во время теста, и вы ожидаете чего-то другого, тест не пройден. Если это произойдет после ожидания вашего последнего сообщения, то при ожидании завершения будет ожидаться сбой службы ожидаемого сообщения ().

Другие вопросы по тегам