Spring-Integrationstests optimieren

Frühlingstests optimieren

1. Einführung

In diesem Artikel werden wir eine ganzheitliche Diskussion über Integrationstests mit Spring und deren Optimierung führen.

Zunächst werden wir kurz auf die Bedeutung von Integrationstests und ihren Platz in moderner Software mit Schwerpunkt auf dem Spring-Ökosystem eingehen.

Später werden wir mehrere Szenarien behandeln, wobei wir uns auf Web-Apps konzentrieren.

Next, we’ll discuss some strategies to improve testing speed, indem Sie verschiedene Ansätze kennenlernen, die sowohl die Art und Weise, wie wir unsere Tests gestalten, als auch die Art und Weise, wie wir die App selbst gestalten, beeinflussen können.

Bevor Sie beginnen, sollten Sie bedenken, dass es sich um einen Erfahrungsbericht handelt. Einige dieser Dinge könnten zu Ihnen passen, andere nicht.

Schließlich verwendet dieser Artikel Kotlin für die Codebeispiele, um sie so kurz wie möglich zu halten. Die Konzepte sind jedoch nicht spezifisch für diese Sprache, und Codefragmente sollten für Java- und Kotlin-Entwickler gleichermaßen von Bedeutung sein.

2. Integrationstests

Integration tests are a fundamental part of automated test suites. Obwohl sie nicht so zahlreich sein sollten wie Unit-Tests, wenn wir ahealthy test pyramid folgen. Wenn wir uns auf Frameworks wie Spring verlassen, müssen wir eine Menge Integrationstests durchführen, um bestimmte Verhaltensweisen unseres Systems zu reduzieren.

The more we simplify our code by using Spring modules (data, security, social…), the bigger a need for integration tests. Dies gilt insbesondere dann, wenn wir Teile unserer Infrastruktur in die Klassen von@Configurationverschieben.

Wir sollten das Framework nicht "testen", aber wir sollten auf jeden Fall überprüfen, ob das Framework so konfiguriert ist, dass es unsere Anforderungen erfüllt.

Integrationstests helfen uns, Vertrauen aufzubauen, haben aber ihren Preis:

  • Das ist eine langsamere Ausführungsgeschwindigkeit, was langsamere Builds bedeutet

  • Integrationstests implizieren auch einen breiteren Testumfang, der in den meisten Fällen nicht ideal ist

Vor diesem Hintergrund werden wir versuchen, Lösungen zu finden, um die oben genannten Probleme zu lösen.

3. Testen von Web-Apps

Spring bietet einige Optionen zum Testen von Webanwendungen, mit denen die meisten Spring-Entwickler vertraut sind:

  • MockMvc: Verspottet die Servlet-API, die für nicht reaktive Webanwendungen nützlich ist

  • TestRestTemplate: Kann verwendet werden, um auf unsere App zu verweisen. Dies ist nützlich für nicht reaktive Web-Apps, bei denen verspottete Servlets nicht erwünscht sind

  • WebTestClient: Ist ein Testtool für reaktive Webanwendungen, sowohl mit verspotteten Anforderungen / Antworten als auch mit einem echten Server

Da wir bereits Artikel zu diesen Themen haben, werden wir keine Zeit damit verbringen, darüber zu sprechen.

Schauen Sie doch einfach mal rein, wenn Sie tiefer graben möchten.

4. Ausführungszeit optimieren

Integrationstests sind großartig. Sie geben uns ein gutes Maß an Vertrauen. Auch wenn sie richtig implementiert sind, können sie die Absicht unserer App auf sehr klare Weise beschreiben, mit weniger Verspottungs- und Einrichtungsgeräuschen.

Wenn unsere App jedoch ausgereift ist und sich die Entwicklung beschleunigt, nimmt die Build-Zeit zwangsläufig zu. Mit zunehmender Erstellungszeit kann es unpraktisch werden, alle Tests jedes Mal auszuführen.

Danach Auswirkung auf unsere Feedbackschleife und Einführung in die besten Entwicklungsmethoden.

Darüber hinaus sind Integrationstests von Natur aus teuer. Das Starten einer Persistenz, das Senden von Anforderungen (auch wenn sielocalhost nie verlassen) oder das Ausführen von E / A-Vorgängen dauert einfach einige Zeit.

Es ist von größter Bedeutung, unsere Erstellungszeit im Auge zu behalten, einschließlich der Testausführung. Und es gibt einige Tricks, die wir im Frühjahr anwenden können, um sie niedrig zu halten.

In den nächsten Abschnitten werden wir einige Punkte behandeln, um unsere Erstellungszeit zu optimieren, sowie einige Fallstricke, die sich auf die Geschwindigkeit auswirken können:

  • Profile mit Bedacht einsetzen - wie sich Profile auf die Leistung auswirken

  • Wenn Sie@MockBean – erneut überdenken, wird die Leistung der verspotteten Treffer angezeigt

  • Refactoring@MockBean  - Alternativen zur Verbesserung der Leistung

  • Überlegen Sie genau, ob @DirtiesContext – eine nützliche, aber gefährliche Anmerkung ist und wie Sie sie nicht verwenden können

  • Verwenden von Testschnitten - ein cooles Tool, das uns helfen oder auf den Weg bringen kann

  • Verwenden der Klassenvererbung - eine Möglichkeit, Tests auf sichere Weise zu organisieren

  • State Management - bewährte Verfahren zur Vermeidung von Flockentests

  • Refactoring in Unit-Tests - der beste Weg, um eine solide und bissige Build zu erhalten

Lass uns anfangen!

4.1. Profile mit Bedacht einsetzen

Profiles sind ein ziemlich ordentliches Werkzeug. Einfache Tags, mit denen bestimmte Bereiche unserer App aktiviert oder deaktiviert werden können. Wir könnten sogarimplement feature flags mit ihnen!

Wenn unsere Profile umfangreicher werden, ist es verlockend, ab und zu in unseren Integrationstests zu tauschen. Hierfür gibt es praktische Tools wie@ActiveProfiles. every time we pull a test with a new profile, a new ApplicationContext gets created.

Das Erstellen von Anwendungskontexten kann mit einer Vanille-Spring-Boot-App, in der sich nichts befindet, schnell erledigt werden. Fügen Sie ein ORM und einige Module hinzu und es wird schnell auf 7+ Sekunden explodieren.

Fügen Sie eine Reihe von Profilen hinzu und verteilen Sie sie auf einige Tests. Wir erhalten schnell einen Build von mehr als 60 Sekunden (vorausgesetzt, wir führen Tests als Teil unseres Builds aus - und das sollten wir auch).

Sobald wir mit einer ausreichend komplexen Anwendung konfrontiert sind, ist die Behebung dieser Probleme einschüchternd. Wenn wir jedoch im Voraus sorgfältig planen, wird es trivial, eine vernünftige Bauzeit einzuhalten.

Es gibt ein paar Tricks, die wir bei Profilen in Integrationstests beachten könnten:

  • Erstellen Sie ein aggregiertes Profil, d. H. test, enthalten alle benötigten Profile - halten Sie sich überall an unser Testprofil

  • Entwerfen Sie unsere Profile mit Blick auf die Testbarkeit. Wenn wir am Ende das Profil wechseln müssen, gibt es vielleicht einen besseren Weg

  • Geben Sie unser Testprofil an einem zentralen Ort an - wir werden später darüber sprechen

  • Vermeiden Sie es, alle Profilkombinationen zu testen. Alternativ könnten wir eine e2e-Testsuite pro Umgebung haben, die die App mit diesem spezifischen Profilsatz testet

4.2. Die Probleme mit@MockBean

@MockBean ist ein ziemlich mächtiges Werkzeug.

Wenn wir etwas Frühlingsmagie brauchen, aber eine bestimmte Komponente verspotten wollen, ist@MockBean sehr praktisch. Aber das zu einem Preis.

Every time @MockBean appears in a class, the ApplicationContext cache gets marked as dirty, hence the runner will clean the cache after the test-class is done. Dies fügt unserem Build erneut einige Sekunden hinzu.

Dies ist umstritten, aber der Versuch, die eigentliche App zu verwenden, anstatt sich über dieses spezielle Szenario lustig zu machen, könnte helfen. Natürlich gibt es hier keine Silberkugel. Grenzen verschwimmen, wenn wir uns nicht erlauben, Abhängigkeiten zu verspotten.

Wir könnten denken: Warum sollten wir bestehen bleiben, wenn wir nur unsere REST-Schicht testen wollen? Dies ist ein fairer Punkt, und es gibt immer einen Kompromiss.

Unter Berücksichtigung einiger Prinzipien kann dies jedoch tatsächlich zu einem Vorteil werden, der zu einem besseren Design sowohl der Tests als auch unserer App führt und die Testzeit verkürzt.

4.3. Refactoring@MockBean

In diesem Abschnitt werden wir versuchen, einen "langsamen" Test mit@MockBean umzugestalten, damit die zwischengespeichertenApplicationContext wiederverwendet werden.

Nehmen wir an, wir möchten einen POST testen, der einen Benutzer erstellt. Wenn wir uns über@MockBean lustig machen, können wir einfach überprüfen, ob unser Dienst mit einem gut serialisierten Benutzer aufgerufen wurde.

Wenn wir unseren Service richtig getestet haben, sollte dieser Ansatz ausreichen:

class UsersControllerIntegrationTest : AbstractSpringIntegrationTest() {

    @Autowired
    lateinit var mvc: MockMvc

    @MockBean
    lateinit var userService: UserService

    @Test
    fun links() {
        mvc.perform(post("/users")
          .contentType(MediaType.APPLICATION_JSON)
          .content("""{ "name":"jose" }"""))
          .andExpect(status().isCreated)

        verify(userService).save("jose")
    }
}

interface UserService {
    fun save(name: String)
}

Wir wollen jedoch@MockBean vermeiden. Am Ende bleiben wir also bei der Entität (vorausgesetzt, der Dienst leistet dies).

Der naivste Ansatz wäre hier, die Nebenwirkung zu testen: Nach dem POST befindet sich mein Benutzer in meiner Datenbank. In unserem Beispiel würde dies JDBC verwenden.

Dies verletzt jedoch die Testgrenzen:

@Test
fun links() {
    mvc.perform(post("/users")
      .contentType(MediaType.APPLICATION_JSON)
      .content("""{ "name":"jose" }"""))
      .andExpect(status().isCreated)

    assertThat(
      JdbcTestUtils.countRowsInTable(jdbcTemplate, "users"))
      .isOne()
}

In diesem speziellen Beispiel verletzen wir Testgrenzen, weil wir unsere App als eine HTTP-Blackbox behandeln, um den Benutzer zu senden, aber später behaupten wir, Implementierungsdetails zu verwenden, das heißt, unser Benutzer wurde in einigen DBs beibehalten.

Wenn wir unsere App über HTTP testen, können wir das Ergebnis dann auch über HTTP bestätigen?

@Test
fun links() {
    mvc.perform(post("/users")
      .contentType(MediaType.APPLICATION_JSON)
      .content("""{ "name":"jose" }"""))
      .andExpect(status().isCreated)

    mvc.perform(get("/users/jose"))
      .andExpect(status().isOk)
}

Es gibt ein paar Vorteile, wenn wir dem letzten Ansatz folgen:

  • Unser Test startet schneller (wahrscheinlich dauert die Ausführung etwas länger, aber es sollte sich auszahlen)

  • Unser Test kennt auch keine Nebenwirkungen, die nicht mit HTTP-Grenzen zusammenhängen, d. H. DBs

  • Schließlich drückt unser Test die Absicht des Systems klar aus: Wenn Sie POSTEN, können Sie Benutzer abrufen

Dies kann natürlich aus verschiedenen Gründen nicht immer möglich sein:

  • Möglicherweise haben wir keinen "Nebeneffekt" -Endpunkt: Hier können Sie überlegen, ob Sie "Testendpunkte" erstellen möchten.

  • Die Komplexität ist zu hoch, um die gesamte App zu treffen: Eine Option besteht darin, Slices zu berücksichtigen (wir werden später darüber sprechen).

4.4. Sorgfältig über@DirtiesContext nachdenken

Manchmal müssen wir in unseren Tests dieApplicationContext ändern. In diesem Szenario bietet@DirtiesContext genau diese Funktionalität.

Aus den oben genannten Gründen ist@DirtiesContext eine extrem teure Ressource, wenn es um die Ausführungszeit geht. Daher sollten wir vorsichtig sein.

Some misuses of @DirtiesContext include application cache reset or in memory DB resets. Es gibt bessere Möglichkeiten, diese Szenarien in Integrationstests zu behandeln, und wir werden einige in weiteren Abschnitten behandeln.

4.5. Verwenden von Testschnitten

Test Slices sind eine Spring Boot-Funktion, die in 1.4 eingeführt wurde. Die Idee ist ziemlich einfach: Spring erstellt einen reduzierten Anwendungskontext für einen bestimmten Teil Ihrer App.

Das Framework kümmert sich auch um die Konfiguration des Minimums.

Es gibt eine vernünftige Anzahl von Slices, die in Spring Boot sofort verfügbar sind, und wir können auch unsere eigenen erstellen:

  • @JsonTest: Registriert JSON-relevante Komponenten

  • @DataJpaTest: Registriert JPA-Beans, einschließlich des verfügbaren ORM

  • @JdbcTest: Nützlich für unformatierte JDBC-Tests, kümmert sich um die Datenquelle und in Speicher-DBs ohne ORM-Schnickschnack

  • @DataMongoTest: Versucht, ein In-Memory-Mongo-Test-Setup bereitzustellen

  • @WebMvcTest: Ein nachgebildeter MVC-Testabschnitt ohne den Rest der App

  • … (Wir könnenthe source überprüfen, um sie alle zu finden)

Diese spezielle Funktion kann uns dabei helfen, enge Tests zu erstellen, ohne die Leistung zu beeinträchtigen, insbesondere bei kleinen und mittelgroßen Apps.

Wenn unsere Anwendung jedoch weiter wächst, häufen sich die Informationen, da pro Segment ein (kleiner) Anwendungskontext erstellt wird.

4.6. Verwenden der Klassenvererbung

Die Verwendung einer einzelnenAbstractSpringIntegrationTest-Klasse als übergeordnetes Element aller unserer Integrationstests ist eine einfache, leistungsstarke und pragmatische Methode, um den Build schnell zu halten.

If we provide a solid setup, our team will simply extend it, knowing that everything ‘just works'. Auf diese Weise können wir uns weniger Gedanken über die Verwaltung des Status oder die Konfiguration des Frameworks machen und uns auf das jeweilige Problem konzentrieren.

Wir könnten dort alle Testanforderungen festlegen:

  • Der Spring Runner - oder am besten Regeln, falls wir später andere Läufer brauchen

  • Profile - idealerweise unser aggregiertestest profile

  • Erstkonfiguration - Einstellung des Status unserer Anwendung

Schauen wir uns eine einfache Basisklasse an, die sich um die vorherigen Punkte kümmert:

@SpringBootTest
@ActiveProfiles("test")
abstract class AbstractSpringIntegrationTest {

    @Rule
    @JvmField
    val springMethodRule = SpringMethodRule()

    companion object {
        @ClassRule
        @JvmField
        val SPRING_CLASS_RULE = SpringClassRule()
    }
}

4.7. State Management

Es ist wichtig, sich daran zu erinnern, wo "Einheit" in Unit Testcomes from ist. Einfach ausgedrückt bedeutet dies, dass wir jederzeit einen einzelnen Test (oder eine Teilmenge) durchführen können, um konsistente Ergebnisse zu erzielen.

Daher sollte der Zustand vor jedem Teststart sauber und bekannt sein.

Mit anderen Worten, das Ergebnis eines Tests sollte konsistent sein, unabhängig davon, ob er isoliert oder zusammen mit anderen Tests ausgeführt wird.

Diese Idee gilt auch für Integrationstests. Wir müssen sicherstellen, dass unsere App einen bekannten (und wiederholbaren) Status hat, bevor wir einen neuen Test starten. Je mehr Komponenten wir wiederverwenden, um die Dinge zu beschleunigen (Anwendungskontext, DBs, Warteschlangen, Dateien ...), desto größer ist die Wahrscheinlichkeit, dass der Staat verschmutzt wird.

Angenommen, wir haben die Klassenvererbung übernommen und haben jetzt einen zentralen Ort für die Verwaltung des Staates.

Erweitern wir unsere abstrakte Klasse, um sicherzustellen, dass sich unsere App in einem bekannten Zustand befindet, bevor wir Tests ausführen.

In unserem Beispiel wird davon ausgegangen, dass es mehrere Repositorys (aus verschiedenen Datenquellen) und einenWiremock-Server gibt:

@SpringBootTest
@ActiveProfiles("test")
@AutoConfigureWireMock(port = 8666)
@AutoConfigureMockMvc
abstract class AbstractSpringIntegrationTest {

    //... spring rules are configured here, skipped for clarity

    @Autowired
    protected lateinit var wireMockServer: WireMockServer

    @Autowired
    lateinit var jdbcTemplate: JdbcTemplate

    @Autowired
    lateinit var repos: Set>

    @Autowired
    lateinit var cacheManager: CacheManager

    @Before
    fun resetState() {
        cleanAllDatabases()
        cleanAllCaches()
        resetWiremockStatus()
    }

    fun cleanAllDatabases() {
        JdbcTestUtils.deleteFromTables(jdbcTemplate, "table1", "table2")
        jdbcTemplate.update("ALTER TABLE table1 ALTER COLUMN id RESTART WITH 1")
        repos.forEach { it.deleteAll() }
    }

    fun cleanAllCaches() {
        cacheManager.cacheNames
          .map { cacheManager.getCache(it) }
          .filterNotNull()
          .forEach { it.clear() }
    }

    fun resetWiremockStatus() {
        wireMockServer.resetAll()
        // set default requests if any
    }
}

4.8. Refactoring in Komponententests

Dies ist wahrscheinlich einer der wichtigsten Punkte. Wir werden immer wieder mit einigen Integrationstests konfrontiert sein, die tatsächlich eine allgemeine Richtlinie unserer App anwenden.

Immer wenn wir einige Integrationstests finden, die eine Reihe von Fällen der Kerngeschäftslogik testen, ist es an der Zeit, unseren Ansatz zu überdenken und sie in Komponententests aufzuteilen.

Ein mögliches Muster, um dies erfolgreich zu erreichen, könnte sein:

  • Identifizieren Sie Integrationstests, die mehrere Szenarien der Kerngeschäftslogik testen

  • Duplizieren Sie die Suite und überarbeiten Sie die Kopie in Komponententests. In diesem Stadium müssen Sie möglicherweise auch den Produktionscode aufschlüsseln, um ihn testbar zu machen

  • Holen Sie sich alle Tests grün

  • Hinterlassen Sie ein erfreuliches Beispiel für den Pfad, das in der Integrationssuite bemerkenswert genug ist. Möglicherweise müssen wir einige überarbeiten oder zusammenfügen und neu gestalten

  • Entfernen Sie die verbleibenden Integrationstests

Michael Feathers behandelt viele Techniken, um dies und mehr zu erreichen, indem er effektiv mit Legacy-Code arbeitet.

5. Zusammenfassung

In diesem Artikel hatten wir eine Einführung in Integrationstests mit dem Fokus auf Spring.

Zunächst sprachen wir über die Bedeutung von Integrationstests und warum diese in Spring-Anwendungen besonders relevant sind.

Danach haben wir einige Tools zusammengefasst, die für bestimmte Arten von Integrationstests in Web Apps nützlich sein können.

Schließlich haben wir eine Liste potenzieller Probleme durchgearbeitet, die die Testausführungszeit verlangsamen, sowie Tricks, um sie zu verbessern.