2013-09-04 8 views
5

Mam kilka sytuacji, w których podrywam mnie, gdy testuję aktora, a aktor rzuca wyjątek niespodziewanie (z powodu błędu) , ale test wciąż mija. Teraz przez większość czasu wyjątek w akcie oznacza, że ​​cokolwiek test zweryfikuje, nie wyjdzie prawidłowo, więc test się nie powiedzie, ale w rzadkich przypadkach to nieprawda. Wyjątek występuje w innym wątku niż bieg testowy, więc biegacz testowy nic o nim nie wie.Wada skalaty, gdy aktor akka zgłasza wyjątek poza wątkiem testowym

Jednym z przykładów jest użycie makiety do sprawdzenia, czy wywoływana jest jakaś zależność, a z powodu pomyłki w kodzie aktora wywołuję nieoczekiwaną metodę w sztuczce. To powoduje, że próbka rzuca wyjątek, który wysadza aktora, ale nie test. Czasami może to nawet doprowadzić do tego, że testy w nieznajomym stanie się tajemnicze z powodu sposobu, w jaki aktor wyleciał w powietrze. Na przykład:

// 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” przechodzi, ale potem za „ok aktor” nie dla jakiegoś powodu ja nie bardzo rozumiem ... z tym wyjątkiem:

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) 

Więc mogę zobacz sposoby złapania tego typu rzeczy, sprawdzając wyjście rejestratora w AfterEach handlers. Zdecydowanie wykonalne, choć trochę skomplikowane w przypadkach, w których naprawdę oczekuję wyjątku i właśnie to próbuję przetestować. Ale czy istnieje bardziej bezpośredni sposób radzenia sobie z tym i niepowodzenie testu?

Dodatek: Spojrzałem na TestEventListener i podejrzewam, że jest tam coś, co mogłoby pomóc, ale nie widzę tego. Jedyną dostępną dokumentacją było wykorzystanie jej do sprawdzenia oczekiwanych wyjątków, a nie nieoczekiwanych.

Odpowiedz

4

Dobra, miałem trochę czasu, żeby się z tym bawić. Mam dobre rozwiązanie, które wykorzystuje detektor zdarzeń i filtr do wychwytywania błędów. (Sprawdzanie isTerminated lub korzystanie z TestProbes jest prawdopodobnie dobre w bardziej skoncentrowanych przypadkach, ale wydaje się niezręczne, gdy próbuje się coś wymieszać w jakimkolwiek starym teście.)

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() 
    } 
    } 
} 

można po prostu użyć withErrorChecking inline w określonych miejscach, lub mieszać je w Suite i używać withFixture zrobić to globalnie we wszystkich testach, tak:

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

Jeśli używasz tego w mój oryginalny przykład, wtedy dostaniesz pierwszy test "wywołuje foo tylko wtedy, gdy otrzyma wiadomość", aby zakończyć się niepowodzeniem, co jest miłe, ponieważ właśnie tam jest prawdziwa porażka. Jednak test końcowy nadal będzie nieudany z powodu rozerwania systemu. Aby to naprawić, poszedłem o krok dalej i użyłem fixture.Suite do utworzenia osobnego TestKit dla każdego testu. To rozwiązuje wiele innych potencjalnych problemów izolacji testowej, gdy masz głośnych aktorów. Wymaga nieco więcej ceremonii deklarujących każdy test, ale myślę, że warto. Wykorzystując tę ​​cechę z moim oryginalnym przykładem, pierwszy test kończy się niepowodzeniem, a drugi przechodzi właśnie to, czego chcę!

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() 
    } 
    } 
} 
4

Inne niż analizowania logów, mogę pomyśleć dwa sposoby zawodzą, gdy testy an awarii Aktor:

  • Upewnić się, że nie zakończony wiadomość została odebrana
  • sprawdzić właściwość TestActorRef.isTerminated

Ta ostatnia opcja jest przestarzała, więc ją zignoruję.

Watching Other Actors from Probes opisuje, jak skonfigurować TestProbe. W takim przypadku może wyglądać ona następująco:

val probe = TestProbe() 
probe watch ref 

// Actual test goes here ... 

probe.expectNoMessage() 

Jeśli aktor zginie z powodu wyjątku, wygeneruje komunikat Zakończono. Jeśli zdarzy się to podczas testu i spodziewasz się czegoś innego, test się nie powiedzie. Jeśli dzieje się to po oczekiwaniu na ostatnią wiadomość, to oczekiwanieNoMessage() powinno zakończyć się niepowodzeniem po otrzymaniu polecenia Zakończono.

+0

Dzięki za sugestie. Naprawdę chcę czegoś, co mogę złożyć w cechę zestawu testowego wielokrotnego użytku. Kiedy mam szansę, pomyślę o tym, w jaki sposób mogę wziąć jeden z tych pomysłów i pracować. – ryryguy

8

Myślenie Aktorzy jest też inne rozwiązanie: niepowodzenia podróżować do przełożonego, więc jest to idealne miejsce, aby je złapać i karmić je do procedury testowej:

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 
    } 
})) 

można wysłać do testowanego aktora, wysyłając pod numer failureParent i wszystkie awarie - oczekiwane lub nieudane do sondy failures w celu kontroli.

+0

Działa pięknie, używał testActor (z 'ImplicitSender') zamiast instancji' TestProbe' w przypadkach, w których weryfikuję tylko wyjątkowe zachowanie. – cfeduke

+0

Fajnie! Zdecydowanie wydaje się bardziej "aktorski". Spróbuję, kiedy dostanę szansę. – ryryguy

Powiązane problemy