Im Gegensatz zu vielen Plattformen leidet Java unter einem Mangel an Verbindungsstubbibliotheken. Wenn Sie schon lange auf dieser Welt sind, sollten Sie wahrscheinlich mit WireMock, Betamax oder sogar Spock vertraut sein. Viele Entwickler in den Tests verwenden Mockito, um das Verhalten von Objekten zu beschreiben. DataJpaTest mit einer lokalen h2-Datenbank, Gurkentests. Heute werden Sie eine leichte Alternative kennenlernen, die Ihnen hilft, mit verschiedenen Problemen umzugehen, die bei der Verwendung dieser Ansätze auftreten können. Insbesondere versucht anyStub, die folgenden Probleme zu lösen:
- Vereinfachen Sie die Konfiguration der Testumgebung
- Automatisieren Sie die Datenerfassung für Tests
- Bleiben Sie beim Testen Ihrer Anwendung und vermeiden Sie das Testen auf etwas anderes
Was ist anyStub und wie funktioniert es?
AnyStub schließt Funktionsaufrufe ab und versucht, übereinstimmende Anrufe zu finden, die bereits aufgezeichnet wurden. Damit können zwei Dinge passieren:
- Wenn ein übereinstimmender Aufruf vorliegt, stellt anyStub das mit diesem Aufruf verknüpfte aufgezeichnete Ergebnis wieder her und gibt es zurück
- Wenn kein übereinstimmender Anruf vorliegt und der Zugriff auf das externe System zulässig ist, führt anyStub diesen Anruf durch, zeichnet dieses Ergebnis auf und gibt es zurück
AnyStub bietet standardmäßig Wrapper für den http-Client von Apache HttpClient zum Erstellen von Stubs für http-Anforderungen und mehrere Schnittstellen von javax.sql. * Für DB-Verbindungen. Sie erhalten auch eine API zum Erstellen von Stubs für andere Verbindungen.
AnyStub ist eine einfache Klassenbibliothek und erfordert keine spezielle Konfiguration Ihrer Umgebung. Diese Bibliothek ist für die Arbeit mit Spring-Boot-Anwendungen vorgesehen. Wenn Sie diesem Pfad folgen, erzielen Sie den größtmöglichen Nutzen. Sie können es außerhalb von Spring in einfachen Java-Anwendungen verwenden, aber auf jeden Fall müssen Sie zusätzliche Arbeit leisten. Die folgende Beschreibung konzentriert sich auf das Testen von Spring-Boot-Anwendungen.
Schauen wir uns die Integrationstests an. Dies ist die aufregendste und umfassendste Möglichkeit, Ihr System zu testen. Tatsächlich erledigen Spring-Boot und JUnit fast alles für Sie, wenn Sie magische Anmerkungen schreiben:
@RunWith(SpringRunner.class) @SpringBootTest
Derzeit werden Integrationstests unterschätzt und in begrenztem Umfang verwendet, und einige Entwickler vermeiden sie. Dies ist hauptsächlich auf die zeitaufwändige Vorbereitung und Wartung von Tests oder die Notwendigkeit einer speziellen Konfiguration der Umgebung auf Build-Servern zurückzuführen.
Mit anyStub müssen Sie den Federkontex nicht lähmen. Stattdessen ist es einfach und unkompliziert, den Kontext nahe an der Produktionskonfiguration zu halten.
In diesem Beispiel wird anhand des Handbuchs von Pivotal gezeigt, wie Sie anyStub mit einem Consuming a RESTful-Webdienst verbinden.
Verbinden einer Bibliothek über pom.xml
<dependency> <groupId>org.anystub</groupId> <artifactId>anystub</artifactId> <version>0.2.27</version> <scope>test</scope> </dependency>
Der nächste Schritt besteht darin, den Federkontext zu ändern.
package hello; import org.anystub.http.StubHttpClient; import org.apache.http.client.HttpClient; import org.apache.http.impl.client.HttpClientBuilder; import org.springframework.boot.web.client.RestTemplateBuilder; import org.springframework.boot.web.client.RestTemplateCustomizer; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; import org.springframework.web.client.RestTemplate; @Configuration public class TestConfiguration { @Bean public RestTemplateBuilder builder() { RestTemplateCustomizer restTemplateCustomizer = new RestTemplateCustomizer() { @Override public void customize(RestTemplate restTemplate) { HttpClient real = HttpClientBuilder.create().build(); StubHttpClient stubHttpClient = new StubHttpClient(real); HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory(); requestFactory.setHttpClient(stubHttpClient); restTemplate.setRequestFactory(requestFactory); } }; return new RestTemplateBuilder(restTemplateCustomizer); } }
Diese Änderung ändert nicht die Komponentenbeziehungen in der Anwendung, sondern ersetzt nur die Implementierung einer einzelnen Schnittstelle. Dies führt uns zum Barbara-Lisk-Substitutionsprinzip . Wenn das Design Ihrer Anwendung nicht dagegen verstößt, wird durch diese Ersetzung die Funktionalität nicht verletzt.
Alles ist fertig. Dieses Projekt enthält bereits einen Test.
@RunWith(SpringRunner.class) @SpringBootTest public class ApplicationTest { @Autowired private RestTemplate restTemplate; @Test public void contextLoads() { assertThat(restTemplate).isNotNull(); } }
Dieser Test ist leer, führt jedoch bereits den Anwendungskontext aus. Der Spaß beginnt hier . Wie oben erwähnt, stimmt der Anwendungskontext im Test mit dem Arbeitskontext überein, in dem der CommandLineRunner erstellt wird, in dem die http-Anforderung an das externe System ausgeführt wird.
@SpringBootApplication public class Application { private static final Logger log = LoggerFactory.getLogger(Application.class); public static void main(String args[]) { SpringApplication.run(Application.class); } @Bean public RestTemplate restTemplate(RestTemplateBuilder builder) { return builder.build(); } @Bean public CommandLineRunner run(RestTemplate restTemplate) throws Exception { return args -> { Quote quote = restTemplate.getForObject( "https://gturnquist-quoters.cfapps.io/api/random", Quote.class); log.info(quote.toString()); }; } }
Dies reicht aus, um die Funktionsweise der Bibliothek zu demonstrieren. Nach dem ersten Start der Tests finden Sie die neue complete/src/test/resources/anystub/stub.yml
.
request0: exception: [] keys: [GET, HTTP/1.1, 'https://gturnquist-quoters.cfapps.io/api/random'] values: [HTTP/1.1, '200', OK, 'Content-Type: application/json;charset=UTF-8', 'Date: Thu, 25 Apr 2019 23:04:49 GMT', 'X-Vcap-Request-Id: 5ffce9f3-d972-4e95-6b5c-f88f9b0ae29b', 'Content-Length: 177', 'Connection: keep-alive', '{"type":"success","value":{"id":3,"quote":"Spring has come quite a ways in addressing developer enjoyment and ease of use since the last time I built an application using it."}}']
Was ist passiert? spring-boot hat RestTemplateBuilder aus der Testkonfiguration in die Anwendung integriert. Dies führte dazu, dass die Anwendung die Stub-Implementierung des http-Clients durcharbeitete. StubHttpClient hat die Anforderung abgefangen, die Stub-Datei nicht gefunden, die Anforderung ausgeführt, das Ergebnis in einer Datei gespeichert und das aus der Datei wiederhergestellte Ergebnis zurückgegeben.
Von nun an können Sie diesen Test ohne Internetverbindung ausführen, und diese Anforderung ist erfolgreich. restTemplate.getForObject()
gibt das gleiche Ergebnis zurück. Auf diese Tatsache können Sie sich bei Ihren zukünftigen Tests verlassen.
Sie finden alle beschriebenen Änderungen auf GitHub .
Tatsächlich haben wir noch keinen einzigen Test erstellt. Bevor wir Tests schreiben, schauen wir uns an, wie es mit Datenbanken funktioniert.
In diesem Beispiel fügen wir einen Integrationstest zum Zugriff auf relationale Daten mit JDBC mit Spring aus dem Pivotal-Lernprogramm hinzu.
Die Testkonfiguration für diesen Fall sieht folgendermaßen aus:
package hello; import org.anystub.jdbc.StubDataSource; import org.h2.jdbcx.JdbcDataSource; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import javax.sql.DataSource; @Configuration public class TestConfiguration { @Bean public DataSource dataSource() { JdbcDataSource ds = new JdbcDataSource(); ds.setURL("jdbc:h2:./test"); return new StubDataSource(ds); } }
Hier wird eine reguläre Datenquelle für eine externe Datenbank erstellt und mit einer Stub-Implementierung - der StubDataSource-Klasse - umschlossen. Spring-Boot bettet es in den Kontext ein. Wir müssen auch mindestens einen Test erstellen, um den Federkontext im Test auszuführen.
package hello; import org.anystub.AnyStubId; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.junit4.SpringRunner; import static org.junit.Assert.*; @RunWith(SpringRunner.class) @SpringBootTest public class ApplicationTest { @Test @AnyStubId public void test() { } }
Dies ist wieder ein leerer Test - seine einzige Aufgabe besteht darin, den Anwendungskontext auszuführen. Hier sehen wir eine sehr wichtige Anmerkung @AnystubId
, die jedoch noch nicht beteiligt ist.
Nach dem ersten Durchlauf finden Sie eine neue src/test/resources/anystub/stub.yml
, die alle Datenbankaufrufe enthält. Sie werden überrascht sein, wie der Frühling hinter den Kulissen mit Datenbanken funktioniert. Beachten Sie, dass neue Testläufe nicht zu einem echten Zugriff auf die Datenbank führen. Wenn Sie test.mv.db löschen, wird es nach wiederholten Testläufen nicht angezeigt. Alle Änderungen können auf GitHub angezeigt werden.
Zusammenfassend. mit anyStub:
- Sie müssen keine spezielle Testumgebung einrichten
- Das Testen wird mit realen Daten durchgeführt
- Der erste Testlauf bestätigt Ihre Annahmen und speichert die Testdaten. Die folgenden Tests überprüfen, ob das System nicht beeinträchtigt wurde
Sie haben wahrscheinlich Fragen: Wie werden Fälle abgedeckt, in denen die Datenbank noch nicht vorhanden ist, was mit negativen Tests und der Ausnahmebehandlung zu tun ist. Wir werden darauf zurückkommen, aber zuerst werden wir uns mit dem Schreiben einfacher Tests befassen.
Wir experimentieren jetzt mit dem Konsumieren eines RESTful-Webdienstes . Dieses Projekt enthält keine Komponenten, die getestet werden können. Im Folgenden werden zwei Klassen erstellt, die zwei Ebenen eines Architekturdesigns darstellen sollen.
package hello; import org.springframework.stereotype.Component; import org.springframework.web.client.RestTemplate; @Component public class DataProvider { private final RestTemplate restTemplate; public DataProvider(RestTemplate restTemplate) { this.restTemplate = restTemplate; } Quote provideData() { return restTemplate.getForObject( "https://gturnquist-quoters.cfapps.io/api/random", Quote.class); } }
DataProvider bietet Zugriff auf Daten in flüchtig externes System.
package hello; import org.springframework.stereotype.Component; @Component public class DataProcessor { private final DataProvider dataProvider; public DataProcessor(DataProvider dataProvider) { this.dataProvider = dataProvider; } int processData() { return dataProvider.provideData().getValue().getQuote().length(); } }
DataProcessor verarbeitet Daten von einem externen System.
Wir beabsichtigen, den DataProcessor
zu testen. Es ist notwendig, die Richtigkeit des Verarbeitungsalgorithmus zu testen und das System vor einer Verschlechterung durch zukünftige Änderungen zu schützen.
Um diese Ziele zu erreichen, können Sie ein DataProvider-Scheinobjekt mit einem Datensatz erstellen und in Tests an den DataProcessor-Konstruktor übergeben. Eine andere Möglichkeit könnte darin bestehen, den DataProcessor zu zerlegen, um die Verarbeitung der Quote-Klasse hervorzuheben. Dann ist eine solche Klasse einfach mit Komponententests zu testen (dies ist sicherlich die empfohlene Methode in angesehenen Büchern über sauberen Code). Versuchen wir, Codeänderungen und die Erfindung von Testdaten zu vermeiden und schreiben Sie einfach einen Test.
@RunWith(SpringRunner.class) @SpringBootTest public class DataProcessorTest { @Autowired private DataProcessor dataProcessor; @Test @AnyStubId(filename = "stub") public void processDataTest() { assertEquals(131, dataProcessor.processData()); } }
Zeit, über die Annotation @AnystubId zu sprechen. Diese Anmerkung hilft beim Verwalten und Steuern von Stub-Dateien in Tests. Es kann mit einer Testklasse oder ihrer Methode verwendet werden. Diese Anmerkung erstellt eine individuelle Stub-Datei für den entsprechenden Bereich. Wenn ein Bereich gleichzeitig von Annotationen auf Klassen- und Methodenebene abgedeckt wird, hat die Annotation der Methode Vorrang. Diese Anmerkung enthält den Parameter Dateiname, der den Namen der Stub-Datei definiert. Die Erweiterung ".yml" wird automatisch hinzugefügt, wenn sie weggelassen wird. Wenn Sie diesen Test ausführen, finden Sie keine neue Datei. Die src/test/resources/anystub/stub.yml
wurde bereits früher erstellt und wird von diesem Test wiederverwendet. Wir haben die Nummer 131 von diesem Stub erhalten, indem wir das Ergebnis der Abfrage analysiert haben.
@Test @AnyStubId public void processDataTest2() { assertEquals(131, dataProcessor.processData()); Base base = getStub(); assertEquals(1, base.times("GET")); assertTrue(base.history().findFirst().get().matchEx_to(null, null, ".*gturnquist-quoters.cfapps.io.*")); }
In diesem Test wird die Annotation @AnyStubId ohne den Parameter Dateiname angezeigt. In diesem Fall wird die src/test/resources/anystubprocessDataTest2.yml
. Der Dateiname wird aus dem Namen der Funktion (Klasse) + ".yml" erstellt. Sobald anyStub eine neue Datei für diesen Test erstellt, müssen Sie einen echten Systemaufruf durchführen. Und es ist unser Glück, dass das neue Zitat die gleiche Länge hat. Die letzten beiden Überprüfungen zeigen, wie das Verhalten der Anwendung getestet wird. Es steht Ihnen zur Verfügung: Abfragen nach Parametern oder Teilen von Parametern auswählen und die Anzahl der Abfragen zählen. In der Dokumentation finden Sie verschiedene Variationen der Zeiten und Übereinstimmungsfunktionen .
@Test @AnyStubId(requestMode = RequestMode.rmTrack) public void processDataTest3() { assertEquals(79, dataProcessor.processData()); assertEquals(79, dataProcessor.processData()); assertEquals(168, dataProcessor.processData()); assertEquals(79, dataProcessor.processData()); Base base = getStub(); assertEquals(4, base.times("GET")); }
In diesem Test wird @AnyStubId mit dem neuen Parameter requestMode angezeigt. Sie können damit Berechtigungen für Stub-Dateien verwalten. Es gibt zwei Aspekte, die gesteuert werden müssen: Dateisuche und Berechtigung zum Aufrufen eines externen Systems.
RequestMode.rmTrack
legt die folgenden Regeln fest: Wenn die Datei gerade erstellt wurde, werden alle Anforderungen an das externe System gesendet und mit den Antworten in die Datei geschrieben, unabhängig davon, ob die Datei eine identische Anforderung enthält (Duplikate in der Datei sind zulässig). Wenn vor dem Ausführen der Tests die Stub-Datei vorhanden ist, sind Anforderungen an das externe System verboten. Anrufe werden in genau derselben Reihenfolge erwartet. Wenn die nächste Anforderung nicht mit der Anforderung in der Datei übereinstimmt, wird eine Ausnahme ausgelöst.
RequestMode.rmNew
dieser Modus ist standardmäßig aktiviert. Jede Anforderung wird in der Stub-Datei durchsucht. Wird eine übereinstimmende Anforderung gefunden - das entsprechende Ergebnis wird aus der Datei wiederhergestellt, wird die Anforderung an das externe System verschoben. Wird die Anforderung nicht gefunden, wird das externe System angefordert und das Ergebnis in einer Datei gespeichert. Doppelte Anforderungen in der Datei - treten nicht auf.
RequestMode.rmNone
Jede Anfrage wird in einer Stub-Datei nachgeschlagen. Wenn eine übereinstimmende Abfrage gefunden wird, wird das Ergebnis aus der Datei wiederhergestellt. Wenn der Test eine Anforderung generiert, die nicht in der Datei enthalten ist, wird eine Ausnahme ausgelöst.
RequestMode.rmAll
vor der ersten Anforderung wird die Stub-Datei gelöscht. Alle Anforderungen werden in die Datei geschrieben (Duplikate in der Datei sind zulässig). Sie können diesen Modus verwenden, wenn Sie die Verbindungsarbeit beobachten möchten.
RequestMode.rmPassThrough
alle Anforderungen werden direkt an das externe System gesendet, wobei der Implementierungsstub umgangen wird.
Diese Änderungen sind auf GitHub verfügbar .
Was sonst?
Wir haben gesehen, wie anyStub Antworten speichert. Wenn beim Zugriff auf ein externes System eine Ausnahme ausgelöst wird, speichert anyStub diese und spielt sie bei nachfolgenden Anforderungen ab.
Oft werden Ausnahmen von Klassen der obersten Ebene ausgelöst, während Verbindungsklassen eine gültige Antwort erhalten (wahrscheinlich mit einem Fehlercode). In diesem Fall ist anyStub dafür verantwortlich, die Antwort mit dem Fehlercode zu reproduzieren, und die übergeordneten Klassen lösen auch Ausnahmen für Ihre Tests aus.
Fügen Sie dem Repository Stub-Dateien hinzu.
Haben Sie keine Angst, Stub-Dateien zu löschen und zu überschreiben.
Verwalten Sie Stub-Dateien mit Bedacht. Sie können eine Datei in mehreren Tests wiederverwenden oder für jeden Test eine eigene Datei bereitstellen. Nutzen Sie diese Gelegenheit für Ihre Bedürfnisse. Normalerweise ist es jedoch eine schlechte Idee, eine einzelne Datei mit unterschiedlichen Zugriffsmodi zu verwenden.
Dies sind alle Hauptfunktionen von anyStub.