2016-02-27 5 views
7

HAUPTIDEE: Wie können wir Unit-Actors mit ziemlich komplexer Geschäftslogik testen (oder Re-Faktor, um Unit-Tests zu erleichtern)?Akka-Unit-Teststrategien ohne Mocks

Ich habe Akka für ein Projekt in meiner Firma verwendet (einige sehr grundlegende Dinge sind in Produktion) und haben meine Schauspieler ständig re-factoring, recherchieren und experimentieren mit Akka testkit, um zu sehen, ob ich es richtig machen kann. ..

Grundsätzlich die meisten der Lesung, die ich beiläufig getan habe, sagt: "Man, alles, was Sie brauchen, ist das Testkit. Wenn Sie Mocks verwenden, machen Sie es falsch !!" aber die Dokumente und Beispiele sind so leicht, dass ich eine Reihe von Fragen finde, die nicht abgedeckt sind (im Grunde sind ihre Beispiele ziemlich konstruierte Klassen, die mit keiner anderen Aktoren interagieren oder nur auf triviale Weise wie die Eingabe am Ende der Methode). Nebenbei, wenn mich jemand mit einer angemessenen Menge an Komplexität auf eine Testsuite für eine Akka-App hinweisen könnte, würde ich das sehr schätzen.

Hier werde ich jetzt zumindest versuchen, einige spezielle Fälle zu detaillieren & würde gerne wissen, was man den "Akka-zertifizierten" Ansatz nennen würde (aber bitte nichts vage ... Ich suche nach der Roland Kuhn Stilmethodik , wenn er jemals wirklich zu spezifischen Themen tauchen würde). Ich werde Strategien akzeptieren, die Refactoring beinhalten, aber bitte beachten Sie meine diesbezüglichen Ängste in den Szenarien.

Szenario 1: Lateral-Verfahren (Verfahren eine andere in der gleichen Schauspieler Aufruf)

case class GetProductById(id : Int) 
case class GetActiveProductsByIds(ids : List[Int]) 

class ProductActor(val partsActor : ActorRef, inventoryActor : ActorRef) extends Actor { 
    override def receive: Receive = { 
    case GetProductById(pId) => pipe(getProductById(pId)) to sender 
    case GetActiveProductsByIds(pIds) => pipe(getActiveProductsByIds(pIds)) to sender 
    } 

    def getProductById(id : Int) : Future[Product] = { 
    for { 
     // Using pseudo-code here 
     parts <- (partsActor ? GetPartsForProduct(id)).mapTo[List[Part]] 
     instock <- (inventoryActor ? StockStatusRequest(id)).mapTo[Boolean] 
     product <- Product(parts, instock) 
    } yield product 
    } 

    def getActiveProductsByIds(ids : List[Int]) : Future[List[Product]] = { 
    for { 
     activeProductIds <- (inventoryActor ? FilterActiveProducts(ids)).mapTo[List[Int]] 
     activeProducts <- Future.sequence(activeProductIds map getProductById) 
    } yield activeProducts 
    } 
} 

Also, im Grunde haben wir hier was 2 holen sind im wesentlichen Verfahren, eine singuläre und eine für mehrere. Im Einzelfall ist das Testen einfach. Wir richten einen TestActorRef ein, injizieren einige Tests in den Konstruktor und stellen lediglich sicher, dass die richtige Nachrichtenkette ausgelöst wird.

Meine Angst hier kommt von der Multiple Fetch-Methode. Es beinhaltet einen Filterschritt (um nur aktive Produkt-IDs zu holen). Um dies zu testen, kann ich nun das gleiche Szenario (TestActorRef von ProductActor mit den Sonden, die die im Konstruktor geforderten Aktoren ersetzen) einrichten. Um den Nachrichtenweiterleitungsfluss zu testen, muss ich jedoch die gesamte Nachrichtenverkettung für nicht nur die Antwort auf FilterActiveProducts vortäuschen, sondern alle, die bereits durch den vorherigen Test der "getProductById" -Methode abgedeckt wurden (nicht wirklich Unit-Tests) , ist es?). Offensichtlich kann dies außer Kontrolle geraten in Bezug auf die Menge an Nachrichtenspott, die notwendig ist, und es wäre viel einfacher zu verifizieren (durch Mocks?), Dass diese Methode einfach für jede ID aufgerufen wird, die den Filter überlebt.

Nun, ich verstehe, dass dies gelöst werden kann durch Extrahieren eines anderen Akteurs (Erstellen eines ProductCollectorActor, der für mehrere IDs erhält und ruft einfach auf den ProductActor mit einer einzigen Nachrichtenanforderung für jede ID, die den Filter passiert). Allerdings habe ich diese & berechnet, wenn ich Extraktionen wie diese für jeden schwer zu testenden Satz von Geschwistermethoden machen würde, die ich habe, werde ich mit Dutzenden von Akteuren für relativ kleine Menge von Domänenobjekten enden. Die Menge an Standard-Overhead wäre viel, und das System wird wesentlich komplexer sein (viel mehr Akteure führen nur, was im Wesentlichen einige Methodenzusammensetzungen sind).

Abgesehen: Inline (statisch) Logik

Ein Weg, ich habe versucht, dies zu regeln durch Inline bewegt (im Grunde alles, was mehr als ein sehr einfacher Steuerungsablauf ist) in den Begleiter oder ein anderes Singleton-Objekt .wenn es ein Verfahren, bei dem obigen Verfahren war zum Beispiel die Produkte herauszufiltern, war, wenn sie eine bestimmte Art abgestimmt, ich könnte so etwas wie folgt vorgehen:

object ProductActor { 
    def passthroughToysOnly(products : List[Product]) : List[Toy] = 
    products flatMap {p => 
     p.category match { 
     case "toy" => Some(p) 
     case _ => None 
     } 
    } 
} 

Diese Einheit isoliert getestet werden kann ziemlich gut, und können tatsächlich das Testen von ziemlich komplexen Einheiten ermöglichen, solange sie nicht an andere Akteure weiterleiten. Ich bin kein großer Fan davon, diese weit von der Logik zu entfernen, die sie verwendet (sollte ich es in den tatsächlichen Akteur setzen & dann testen, indem Sie zugrunde liegendenActor aufrufen?).

Insgesamt führt es immer noch zu dem Problem, dass bei mehr naiven Message-Passing-basierten Tests in den Methoden, die dies tatsächlich nennen, ich im Wesentlichen alle meine Nachrichtenerwartungen bearbeiten muss, um zu reflektieren, wie die Daten von diesen transformiert werden statisch '(ich weiß, dass sie in Scala nicht technisch statisch sind, sondern mit mir tragen) Methoden. Ich denke, damit kann ich leben, da es ein realistischer Teil des Komponententestens ist (in einer Methode, die mehrere andere aufruft, prüfen wir wahrscheinlich die gestaltkombinatorische Logik in Gegenwart von Testdaten mit unterschiedlichen Eigenschaften).

Wo das alles wirklich für mich bricht ist hier -

Szenario 2: Rekursive Algorithmen

case class GetTypeSpecificProductById(id : Int) 

class TypeSpecificProductActor(productActor : ActorRef, bundleActor : ActorRef) extends Actor { 
    override def receive: Receive = { 
    case GetTypeSpecificProductById(pId) => pipe(getTypeSpecificProductById(pId)) to sender 
    } 

    def getTypeSpecificProductById(id : Int) : Future[Product] = { 
    (productActor ? GetProductById(id)).mapTo[Product] flatMap (p => p.category match { 
     case "toy" => Toy(p.id, p.name, p.color) 
     case "bundle" => 
      Bundle(p.id, p.name, 
      getProductsInBundle((bundleActor ? GetProductIdsForBundle(p.id).mapTo[List[Int])) 
     } 
    ) 
    } 

    def getProductsInBundle(ids : List[Int]) : List[Product] = 
    ids map getProductById 
} 

Also ja, es ein bisschen Pseudo-Code hier ist aber der Kern ist, dass wir jetzt habe eine rekursive Methode (getProductId ruft im Fall eines Bundles getProductsById auf, das wiederum getProductId aufruft). Mit Spott gibt es Punkte, wo wir die Rekursion abschneiden könnten, um die Dinge testbarer zu machen. Aber selbst das ist komplex, da innerhalb bestimmter Methoden innerhalb der Methode Schauspieleraufrufe stattfinden.

Dies ist wirklich der perfekte Sturm für mich .... Extrahieren der Übereinstimmung für den "Bündel" -Fall in einen niedrigeren Akteur kann vielversprechend sein, bedeutet aber auch, dass wir jetzt mit zirkulären Abhängigkeit befassen müssen (bundleAssembly Actor benötigt) typeSpecificActor, der bundleAssembly benötigt).

Diese über reine Botschaft prüfbar sein kann spöttischen (Stub-Nachrichten schaffen, in dem ich beurteilen kann, welche Ebene der Rekursion sie & sorgfältig Gestaltung dieser Nachrichtenfolge haben wird), aber es wird ziemlich komplex & schlimmer sein, wenn mehr Logik als eine einzige erforderlich ist Extra-Actor-Call für den Bundle-Typ.

Bemerkungen

Dank im Voraus für jede Hilfe! Ich bin wirklich sehr leidenschaftlich über minimale, testbare, gut gestaltete Code und habe Angst, dass, wenn ich versuche, alles über die Extraktion zu erreichen, ich noch zirkuläre Probleme haben werde, noch nicht wirklich inline/kombinatorische Logik testen können & mein Code wird 1000x ausführlicher, als es hätte sein können, mit einer Fülle von Vorsätzen für winzige, einfach zu verantwortende Akteure. Im Wesentlichen wird der Code um die Teststruktur geschrieben.

Ich bin auch sehr vorsichtig mit überentwickelten Tests, denn wenn sie komplizierte Nachrichtenfolgen anstelle von Methodenaufrufen testen (was ich nicht weiß, wie einfache semantische Aufrufe außer mit Mocks zu erwarten sind), könnten die Tests gelingen, aber gewonnen werden Es ist wirklich keine echte Unit-Tests der Kern-Methode Funktionalität. Stattdessen wird es nur eine direkte Reflektion von Kontrollflusskonstrukten im Code (oder dem Nachrichtenweitergabesystem) sein.

Also vielleicht ist es, dass ich zu viel von Unit Tests frage, aber bitte, wenn Sie etwas Weisheit haben, setzen Sie mich gerade!

+0

Ihre Tests sind möglicherweise zu granular. Tests sollten die Geschäftslogik prüfen und in gewisser Weise dokumentieren. Eine Möglichkeit besteht also darin, Tests für den Vertrag zu schreiben, den Ihr Service bietet. Komponententests, die zum Testen bestimmter Implementierungsdetails geschrieben werden, können das Refactoring erschweren. Sie können die meiste Zeit damit verbringen, Ihre Komponententests zu reparieren. –

+0

Danke für den Vorschlag, ich denke in gewisser Weise könnte man recht haben. Mein Verständnis des Nutzens von Komponententests ist jedoch, dass, wenn Sie Ihre Anwendung in geeignete Einheiten aufteilen, sie beide eine semantische Bedeutung haben und isoliert getestet werden können. Dann kann die Codebasis als äußerst zuverlässig angesehen werden, selbst wenn es wenige Tests gibt, um die äußeren Integrationen abzudecken (da sie alle etwas lineare Zusammensetzungen von Einheiten sein werden). Und in vielen Fällen (wie beim Erstellen einer Lib oder einer API) wird es keine äußeren Integrationen zum Testen geben (dies sind alle Benutzercodes), so dass Sie wirklich nichts außer den Einheiten haben. – jm0

+0

Also mein qunadry re: Testen in Akka ist wirklich, dass ich Unit-Test wollen (äußere Methode testet zu viel kombinatorische Daten/Message-Passing), aber ich will die Unit-Tests semantischen Wert haben (nicht willkürlich granular sein, wie ich erwähne Schlussbemerkung). – jm0

Antwort

1

Ich stimme nicht mit Ihrer Aussage "Ich bin kein großer Fan davon, diese weit von der Logik entfernt, die sie verwendet".

Ich finde dies eine wesentliche Komponente der Komponententests und Code-Organisation.

Jamie Allen, in Effektive Akka, sagt folgendes über die Geschäftslogik Externalisierung (Hervorhebung von mir):

Dies hat einige zusätzliche Vorteile. Zunächst einmal können wir nicht nur wir schreiben nützliche Unit-Tests, aber wir können auch sinnvolle Stack-Traces, die sagen den Namen, wo der Fehler in der Funktion aufgetreten ist. Es verhindert auch über externen Zustand zu schließen, da alles als Operand an es übergeben werden muss. Außerdem können wir Bibliotheken wiederverwendbarer -Funktionen erstellen, die die Code-Duplizierung reduzieren.

Wenn das Schreiben von Code Ich nehme einen Schritt weiter als Ihr Beispiel und die Business-Logik in ein separates Paket verschieben:

package businessLogic 

object ProductGetter { 
    def passthroughToysOnly(products : List[Product]) : List[Toy] = 
    products flatMap {p => 
     p.category match { 
     case "toy" => Some(p) 
     case _ => None 
    } 
    } 
} 

Diese für die Änderung der Gleichzeitigkeit Methodik Futures ermöglicht, Java-Threads oder sogar einige noch nicht erstellte Parallelbibliotheken, ohne meine Geschäftslogik umzuformen. Die Business-Logik-Pakete werden zum "Was" des Codes, die Akka-Bibliotheken zum "Wie".

Wenn Sie die Geschäftslogik isolieren, werden alle Empfangsmethoden zu einfachen "Routern" von Nachrichten an externe Funktionen. Wenn Sie also Ihre Geschäftslogik mit Komponententests durchbohren, müssen Sie nur sicherstellen, dass die Fallmuster korrekt übereinstimmen.

Adressierung Ihres spezifischen Problems: Ich würde die getActiveProductsByIds aus dem Actor entfernen. Wenn der Benutzer des Actors nur aktive Produkte erhalten möchte, überlasse es ihm, zuerst die IDs zu filtern. Dein Darsteller sollte nur 1 Sache machen: GetProductById. Zitiert Alle wieder:

Es ist sehr einfach einen Schauspieler machen zusätzlich Aufgaben - wir einfach fügen Sie neue Nachrichten an ihren Empfangsblock, damit sie mehr und verschiedene Arten von Arbeit verrichten. Dies schränkt jedoch Ihre Fähigkeit ein, Systeme von Akteuren zu komponieren und kontextuelle Gruppierungen zu definieren. Halten Sie Ihre Darsteller auf eine einzige Art von Arbeit konzentriert und erlauben Sie so, diese flexibel zu verwenden.

+0

Ich stimme Ihrer Aussage über die Verlagerung der Geschäftslogik zu. Tatsächlich wird in meiner realen Anwendungslogik die Typfilterung für das Product-Objekt selbst (oder in einem anderen Aktor, wenn es mehr Aktorinteraktionen involviert) durchgeführt. Die Szenarien sind Pseudocode konstruiert, um viele Probleme aufzuzeigen, gebe ich zu. Ich stimme zu, dass ein Filter-Fetch zu einem anderen Akteur gehört, aber wenn man sich diese Methode anschaut, die keinen Filter vorstellt, bleibt das Problem bestehen. Wenn ich es einfach verschiebe, um Multi-Fetch zu testen, wird es viele solche Aktoren geben (ProductCollectorActor, ProductFilterActor). Klassen im Wesentlichen nur zur Erleichterung von Tests. – jm0

+0

Im Wesentlichen wird es keinen "Actor-System" -Stil geben, wenn man diese in mehreren Schauspielern hat, es ist nur eine schwerere App mit vielen Akteur-Layern, um einige kleine DAO-Callouts zu erreichen. – jm0

+1

@ jm0 Mein primärer Punkt war, dass, wenn Sie Business-Logik isoliert und gründlich getestet haben, die Actor-Tests ziemlich simpel sein können, im Grunde nur Typ-Prüfung Tests auf der Empfangsmethode. –

0

Zunächst ist dies eine sehr interessante Frage. Akka-Dokumentation ist im Allgemeinen sehr gut, und der Teil testing hat viele aufschlussreiche Notizen auf dem Weg, um häufige Fallstricke zu vermeiden und Best Practices vorzuschlagen.

Neulich war ich reading about this, und fand einen Vorschlag, den ich vorher nicht versuchte: die observer pattern verwendend. Die Idee ist, dass Sie Ihren Actors nur das Messaging überlassen (das Sie nicht testen müssen, das Akka-Team wird es für Sie tun) und Broadcasts an Abonnenten senden. Auf diese Weise wird Ihre Logik vollständig von den Actors isoliert, was das Testen wesentlich vereinfacht.

Hinweis: Ich habe dies nicht in einem Produktionssystem versucht, aber da Sie erwähnt haben, dass nur sehr einfache Dinge in Ihrem Produktionssystem sind, könnte dies einen Versuch wert sein.