Optimalisering av vårintegrasjonstester

1. Introduksjon

I denne artikkelen vil vi ha en helhetlig diskusjon om integrasjonstester ved hjelp av våren og hvordan vi kan optimalisere dem.

Først vil vi kort diskutere viktigheten av integrasjonstester og deres plass i moderne programvare med fokus på vårens økosystem.

Senere vil vi dekke flere scenarier, med fokus på nettapper.

Deretter vil vi diskutere noen strategier for å forbedre testhastigheten, ved å lære om forskjellige tilnærminger som kan påvirke både måten vi former testene våre på og måten vi former selve appen på.

Før du begynner, er det viktig å huske på at dette er en meningsartikkel basert på erfaring. Noen av disse tingene kan passe deg, andre kanskje ikke.

Til slutt bruker denne artikkelen Kotlin for kodeeksemplene for å holde dem så kortfattede som mulig, men konseptene er ikke spesifikke for dette språket, og kodebiter skal føles meningsfylte for både Java og Kotlin-utviklere.

2. Integrasjonstester

Integrasjonstester er en grunnleggende del av automatiserte testsuiter. Selv om de ikke burde være så mange som enhetstester hvis vi følger en sunn testpyramide. Ved å stole på rammer som Spring, trenger vi en god del integrasjonstester for å risikere visse oppførsler i systemet vårt.

Jo mer vi forenkler koden vår ved å bruke Spring-moduler (data, sikkerhet, sosial ...), jo større er behovet for integrasjonstester. Dette blir spesielt sant når vi flytter biter av boben til infrastrukturen vår @Konfigurasjon klasser.

Vi bør ikke "teste rammeverket", men vi bør absolutt kontrollere at rammeverket er konfigurert for å oppfylle våre behov.

Integrasjonstester hjelper oss å bygge tillit, men de har en pris:

  • Det er en langsommere kjøringshastighet, noe som betyr langsommere bygg
  • Integrasjonstester innebærer også et bredere testomfang som i de fleste tilfeller ikke er ideelt

Med dette i bakhodet vil vi prøve å finne noen løsninger for å dempe de ovennevnte problemene.

3. Testing av nettapper

Spring gir noen muligheter for å teste webapplikasjoner, og de fleste Spring-utviklere er kjent med dem, disse er:

  • MockMvc: Spotter servlet API, nyttig for ikke-reaktive nettapper
  • TestRestTemplate: Kan brukes til å peke på appen vår, nyttig for ikke-reaktive webapper der spottede servlets ikke er ønskelige
  • WebTestClient: Er et testverktøy for reaktive webapper, både med spottede forespørsler / svar eller å treffe en ekte server

Siden vi allerede har artikler som dekker disse emnene, bruker vi ikke tid på å snakke om dem.

Ta gjerne en titt hvis du vil grave dypere.

4. Optimalisering av gjennomføringstid

Integrasjonstester er gode. De gir oss en god grad av selvtillit. Også hvis de implementeres på riktig måte, kan de beskrive intensjonen til appen vår på en veldig klar måte, med mindre hån og oppsettstøy.

Imidlertid, når appen vår modnes og utviklingen hoper seg opp, vil byggetiden uunngåelig øke. Når byggetiden øker, kan det bli upraktisk å fortsette å kjøre alle testene hver gang.

Deretter påvirker vi tilbakemeldingsløkken vår og kommer på vei til beste utviklingspraksis.

Videre er integrasjonstester iboende dyre. Starter utholdenhet av noe slag, sender forespørsler gjennom (selv om de aldri drar lokal vert), eller å ta noen IO tar ganske enkelt tid.

Det er viktig å holde øye med byggetiden vår, inkludert testutførelse. Og det er noen triks vi kan bruke om våren for å holde det lavt.

I de neste avsnittene vil vi dekke noen få punkter for å hjelpe oss med å optimalisere byggetiden vår, samt noen fallgruver som kan påvirke hastigheten:

  • Å bruke profiler klokt - hvordan profiler påvirker ytelsen
  • Revurderer @MockBean - hvordan spott treffer ytelse
  • Refactoring @MockBean - alternativer for å forbedre ytelsen
  • Tenker nøye på @DirtiesContext - en nyttig, men farlig kommentar, og hvordan du ikke bruker den
  • Ved hjelp av testskiver - et kult verktøy som kan hjelpe eller komme på vei
  • Bruke klassearv - en måte å organisere tester på en trygg måte
  • Statlig ledelse - god praksis for å unngå flassete tester
  • Refactoring til enhetstester - den beste måten å få en solid og sprø bygging

La oss komme i gang!

4.1. Bruke profiler klokt

Profiler er et ganske pent verktøy. Nemlig enkle koder som kan aktivere eller deaktivere bestemte områder av appen vår. Vi kan til og med implementere funksjonsflagg med dem!

Etter hvert som profilene våre blir rikere, er det fristende å bytte innimellom i integrasjonstestene våre. Det er praktiske verktøy for å gjøre det, som @ActiveProfiles. Derimot, hver gang vi tar en test med en ny profil, en ny ApplicationContext blir opprettet.

Å lage applikasjonskontekster kan være snappy med en vaniljefjærstart-app med ingenting i den. Legg til en ORM og noen få moduler, og den skyter raskt til 7+ sekunder.

Legg til en haug med profiler, og spred dem gjennom noen få tester, så får vi raskt en 60+ sekunders build (forutsatt at vi kjører tester som en del av vår build - og vi burde).

Når vi står overfor en kompleks applikasjon, er det skremmende å fikse dette. Men hvis vi planlegger nøye på forhånd, blir det trivielt å holde en fornuftig byggetid.

Det er noen triks vi kan huske på når det gjelder profiler i integrasjonstester:

  • Lag en samlet profil, dvs. testinkluderer alle nødvendige profiler - hold deg til testprofilen vår overalt
  • Design profilene våre med testerbarhet i tankene. Hvis vi ender med å bytte profil, er det kanskje en bedre måte
  • Oppgi testprofilen vår på et sentralt sted - vi snakker om dette senere
  • Unngå å teste alle profilkombinasjoner. Alternativt kan vi ha en e2e test-suite per miljø som tester appen med det spesifikke profilsettet

4.2. Problemene med @MockBean

@MockBean er et ganske kraftig verktøy.

Når vi trenger litt vårmagi, men ønsker å spotte en bestemt komponent, @MockBean kommer veldig godt med. Men det gjør det til en pris.

Hver gang @MockBean vises i en klasse, den ApplicationContext hurtigbufferen blir merket som skitten, og dermed vil løperen rense hurtigbufferen etter at testklassen er ferdig. Som igjen legger til en ekstra haug med sekunder til byggingen vår.

Dette er kontroversielt, men det kan hjelpe å prøve å utøve selve appen i stedet for å spotte for dette spesielle scenariet. Selvfølgelig er det ingen sølvkule her. Grenser blir uskarpe når vi ikke tillater oss å spotte avhengigheter.

Vi kan tenke: Hvorfor skulle vi vedvare når alt vi ønsker å teste er vårt REST-lag? Dette er et rettferdig poeng, og det er alltid et kompromiss.

Imidlertid, med noen få prinsipper i tankene, kan dette faktisk gjøres om til en fordel som fører til bedre design av begge testene og appen vår og reduserer testtiden.

4.3. Refactoring @MockBean

I denne delen vil vi prøve å omformulere en 'treg' test ved hjelp av @MockBean for å få det til å bruke hurtigbufret ApplicationContext.

La oss anta at vi vil teste en POST som oppretter en bruker. Hvis vi spottet - brukte @MockBean, kunne vi bare bekrefte at tjenesten vår har blitt ringt med en pent seriell bruker.

Hvis vi testet tjenesten vår riktig, bør denne tilnærmingen være tilstrekkelig:

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

Vi vil unngå @MockBean selv om. Så vi vil ende opp med å vedvare enheten (forutsatt at det er det tjenesten gjør).

Den mest naive tilnærmingen her ville være å teste bivirkningen: Etter POSTing er brukeren min i DB, i vårt eksempel vil dette bruke JDBC.

Dette bryter imidlertid testgrensene:

@Test morsomme lenker () {mvc.perform (post ("/ brukere") .contentType (MediaType.APPLICATION_JSON) .content ("" "{" name ":" jose "}" "")). Og Expect (status ( ) .isCreated) assertThat (JdbcTestUtils.countRowsInTable (jdbcTemplate, "brukere")) .isOne ()}

I dette spesielle eksemplet bryter vi testgrensene fordi vi behandler appen vår som en HTTP-svart boks for å sende brukeren, men senere hevder vi å bruke implementeringsdetaljer, det vil si at brukeren vår har vært vedvarende i noen DB.

Hvis vi utøver appen vår via HTTP, kan vi også hevde resultatet gjennom HTTP?

@Test morsomme lenker () {mvc.perform (post ("/ brukere") .contentType (MediaType.APPLICATION_JSON) .content ("" "{" name ":" jose "}" "")). Og Expect (status ( ) .isCreated) mvc.perform (get ("/ users / jose")). og Expect (status (). isOk)}

Det er noen fordeler hvis vi følger den siste tilnærmingen:

  • Testen vår starter raskere (uten tvil, det kan ta litt lenger tid å utføre skjønt, men det skal lønne seg)
  • Testen vår er heller ikke klar over bivirkninger som ikke er relatert til HTTP-grenser, dvs. DB-er
  • Til slutt uttrykker testen vår klarhet intensjonen med systemet: Hvis du POSTER, vil du kunne FÅ brukere

Selvfølgelig er dette ikke alltid mulig av forskjellige grunner:

  • Vi har kanskje ikke endepunktet 'bivirkning': Et alternativ her er å vurdere å lage 'testing endepunkter'
  • Kompleksiteten er for høy til å treffe hele appen: Et alternativ her er å vurdere skiver (vi snakker om dem senere)

4.4. Tenker nøye om @DirtiesContext

Noen ganger kan det hende vi trenger å endre ApplicationContext i testene våre. For dette scenariet, @DirtiesContext leverer akkurat den funksjonaliteten.

Av samme grunner som er utsatt ovenfor, @DirtiesContext er en ekstremt kostbar ressurs når det gjelder utførelsestid, og som sådan bør vi være forsiktige.

Noen misbruk av @DirtiesContext inkluderer tilbakestilling av applikasjonsbuffer eller i minnet DB-tilbakestillinger. Det er bedre måter å håndtere disse scenariene i integrasjonstester, og vi vil dekke noen i ytterligere seksjoner.

4.5. Bruke testskiver

Testskiver er en Spring Boot-funksjon introdusert i 1.4. Ideen er ganske enkel, Spring vil skape en redusert applikasjonskontekst for et bestemt stykke av appen din.

Rammeverket vil også ta seg av å konfigurere det aller minste.

Det er et fornuftig antall skiver tilgjengelig ut av esken i Spring Boot, og vi kan også lage våre egne:

  • @JsonTest: Registrerer JSON-relevante komponenter
  • @DataJpaTest: Registrerer JPA-bønner, inkludert tilgjengelig ORM
  • @JdbcTest: Nyttig for rå JDBC-tester, tar seg av datakilden og i minnet DB-er uten ORM-frills
  • @DataMongoTest: Forsøker å gi et oppsett for mongotesting i minnet
  • @WebMvcTest: En mock MVC-testbit uten resten av appen
  • ... (vi kan sjekke kilden for å finne dem alle)

Denne spesielle funksjonen, hvis den brukes klokt, kan hjelpe oss med å bygge smale tester uten en så stor straff når det gjelder ytelse, spesielt for små / mellomstore apper.

Imidlertid, hvis applikasjonen vår fortsetter å vokse, hoper den seg også opp når den skaper en (liten) applikasjonskontekst per stykke.

4.6. Bruke klassearv

Bruke en singel AbstractSpringIntegrationTest klasse som overordnet til alle våre integrasjonstester er en enkel, kraftig og pragmatisk måte å holde bygningen rask på.

Hvis vi gir et solid oppsett, vil teamet vårt bare utvide det, vel vitende om at alt 'bare fungerer'. På denne måten kan vi bekymre oss mindre om å administrere staten eller konfigurere rammeverket og fokusere på problemet.

Vi kunne stille alle testkravene der:

  • Vårløperen - eller helst regler, i tilfelle vi trenger andre løpere senere
  • profiler - ideelt sett vårt aggregat test profil
  • innledende konfigurasjon - innstilling av tilstanden til applikasjonen vår

La oss se på en enkel baseklasse som tar seg av de forrige punktene:

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

4.7. Statlig ledelse

Det er viktig å huske hvor ‘enhet’ i Enhetstest kommer fra. Enkelt sagt betyr det at vi når som helst kan kjøre en enkelt test (eller en delmengde) for å få konsistente resultater.

Derfor bør staten være ren og kjent før hver test starter.

Med andre ord, resultatet av en test skal være konsistent, uavhengig av om den utføres isolert eller sammen med andre tester.

Denne ideen gjelder akkurat det samme for integrasjonstester. Vi må sørge for at appen vår har en kjent (og repeterbar) tilstand før vi starter en ny test. Jo flere komponenter vi gjenbruker for å få fart på ting (appkontekst, DB-er, køer, filer ...), jo større er sjansene for å få forurensning fra staten.

Forutsatt at vi gikk inn med klassearv, har vi nå et sentralt sted å administrere staten.

La oss forbedre vår abstrakte klasse for å sikre at appen vår er i en kjent tilstand før vi kjører tester.

I vårt eksempel antar vi at det er flere arkiver (fra forskjellige datakilder), og a Wiremock server:

@SpringBootTest @ActiveProfiles ("test") @AutoConfigureWireMock (port = 8666) @AutoConfigureMockMvc abstrakt klasse AbstractSpringIntegrationTest {// ... vårregler er konfigurert her, hoppet over for klarhet @Autowired-beskyttet lateinit var wireMockServer: WireMockServer JdbcTemplate @Autowired lateinit var repos: Set @Autowired lateinit var cacheManager: CacheManager @Fore fun resetState () {cleanAllDatabases () cleanAllCaches () resetWiremockStatus ()} fun cleanAllDatabases () {JdbcTestUtils.deleteFromTables (jdbcTemplate, "table1" "," table1 "" 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 () // angi eventuelle standardforespørsler}}

4.8. Refactoring til enhetstester

Dette er trolig et av de viktigste punktene. Vi vil finne oss igjen og igjen med noen integrasjonstester som faktisk utøver noen høynivåpolitikk i appen vår.

Når vi finner noen integrasjonstester som tester en rekke tilfeller av kjernevirksomhetslogikk, er det på tide å revurdere vår tilnærming og dele dem opp i enhetstester.

Et mulig mønster her for å oppnå dette med suksess kan være:

  • Identifiser integrasjonstester som tester flere scenarier for kjernevirksomhetslogikk
  • Dupliser suiten, og omformer kopien til enhetstester - på dette stadiet kan det hende vi må bryte ned produksjonskoden for å gjøre den testbar
  • Få alle testene grønne
  • Legg igjen en lykkelig stikkprøve som er bemerkelsesverdig nok i integrasjonspakken - det kan hende at vi trenger å refaktorere eller bli med og omforme noen få
  • Fjern de gjenværende integrasjonstestene

Michael Feathers dekker mange teknikker for å oppnå dette og mer i å jobbe effektivt med Legacy Code.

5. Sammendrag

I denne artikkelen hadde vi en introduksjon til integrasjonstester med fokus på våren.

Først snakket vi om viktigheten av integrasjonstester og hvorfor de er spesielt relevante i vårapplikasjoner.

Etter det oppsummerte vi noen verktøy som kan være nyttige for visse typer integrasjonstester i Web Apps.

Til slutt gikk vi gjennom en liste over potensielle problemer som reduserer testutførelsestiden, samt triks for å forbedre den.


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