Optimalisatie van veerintegratietests

1. Inleiding

In dit artikel zullen we een holistische discussie hebben over integratietests met Spring en hoe u deze kunt optimaliseren.

Eerst bespreken we kort het belang van integratietests en hun plaats in moderne software, met de nadruk op het Spring-ecosysteem.

Later behandelen we meerdere scenario's, waarbij we ons concentreren op web-apps.

Vervolgens bespreken we enkele strategieën om de testsnelheid te verbeteren, door te leren over verschillende benaderingen die van invloed kunnen zijn op zowel de manier waarop we onze tests vormgeven als de manier waarop we de app zelf vormgeven.

Voordat u aan de slag gaat, is het belangrijk om in gedachten te houden dat dit een opinieartikel is op basis van ervaring. Sommige van deze dingen passen misschien bij u, andere misschien niet.

Ten slotte gebruikt dit artikel Kotlin voor de codevoorbeelden om ze zo beknopt mogelijk te houden, maar de concepten zijn niet specifiek voor deze taal en codefragmenten zouden zowel voor Java- als voor Kotlin-ontwikkelaars zinvol moeten aanvoelen.

2. Integratietests

Integratietests zijn een fundamenteel onderdeel van geautomatiseerde testsuites. Hoewel ze niet zo talrijk zouden moeten zijn als unit-tests als we een gezonde testpiramide volgen. Als we vertrouwen op frameworks zoals Spring, hebben we een behoorlijke hoeveelheid integratietests nodig om bepaalde gedragingen van ons systeem te verminderen.

Hoe meer we onze code vereenvoudigen door Spring-modules te gebruiken (data, beveiliging, sociaal…), hoe groter de behoefte aan integratietests. Dit wordt met name het geval wanneer we stukjes en beetjes van onze infrastructuur verplaatsen naar @Configuratie klassen.

We moeten het raamwerk niet “testen”, maar we moeten zeker controleren of het raamwerk is geconfigureerd om aan onze behoeften te voldoen.

Integratietests helpen ons vertrouwen op te bouwen, maar ze hebben een prijs:

  • Dat is een langzamere uitvoeringssnelheid, wat een langzamere build betekent
  • Integratietests impliceren ook een bredere testscope, wat in de meeste gevallen niet ideaal is

Met dit in gedachten zullen we proberen enkele oplossingen te vinden om de bovengenoemde problemen te verminderen.

3. Webapps testen

Spring biedt een paar opties om webapplicaties te testen, en de meeste Spring-ontwikkelaars zijn er bekend mee, deze zijn:

  • MockMvc: Bespot de servlet-API, handig voor niet-reactieve web-apps
  • TestRestTemplate: Kan worden gebruikt om naar onze app te verwijzen, handig voor niet-reactieve web-apps waar bespotte servlets niet wenselijk zijn
  • WebTestClient: is een testtool voor reactieve web-apps, beide met bespotte verzoeken / reacties of het raken van een echte server

Aangezien we al artikelen hebben over deze onderwerpen, zullen we er geen tijd aan besteden om erover te praten.

Neem gerust een kijkje als je dieper wilt graven.

4. De uitvoeringstijd optimaliseren

Integratietests zijn geweldig. Ze geven ons een groot vertrouwen. Als ze op de juiste manier worden geïmplementeerd, kunnen ze de bedoeling van onze app op een zeer duidelijke manier beschrijven, met minder spot en opstartgeluid.

Naarmate onze app ouder wordt en de ontwikkeling zich opstapelt, gaat de bouwtijd onvermijdelijk omhoog. Naarmate de bouwtijd toeneemt, kan het onpraktisch worden om elke keer alle tests uit te voeren.

Daarna onze feedbackloop beïnvloeden en op weg helpen met de beste ontwikkelingspraktijken.

Bovendien zijn integratietests inherent duur. Op een of andere manier persistentie opstarten, verzoeken doorsturen (zelfs als ze nooit weggaan localhost), of wat IO doen kost gewoon tijd.

Het is van het grootste belang om onze bouwtijd in de gaten te houden, inclusief testuitvoering. En er zijn enkele trucs die we in het voorjaar kunnen toepassen om het laag te houden.

In de volgende secties behandelen we een paar punten om ons te helpen onze bouwtijd te optimaliseren, evenals enkele valkuilen die van invloed kunnen zijn op de snelheid:

  • Verstandig gebruik van profielen - hoe profielen de prestaties beïnvloeden
  • Heroverwegen @MockBean - hoe spottende prestaties de prestaties raken
  • Refactoring @RTLnieuws - alternatieven om de prestaties te verbeteren
  • Goed nadenken over @DirtiesContext - een nuttige maar gevaarlijke annotatie en hoe je deze niet moet gebruiken
  • Testplakjes gebruiken - een coole tool die kan helpen of op weg kan gaan
  • Klasse-overerving gebruiken - een manier om tests op een veilige manier te organiseren
  • Staatsbeheer - goede praktijken om flakey-tests te vermijden
  • Refactoring in unit-tests - de beste manier om een ​​solide en pittige build te krijgen

Laten we beginnen!

4.1. Gebruik profielen verstandig

Profielen zijn een behoorlijk handige tool. Namelijk eenvoudige tags die bepaalde delen van onze app kunnen in- of uitschakelen. We zouden zelfs kenmerkvlaggen met hen kunnen implementeren!

Naarmate onze profielen rijker worden, is het verleidelijk om af en toe te wisselen in onze integratietests. Daar zijn handige tools voor, zoals @ActiveProfiles. Echter, elke keer dat we een test doen met een nieuw profiel, een nieuw ApplicationContext wordt gemaakt.

Het maken van applicatiecontexten kan pittig zijn met een vanilla spring-boot-app met niets erin. Voeg een ORM en een paar modules toe en het zal snel omhoogschieten naar 7+ seconden.

Voeg een aantal profielen toe en verspreid ze door een paar tests en we krijgen snel een build van 60+ seconden (ervan uitgaande dat we tests uitvoeren als onderdeel van onze build - en dat zouden we moeten doen).

Als we eenmaal met een voldoende complexe applicatie worden geconfronteerd, is het moeilijk om dit op te lossen. Als we echter van tevoren zorgvuldig plannen, wordt het triviaal om een ​​verstandige bouwtijd aan te houden.

Er zijn een paar trucs die we in gedachten kunnen houden als het gaat om profielen in integratietests:

  • Maak een totaalprofiel, d.w.z. test, neem alle benodigde profielen op - blijf overal bij ons testprofiel
  • Ontwerp onze profielen met het oog op testbaarheid. Als we uiteindelijk van profiel moeten wisselen, is er misschien een betere manier
  • Geef ons testprofiel op een gecentraliseerde plaats op - we zullen er later over praten
  • Test niet alle combinaties van profielen. Als alternatief kunnen we een e2e-testsuite per omgeving hebben om de app te testen met die specifieke profielset

4.2. De problemen met @RTLnieuws

@RTLnieuws is een behoorlijk krachtig hulpmiddel.

Als we wat lentemagie nodig hebben maar een bepaald onderdeel willen bespotten, @RTLnieuws komt echt goed van pas. Maar het doet dat tegen een prijs.

Elke keer @RTLnieuws verschijnt in een klas, de ApplicationContext cache wordt gemarkeerd als vuil, daarom zal de hardloper de cache opschonen nadat de testklasse is voltooid. Dat voegt weer een hoop extra seconden toe aan onze build.

Dit is controversieel, maar het zou kunnen helpen om de daadwerkelijke app uit te oefenen in plaats van te bespotten voor dit specifieke scenario. Natuurlijk is er hier geen wondermiddel. Grenzen worden wazig als we onszelf niet toestaan ​​om afhankelijkheden te bespotten.

We zouden kunnen denken: waarom zouden we doorgaan als we alleen onze REST-laag willen testen? Dit is een goed punt, en er is altijd een compromis.

Met een paar principes in gedachten kan dit echter in feite worden omgezet in een voordeel dat leidt tot een beter ontwerp van zowel tests als onze app en de testtijd verkort.

4.3. Refactoring @RTLnieuws

In dit gedeelte zullen we proberen een ‘langzame 'test te refactoren met @RTLnieuws om ervoor te zorgen dat het de in het cachegeheugen opgeslagen ApplicationContext.

Laten we aannemen dat we een POST willen testen die een gebruiker aanmaakt. Als we aan het spotten waren - gebruikten @RTLnieuws, konden we eenvoudig controleren of onze service is gebeld door een gebruiker met een mooie serienummer.

Als we onze service goed hebben getest, zou deze aanpak moeten volstaan:

klasse UsersControllerIntegrationTest: AbstractSpringIntegrationTest () {@Autowired lateinit var mvc: MockMvc @MockBean lateinit var userService: UserService @Test fun links () {mvc.perform (post ("/ users") .contentType (MediaType.APPLICATION_J "). "" {"name": "jose"} "" ")) .andExpect (status (). isCreated) verifieer (userService) .save (" jose ")}} interface UserService {fun save (name: String)}

We willen vermijden @RTLnieuws wel. Dus we zullen uiteindelijk de entiteit vasthouden (ervan uitgaande dat de service dat doet).

De meest naïeve benadering hier zou zijn om de bijwerking te testen: na POSTing bevindt mijn gebruiker zich in mijn database, in ons voorbeeld zou dit JDBC gebruiken.

Dit is echter in strijd met de testgrenzen:

@Test leuke links () {mvc.perform (post ("/ users") .contentType (MediaType.APPLICATION_JSON) .content ("" "{" name ":" jose "}" "")) .andExpect (status ( ) .isCreated) assertThat (JdbcTestUtils.countRowsInTable (jdbcTemplate, "gebruikers")) .isOne ()}

In dit specifieke voorbeeld overtreden we testgrenzen omdat we onze app behandelen als een HTTP-black box om de gebruiker te sturen, maar later beweren we met behulp van implementatiedetails, dat wil zeggen dat onze gebruiker in een of andere database is vastgehouden.

Als we onze app via HTTP gebruiken, kunnen we het resultaat dan ook via HTTP bevestigen?

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

Er zijn een paar voordelen als we de laatste benadering volgen:

  • Onze test zal sneller starten (het kan misschien wat langer duren om uit te voeren, maar het moet terugverdiend worden)
  • Ook is onze test niet op de hoogte van bijwerkingen die geen verband houden met HTTP-grenzen, d.w.z. DB's
  • Ten slotte drukt onze test duidelijk de bedoeling van het systeem uit: als u POST, kunt u gebruikers KRIJGEN

Dit is natuurlijk om verschillende redenen niet altijd mogelijk:

  • We hebben misschien niet het 'neveneffect'-eindpunt: een optie hier is om te overwegen om' test-eindpunten 'te creëren
  • Complexiteit is te hoog om de hele app te raken: een optie hier is om segmenten te overwegen (we zullen er later over praten)

4.4. Zorgvuldig nadenken over @DirtiesContext

Soms moeten we het ApplicationContext in onze tests. Voor dit scenario @DirtiesContext levert precies die functionaliteit.

Om dezelfde redenen als hierboven beschreven, @DirtiesContext is een extreem dure bron als het gaat om uitvoeringstijd, en daarom moeten we voorzichtig zijn.

Sommige misbruiken van @DirtiesContext omvatten applicatiecache-reset of in geheugen DB-resets. Er zijn betere manieren om met deze scenario's om te gaan in integratietests, en we zullen er enkele in verdere secties behandelen.

4.5. Testplakken gebruiken

Test Slices zijn een Spring Boot-functie die is geïntroduceerd in de 1.4. Het idee is vrij eenvoudig: Spring zal een beperkte toepassingscontext creëren voor een specifiek deel van uw app.

Het framework zorgt ook voor het configureren van het minimum.

Er is een redelijk aantal plakjes beschikbaar in Spring Boot en we kunnen er ook zelf een maken:

  • @JsonTest: Registreert JSON-relevante componenten
  • @BuienRadarNL: Registreert JPA-bonen, inclusief de beschikbare ORM
  • @JDbcTest: Handig voor onbewerkte JDBC-tests, zorgt voor de gegevensbron en in het geheugen DB's zonder ORM-franje
  • @BuienRadarNL: Probeert een mongo-testopstelling in het geheugen te bieden
  • @WebMvcTest: Een mock MVC-testplak zonder de rest van de app
  • ... (we kunnen de bron controleren om ze allemaal te vinden)

Deze specifieke functie, mits verstandig gebruikt, kan ons helpen bij het bouwen van smalle tests zonder zo'n grote nadelige invloed op de prestaties, met name voor kleine / middelgrote apps.

Als onze applicatie echter blijft groeien, stapelt deze zich ook op omdat het één (kleine) applicatiecontext per slice creëert.

4.6. Klasse-overerving gebruiken

Met behulp van een enkele AbstractSpringIntegrationTest class als de ouder van al onze integratietests is een eenvoudige, krachtige en pragmatische manier om de build snel te houden.

Als we een solide opzet bieden, zal ons team deze gewoon uitbreiden, wetende dat alles ‘gewoon werkt '. Op deze manier kunnen we ons minder zorgen maken over het beheren van de staat of het configureren van het raamwerk en ons concentreren op het probleem bij de hand.

We zouden daar alle testvereisten kunnen stellen:

  • The Spring runner - of liever regels, voor het geval we later andere lopers nodig hebben
  • profielen - idealiter ons aggregaat test profiel
  • initiële configuratie - het instellen van de status van onze applicatie

Laten we eens kijken naar een eenvoudige basisklasse die voor de vorige punten zorgt:

@SpringBootTest @ActiveProfiles ("test") abstracte klasse AbstractSpringIntegrationTest {@Rule @JvmField val springMethodRule = SpringMethodRule () begeleidend object {@ClassRule @JvmField val SPRING_CLASS_RULE = SpringClassRule ()}}

4.7. Staatsbeheer

Het is belangrijk om te onthouden waar ‘unit 'in Unit Test vandaan komt. Simpel gezegd, het betekent dat we op elk moment een enkele test (of een subset) kunnen uitvoeren om consistente resultaten te krijgen.

Daarom moet de staat schoon en bekend zijn voordat elke test begint.

Met andere woorden, het resultaat van een test moet consistent zijn, ongeacht of deze afzonderlijk of samen met andere tests wordt uitgevoerd.

Dit idee is net zo van toepassing op integratietests. We moeten ervoor zorgen dat onze app een bekende (en herhaalbare) status heeft voordat we een nieuwe test starten. Hoe meer componenten we hergebruiken om dingen te versnellen (app-context, DB's, wachtrijen, bestanden…), hoe groter de kans op vervuiling door de staat.

Ervan uitgaande dat we all-in gingen met klasse-overerving, hebben we nu een centrale plaats om de staat te beheren.

Laten we onze abstracte klasse verbeteren om ervoor te zorgen dat onze app zich in een bekende staat bevindt voordat we tests uitvoeren.

In ons voorbeeld gaan we ervan uit dat er verschillende opslagplaatsen zijn (uit verschillende gegevensbronnen), en een Wiremock server:

@SpringBootTest @ActiveProfiles ("test") @AutoConfigureWireMock (port = 8666) @AutoConfigureMockMvc abstracte klasse AbstractSpringIntegrationTest {// ... springregels zijn hier geconfigureerd, voor de duidelijkheid overgeslagen @Autowired beschermd lateinit var wireMock lateServer: WireMock JdbcTemplate @Autowired lateinit var repos: Set @Autowired lateinit var cacheManager: CacheManager @Before fun resetState () {cleanAllDatabases () cleanAllCaches () resetWiremockStatus ()} plezier cleanAllDatabases () {JdbcTestUtils.deleteFromTables (jdbcTemplate, "table1" ALLT "table2" table1 VERANDER KOLOM ID HERSTART MET 1 ") repos.forEach {it.deleteAll ()}} fun cleanAllCaches () {cacheManager.cacheNames .map {cacheManager.getCache (it)} .filterNotNull () .forEach {it.clear () }} leuke resetWiremockStatus () {wireMockServer.resetAll () // stel eventuele standaardverzoeken in}}

4.8. Refactoring in unit-tests

Dit is waarschijnlijk een van de belangrijkste punten. We zullen onszelf keer op keer zien met enkele integratietests die feitelijk een hoogstaand beleid van onze app toepassen.

Telkens wanneer we enkele integratietests vinden die een aantal gevallen van kernlogica testen, is het tijd om onze aanpak te heroverwegen en ze op te splitsen in unit-tests.

Een mogelijk patroon hier om dit met succes te bereiken, zou kunnen zijn:

  • Identificeer integratietests die meerdere scenario's van kernlogica testen
  • Dupliceer de suite en herstructureer de kopie in unit-tests - in dit stadium moeten we mogelijk ook de productiecode opsplitsen om deze testbaar te maken
  • Zorg dat alle tests groen zijn
  • Laat een voorbeeld van een gelukkig pad achter dat opmerkelijk genoeg is in de integratiesuite - we moeten misschien een refactureren of aansluiten en een paar hervormen
  • Verwijder de resterende integratietests

Michael Feathers behandelt vele technieken om dit en meer te bereiken in Effectief werken met oude code.

5. Samenvatting

In dit artikel hebben we een inleiding gehad tot integratietests met een focus op Spring.

Ten eerste hebben we het gehad over het belang van integratietests en waarom ze bijzonder relevant zijn in Spring-applicaties.

Daarna hebben we enkele tools samengevat die van pas kunnen komen bij bepaalde soorten integratietests in webapps.

Ten slotte hebben we een lijst met mogelijke problemen doorgenomen die de uitvoeringstijd van onze tests vertragen, evenals trucs om deze te verbeteren.


$config[zx-auto] not found$config[zx-overlay] not found