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!
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. –
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
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