Vor einiger Zeit habe ich eine Präsentation über Mocking- und Test-Frameworks für Java vorbereitet. Da das Ziel darin bestand, echten, laufenden Code zu demonstrieren, habe ich viel Zeit damit verbracht, verschiedene Beispiele aus Readmes, Javadoc, Wiki-Seiten und Blogbeiträgen zu kopieren, einzufügen, zu erweitern und zu korrigieren. Seitdem wurde diese Codebasis um verschiedene neue Funktionen erweitert, auf die ich gestoßen bin, und ich habe sie oft für Experimente, als hilfreiche Referenz usw. herangezogen. Ich kann mir vorstellen, dass diese Art von "Live"-Referenz auch für andere nützlich sein könnte, also dachte ich, ich teile sie.
Mock Braindump
Zur Einführung hier zunächst ein Braindump verschiedener interessanter Punkte im Zusammenhang mit den Frameworks im Beispiel1. Erwarten Sie keinen detaillierten Überblick oder Vergleich - davon gibt es eine Menge - und für spezifische Fragen zur Funktionsweise der Frameworks lesen Sie bitte deren Dokumentation2.
Mockito oder EasyMock?
EasyMock und Mockito sind derzeit de facto die Standard-Mocking-Frameworks. Ihr Funktionsumfang ist mehr oder weniger identisch, und wenn eines der beiden Frameworks etwas Neues bietet, können Sie ziemlich sicher sein, dass es in der nächsten Version des anderen enthalten sein wird.
Statik, Lokale und Finale: Die letzte Grenze von TDD?
Die Mission von Jmockit und JEasyTest scheint zu sein, "mutig dorthin zu gehen, wo noch kein Mocking-Framework zuvor war". [java] public final class ServiceA { public void doBusinessOperationXyz(EntityX data) throws InvalidItemStatus { List<?> items = Database.find("select item from EntityY item where item.someProperty=?", data.getSomeProperty()); BigDecimal total = new ServiceB().computeTotal(items); data.setTotal(total); Database.save(data); } } public static final class ServiceB { ... [/java] Die Herausforderung besteht darin, diese Klasse zu testen, insbesondere die finden. und Speicheraufrufe auf Database und den computeTotal-Aufruf auf der neuen ServiceB-Instanz. Mit herkömmlichem Mocking ist dies nahezu unmöglich:
- finden und speichern sind statisch
- ServiceB ist eine finale Klasse und kann daher nicht nachgebildet werden.
- selbst wenn es möglich wäre, wird die aufgerufene ServiceB-Instanz in dem zu testenden Code erstellt
Meine unmittelbare Reaktion? Wenn das die Art von Code ist, die Sie testen sollen, haben Sie andere Probleme! Ja, natürlich ist dies ein künstliches Beispiel, das speziell ausgewählt wurde, um die Fälle hervorzuheben, mit denen normales Mocking nicht umgehen kann. Aber selbst wenn der echte Code, mit dem Sie Probleme haben, nur einen dieser Fälle enthält, würde ich in Erwägung ziehen, den Code zu überarbeiten, bevor ich mich nach einem anderen Test-Framework umsehe. Dependency Injection mag zu einer Art Religion geworden sein, aber sie ist nicht umsonst so weit verbreitet. Für Code, der mit DI arbeitet, sind die Standard-Mocking-Frameworks fast immer ausreichend. Aber selbst wenn man vielleicht nicht versuchen sollte, das Beispiel zu testen, könnte man es doch? Nun, die Erfahrung zeigt, dass es nur wenige Probleme gibt, die nicht mit einer ausreichend großen Portion Bytecode-Manipulation gelöst werden können. JEasyTest arbeitet mit seiner Art von Magie, indem es vor dem Test einen Schritt des Webens durchführt, der von einem Eclipse-Plugin oder durch Hinzufügen eines Plugins zu Ihrem Maven-Build3. Jmockit verwendet den etwas moderneren Instrumentierungsansatz und wird mit einem Java-Agenten geliefert, was bedeutet, dass es nur ein zusätzliches VM-Argument benötigt, um in Ihrer IDE zu laufen. Abgesehen von den Problemen mit der Benutzerfreundlichkeit habe ich festgestellt, dass ich mit dem Code zur Vorbereitung einer Testvorrichtung und der Registrierung von Erwartungen in beiden Frameworks nicht zufrieden bin; in einigen Fällen ist er geradezu unbeholfen. Hier ist der JEasyTest-Test: [java] @JEasyTest public void testBusinessOperation() throws InvalidItemStatus { on(Database.class).expectStaticNonVoidMethod("find").with( arg("select item from EntityY item where item.someProperty=?"), arg("abc")).andReturn(Collections.EMPTY_LIST); on(ServiceB.class).expectEmptyConstructor().andReturn(serviceB); on(Database.class).expectStaticVoidMethod("save").with(arg(entity)); expect(serviceB.computeTotal(Collections.EMPTY_LIST)).andReturn(total); replay(serviceB); serviceA.doBusinessOperationXyz(entity); verify(serviceB); assertEquals(total, entity.getTotal()); } [/java] expectStaticNonVoidMethod? ARGH! Fühlt sich mehr nach ASM als nach Unit Testing an. Der "Erwartungs"-Modus von Jmockit kommt dem am nächsten, womit Mockito/EasyMock-Benutzer wahrscheinlich vertraut sind4: [java] @MockField private final Datenbank unbenutzt = null; @MockField private ServiceB serviceB; @Test public void doBusinessOperationXyz() throws Exception { EntityX data = new EntityX(); BigDecimal total = new BigDecimal("125.40"); List<?> items = new ArrayList<Objekt>(); Database.find(withSubstring("select"), withAny("")); returns(items); new ServiceB().computeTotal(items); returns(total); Database.save(data); endRecording(); new ServiceA().doBusinessOperationXyz(data); assertEquals(total, data.getTotal()); } [/java] Vom Konzept her erinnert dies an einen EasyMock-Test mit Aufzeichnungs- und Wiedergabephasen. Die Felder tt> @MockField </tt stehen für die Erstellung tatsächlicher Mock-Objekte: Die Felddeklarationen zeigen Jmockit nur an, dass Mocks der angegebenen Typen benötigt werden, wenn der Test ausgeführt wird, und überladen die Testklasse mit ungenutzten Eigenschaften. Außerdem sind die Methoden der "Mock-Verwaltung"(withAny, returns usw.) nicht statisch, d.h. sie sind nicht visuell gekennzeichnet, indem sie z.B. kursiv dargestellt werden. Ich war überrascht, wie sehr mich diese scheinbar unbedeutende Diskrepanz befremdet hat - es sieht einfach nicht wie ein Unit-Test aus.
JMock
Soweit ich sehen kann, passiert nicht mehr viel rund um jMock: die letzte Veröffentlichung war im August 2008 und die letzte Newsmeldung vor über einem halben Jahr. Die Syntax, die versucht, eine pseudo-"natürlichsprachliche" DSL zu imitieren, ist einfach zu umständlich. Die Unterstützung von jMock für Multithreading hat mich dazu veranlasst, einen genaueren Blick darauf zu werfen, aber es ist eigentlich nur ein Mechanismus, der sicherstellt, dass Assertion-Fehler, die in anderen Threads ausgelöst werden, tatsächlich vom Test-Thread registriert werden; es gibt keine Unterstützung für das Testen von gleichzeitigem Verhalten.
Testen von gleichzeitigem Code
Ich mag MultithreadedTC, ein kleines Framework5, das das Starten und Koordinieren mehrerer Test-Threads erleichtern soll. Dies geschieht mit Hilfe einer globalen "Uhr", die sich immer dann vorwärts bewegt, wenn alle Threads blockiert sind - entweder "natürlich" (z.B. während eines Aufrufs wie blockingQueue.take() ) oder absichtlich mit einem waitForTick(n) Befehl. Als solches bietet MultithreadedTC nicht viel mehr als "manuelle" Verriegelungen, wie sie in dem kürzlich erschienenen Blogbeitrag von Iwein Fuld beschrieben wurden, aber die Uhr-Metapher scheint den Testablauf leichter verständlich zu machen, insbesondere bei längeren Tests. Wie bei der Verriegelung besteht das Hauptproblem bei MultithreadedTC jedoch darin, dass Sie die Ausführung von Code in den zu testenden Klassen nicht einfach kontrollieren können. [java] public void thread1() throws InterruptedException { ... waitForTick(1); service.someMethod(); waitForTick(2); ... } public void thread2() throws InterruptedException { ... waitForTick(1); service.otherMethod(); waitForTick(2); ... } [/java] Dieser Code stellt sicher, dass service.someMethod() und service.otherMethod() fast zur gleichen Zeit starten und garantiert, dass keiner der beiden Threads fortgesetzt wird, bevor beide Methoden abgeschlossen sind. Aber was ist, wenn Sie sicherstellen wollen, dass die Hälfte von someMethod abgeschlossen ist, bevor otherMethod aufgerufen wird? Dafür müssen Sie in der Lage sein, auf die Implementierungen von someMethod und otherMethod zuzugreifen, z.B. indem Sie die Service-Implementierungen subklassifizieren oder etwas wie Byteman verwenden. Letztendlich denke ich jedoch, dass Unit-Tests einfach nicht der richtige Weg sind, um nebenläufigen Code zu testen. Das "Choreographieren" der sorgfältig ausgewählten Aktionen einer kleinen Anzahl von Test-Threads ist ein schlechter Ersatz für eine echte gleichzeitige Nutzung, und die Fehler, die Sie finden werden, wenn überhaupt, sind nicht die Art von Nebenläufigkeitsproblemen, die am Ende Alpträume verursachen. Für richtige Nebenläufigkeitstests scheint es bisher keinen guten Ersatz dafür zu geben, eine ganze Reihe von Threads zu starten - auf so vielen Kernen wie möglich - und sie eine ganze Weile laufen zu lassen (siehe z.B. die Integrationstests von Multiverse, dem Java STM). Wenn es möglich ist, ein gewisses Maß an Zufälligkeit in das Timing einzubringen (z.B. mit Byteman), umso besser!
JUnit Rocks regiert!
Mit Version 4.7 von JUnit wurden Regeln eingeführt, die sich um Aspekte drehen, die vor und nach der Ausführung eines Tests aufgerufen werden. Einige der Standardbeispiele demonstrieren "haushaltsbezogene" Funktionen wie das Öffnen und Schließen einer Ressource oder das Erstellen und Aufräumen eines temporären Ordners. Regeln können sich aber auch auf das Ergebnis eines Tests auswirken, z.B. indem sie ihn zum Scheitern bringen, selbst wenn alle Testaussagen erfolgreich waren. Obwohl man sehen kann, wie nützlich das Konzept der Regeln sein kann, haben sie immer noch etwas von der "v1"-Rauheit an sich. Die "Housekeeping"-Regeln sind im Wesentlichen bequeme Ersetzungen für die Logik tt>@Before </tt/tt>@After</tt, und die Syntax der "testbeeinflussenden" Regeln wirkt unübersichtlich: [java] public class UsesErrorCollectorTwice { @Rule public ErrorCollector collector = new ErrorCollector(); @Test public void example() { collector.addError(new Throwable("erste Sache ging schief")); collector.addError(new Throwable("die zweite Sache ist schief gelaufen")); collector.checkThat("ERROR", not("ERROR")); collector.checkThat("OK", not("ERROR")); System.out.println("Hier ist es!"); [/java] } } Wäre es nicht schöner, wenn die Assertions ein wenig mehr, ähm, Assert-artig wären? Oder nehmen Sie: [java] public class HasExpectedException { @Rule public ExpectedException thrown = ExpectedException.none(); @Test public void throwsNullPointerExceptionWithMessage() { thrown.expect(NullPointerException.class); thrown.expectMessage("happened?"); thrown.expectMessage(startsWith("What")); throw new NullPointerException("What happened?"); } } [/java] Ich meine, es ist sicherlich nützlich, detailliertere Aussagen über Ausnahmen machen zu können, aber könnte dies nicht in eines der aktuellen Muster für die Ausnahmeprüfung6 integriert werden? Die potenzielle Macht der Regeln wirft auch eine Frage auf: Ist es klug, sich anzugewöhnen, in einem Unit-Test ein umfassendes Ressourcenmanagement durchzuführen (z.B. das Starten von Servern oder DB-Verbindungen)?
- Beispielhafter Quellcode hier. Probieren Sie das Projekt mit svn checkout https://aphillips.googlecode.com/svn/mock-poc/trunk target-folder aus.
- Siehe das POM des Beispielcodes.
- Das heißt natürlich nicht, dass das die beste Methode ist!
- Der ursprüngliche Code wird nicht mehr aktiv weiterentwickelt, aber es gab in letzter Zeit einige Arbeiten, die auf eine bessere Integration von JUnit 4 abzielten.
-
@Test public void tryCatchTestForException() { versuchen { throw new NullPointerException(); fail(); } catch (NullPointerException exception) { // erwartet } } @Test(erwartet = NullPointerException.class) public void annotationTestForException() { throw new NullPointerException(); }
Verfasst von
Andrew Phillips
Unsere Ideen
Weitere Blogs
Contact



