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