Optimering af Spring Integration Tests

1. Introduktion

I denne artikel vil vi have en holistisk diskussion om integrationstest ved hjælp af Spring og hvordan vi optimerer dem.

Først vil vi kort diskutere vigtigheden af ​​integrationstests og deres plads i moderne software med fokus på forårets økosystem.

Senere dækker vi flere scenarier med fokus på web-apps.

Dernæst diskuterer vi nogle strategier for at forbedre testhastighedenved at lære om forskellige tilgange, der kan påvirke både den måde, vi former vores test på, og den måde, vi former selve appen på.

Før du kommer i gang, er det vigtigt at huske på, at dette er en meningsartikel baseret på erfaring. Nogle af disse ting passer måske til dig, andre måske ikke.

Endelig bruger denne artikel Kotlin til kodeprøverne for at holde dem så kortfattede som muligt, men begreberne er ikke specifikke for dette sprog, og kodestykker skal føles meningsfulde for både Java og Kotlin-udviklere.

2. Integrationstests

Integrationstest er en grundlæggende del af automatiserede testpakker. Selvom de ikke burde være så mange som enhedstest, hvis vi følger en sund testpyramide. Hvis vi stoler på rammer som Spring, har vi brug for en hel del integrationstest for at risikere visse opførsler i vores system.

Jo mere vi forenkler vores kode ved hjælp af Spring-moduler (data, sikkerhed, social ...), jo større er behovet for integrationstest. Dette bliver især sandt, når vi flytter bits og bobs af vores infrastruktur ind @Konfiguration klasser.

Vi bør ikke "teste rammen", men vi bør bestemt kontrollere, at rammen er konfigureret til at opfylde vores behov.

Integrationstest hjælper os med at opbygge tillid, men de har en pris:

  • Det er en langsommere udførelseshastighed, hvilket betyder langsommere bygger
  • Integrationstest indebærer også et bredere testomfang, som i de fleste tilfælde ikke er ideelt

Med dette i tankerne vil vi prøve at finde nogle løsninger til at afbøde de ovennævnte problemer.

3. Test af webapps

Spring bringer et par muligheder for at teste webapplikationer, og de fleste Spring-udviklere kender dem, disse er:

  • MockMvc: Spotter servlet-API'et, nyttigt til ikke-reaktive webapps
  • TestRestTemplate: Kan bruges til at pege på vores app, hvilket er nyttigt til ikke-reaktive webapps, hvor spottede servlets ikke er ønskelige
  • WebTestClient: Er et testværktøj til reaktive webapps, begge med hånede anmodninger / svar eller rammer en ægte server

Da vi allerede har artikler, der dækker disse emner, bruger vi ikke tid på at tale om dem.

Du er velkommen til at kigge, hvis du gerne vil grave dybere.

4. Optimering af udførelsestid

Integrationstest er gode. De giver os en god grad af selvtillid. Også hvis de implementeres korrekt, kan de beskrive hensigten med vores app på en meget klar måde med mindre hån og opsætningsstøj.

Når vores app modnes, og udviklingen hobes op, går byggetiden uundgåeligt op. Efterhånden som byggetiden øges, kan det blive upraktisk at fortsætte med at køre alle test hver gang.

Derefter påvirker vores feedback loop og kommer på vejen for bedste udviklingspraksis.

Desuden er integrationstest i sagens natur dyre. Starter vedholdenhed af en slags, sender anmodninger igennem (selvom de aldrig forlader lokal vært), eller det tager tid at lave noget IO.

Det er altafgørende at holde øje med vores byggetid, inklusive testudførelse. Og der er nogle tricks, vi kan anvende om foråret for at holde det lavt.

I de næste sektioner dækker vi et par punkter for at hjælpe os med at optimere vores byggetid samt nogle faldgruber, der kan påvirke dens hastighed:

  • Brug af profiler klogt - hvordan profiler påvirker ydeevnen
  • Genovervejelse @MockBean - hvordan mocking rammer ydeevne
  • Refactoring @MockBean - alternativer til forbedring af ydeevnen
  • Tænker nøje på @DirtiesContext - en nyttig, men farlig kommentar, og hvordan man ikke bruger den
  • Brug af testskiver - et sejt værktøj, der kan hjælpe eller komme på vej
  • Brug af klassearv - en måde at organisere prøver på en sikker måde
  • Statsledelse - god praksis for at undgå uhyggelige tests
  • Omdannelse til enhedstest - den bedste måde at få en solid og snappy opbygning på

Lad os komme igang!

4.1. Brug klogt af profiler

Profiler er et ret pænt værktøj. Nemlig enkle tags, der kan aktivere eller deaktivere bestemte områder i vores app. Vi kunne endda implementere funktionsflag med dem!

Da vores profiler bliver rigere, er det fristende at bytte ind imellem i vores integrationstest. Der er praktiske værktøjer til at gøre det, ligesom @ActiveProfiles. Imidlertid, hver gang vi laver en test med en ny profil, en ny ApplicationContext bliver skabt.

Oprettelse af applikationskontekster kan være snappy med en vaniljefjeder boot-app uden noget i den. Tilføj en ORM og et par moduler, og det skyder hurtigt op til 7+ sekunder.

Tilføj en masse profiler, og spred dem gennem et par tests, så får vi hurtigt en 60+ sekunders build (forudsat at vi kører tests som en del af vores build - og vi burde).

Når vi først står over for en kompleks applikation, er det skræmmende at løse dette. Men hvis vi planlægger nøje på forhånd, bliver det trivielt at holde en fornuftig byggetid.

Der er et par tricks, vi kan huske på, når det kommer til profiler i integrationstest:

  • Opret en samlet profil, dvs. prøve, inkluderer alle nødvendige profiler indeni - hold dig til vores testprofil overalt
  • Design vores profiler med testbarhed i tankerne. Hvis vi ender med at skifte profil, er der måske en bedre måde
  • Angiv vores testprofil et centralt sted - vi taler om dette senere
  • Undgå at teste alle profilkombinationer. Alternativt kunne vi have en e2e-test-suite pr. Miljø, der tester appen med det specifikke profilsæt

4.2. Problemer med @MockBean

@MockBean er et ret kraftigt værktøj.

Når vi har brug for noget forårsmagi, men ønsker at spotte en bestemt komponent, @MockBean kommer virkelig godt med. Men det gør det til en pris.

Hver gang @MockBean vises i en klasse, den ApplicationContext cache bliver markeret som snavset, derfor runner løberen cachen, når testklassen er færdig. Hvilket igen tilføjer en ekstra masse sekunder til vores build.

Dette er en kontroversiel, men at prøve at udøve den faktiske app i stedet for at spotte for dette særlige scenario kan hjælpe. Selvfølgelig er der ingen sølvkugle her. Grænser bliver slørede, når vi ikke tillader os at spotte afhængigheder.

Vi tænker måske: Hvorfor skulle vi fortsætte, når alt, hvad vi vil teste, er vores REST-lag? Dette er et rimeligt punkt, og der er altid et kompromis.

Men med et par principper i tankerne kan dette faktisk gøres til en fordel, der fører til bedre design af både test og vores app og reducerer testtid.

4.3. Refactoring @MockBean

I dette afsnit forsøger vi at omformulere en 'langsom' test ved hjælp af @MockBean for at få det til at genbruge cachen ApplicationContext.

Lad os antage, at vi vil teste en POST, der opretter en bruger. Hvis vi hånede - ved hjælp af @MockBean, kunne vi simpelthen kontrollere, at vores service er blevet kaldt med en pænt seriel bruger.

Hvis vi testede vores service korrekt, skulle denne tilgang være tilstrækkelig:

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) verificer (userService) .save (" jose ")}} interface UserService {fun save (name: String)}

Vi vil undgå @MockBean selvom. Så vi ender med at fastholde enheden (forudsat at det er hvad tjenesten gør).

Den mest naive tilgang her ville være at teste bivirkningen: Efter POSTing er min bruger i min DB, i vores eksempel vil dette bruge JDBC.

Dette overtræder dog testgrænser:

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

I dette specifikke eksempel overtræder vi testgrænser, fordi vi behandler vores app som en HTTP-sort boks for at sende brugeren, men senere hævder vi ved hjælp af implementeringsoplysninger, det vil sige, vores bruger er blevet fastholdt i nogle DB.

Hvis vi udøver vores app via HTTP, kan vi også hævde resultatet via HTTP?

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

Der er et par fordele, hvis vi følger den sidste tilgang:

  • Vores test starter hurtigere (det kan uden tvivl tage lidt længere tid at udføre, men det skal betale tilbage)
  • Vores test er heller ikke opmærksom på bivirkninger, der ikke er relateret til HTTP-grænser, dvs. DB'er
  • Endelig udtrykker vores test med klarhed systemets hensigt: Hvis du POSTER, vil du være i stand til at FÅ brugere

Selvfølgelig er det måske ikke altid muligt af forskellige årsager:

  • Vi har muligvis ikke 'bivirkning'-slutpunktet: En mulighed her er at overveje at oprette' test slutpunkter '
  • Kompleksitet er for høj til at ramme hele appen: En mulighed her er at overveje skiver (vi taler om dem senere)

4.4. Tænker nøje over @DirtiesContext

Nogle gange skal vi muligvis ændre ApplicationContext i vores tests. For dette scenarie, @DirtiesContext leverer præcis den funktionalitet.

Af de samme grunde udsat ovenfor, @DirtiesContext er en ekstremt dyr ressource, når det kommer til udførelsestid, og som sådan skal vi være forsigtige.

Nogle misbrug af @DirtiesContext inkluderer nulstilling af applikationscache eller i hukommelse DB-nulstillinger. Der er bedre måder at håndtere disse scenarier på i integrationstest, og vi vil dække nogle i yderligere sektioner.

4.5. Brug af testskiver

Testskiver er en Spring Boot-funktion, der blev introduceret i 1.4. Ideen er ret enkel, Spring opretter en reduceret applikationskontekst til et bestemt stykke af din app.

Også rammen tager sig af konfigurationen af ​​det mindste.

Der er et fornuftigt antal skiver tilgængelige ud af kassen i Spring Boot, og vi kan også oprette vores egne:

  • @JsonTest: Registrerer JSON-relevante komponenter
  • @DataJpaTest: Registrerer JPA-bønner, inklusive den tilgængelige ORM
  • @JdbcTest: Nyttigt til rå JDBC-tests, tager sig af datakilden og i hukommelsen DB'er uden ORM-dikkedarer
  • @DataMongoTest: Forsøger at levere en opsætning af mongotest i hukommelsen
  • @WebMvcTest: En mock MVC-testskive uden resten af ​​appen
  • ... (vi kan kontrollere kilden for at finde dem alle)

Denne særlige funktion, hvis den bruges klogt, kan hjælpe os med at opbygge smalle tests uden en så stor straf med hensyn til ydeevne, især for små / mellemstore apps.

Men hvis vores ansøgning fortsætter med at vokse, hober den sig også op, da den skaber en (lille) applikationskontekst pr. Skive.

4.6. Brug af klasse arv

Brug af en enkelt AbstractSpringIntegrationTest klasse som forælder til alle vores integrationstest er en enkel, kraftfuld og pragmatisk måde at holde bygningen hurtigt på.

Hvis vi leverer en solid opsætning, vil vores team blot udvide det, vel vidende at alt 'bare fungerer'. På denne måde kan vi bekymre os mindre om at styre staten eller konfigurere rammerne og fokusere på det aktuelle problem.

Vi kunne stille alle testkrav der:

  • Spring løberen - eller helst regler, hvis vi har brug for andre løbere senere
  • profiler - ideelt set vores samlede prøve profil
  • indledende konfiguration - indstilling af tilstanden for vores applikation

Lad os se på en simpel baseklasse, der tager sig af de tidligere punkter:

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

4.7. Statsledelse

Det er vigtigt at huske, hvor 'enhed' i enhedstest kommer fra. Kort sagt betyder det, at vi til enhver tid kan køre en enkelt test (eller en delmængde) for at få ensartede resultater.

Derfor bør staten være ren og kendt inden hver test starter.

Med andre ord skal resultatet af en test være konsistent, uanset om den udføres isoleret eller sammen med andre tests.

Denne idé gælder nøjagtigt det samme for integrationstests. Vi er nødt til at sikre, at vores app har en kendt (og gentagelig) tilstand, inden vi starter en ny test. Jo flere komponenter vi genbruger for at fremskynde tingene (appkontekst, DB'er, køer, filer ...), jo flere chancer for at få statsforurening.

Forudsat at vi gik all-in med klassearv, har vi nu et centralt sted at styre staten.

Lad os forbedre vores abstrakte klasse for at sikre, at vores app er i en kendt tilstand, inden vi kører tests.

I vores eksempel antager vi, at der er flere arkiver (fra forskellige datakilder) og en Wiremock server:

@SpringBootTest @ActiveProfiles ("test") @AutoConfigureWireMock (port = 8666) @AutoConfigureMockMvc abstrakt klasse AbstractSpringIntegrationTest {// ... forårsregler er konfigureret her, springet over for klarhed @Autowired beskyttet lateinit var wireMockServer: WireMockServer @ WirMockServer JdbcTemplate @Autowired lateinit var repos: Set @Autowired lateinit var cacheManager: CacheManager @Before fun resetState () {cleanAllDatabases () cleanAllCaches () resetWiremockStatus ()} fun cleanAllDatabases () {JdbcTestUtils.deleteFromTables (jdbcTemplate, "table1" "Tabel1" "Tabel1" " tabel1 ALTER COLUMN id RESTART WITH 1 ") repos.forEach {it.deleteAll ()}} fun cleanAllCaches () {cacheManager.cacheNames .map {cacheManager.getCache (it)} .filterNotNull () .forEach {it.clear () }} sjov resetWiremockStatus () {wireMockServer.resetAll () // angiv eventuelle standardanmodninger}}

4.8. Omdannelse til enhedstest

Dette er sandsynligvis et af de vigtigste punkter. Vi finder os igen og igen med nogle integrationstest, der faktisk udøver en politik på højt niveau i vores app.

Når vi finder nogle integrationstest, der tester en masse tilfælde af kerneforretningslogik, er det tid til at genoverveje vores tilgang og opdele dem i enhedstests.

Et muligt mønster her for at opnå dette med succes kan være:

  • Identificer integrationstest, der tester flere scenarier for kerneforretningslogik
  • Kopier pakken og refaktorerer kopien i enhedstest - på dette tidspunkt er vi muligvis også nødt til at nedbryde produktionskoden for at gøre den testbar
  • Få alle tests grønne
  • Efterlad en glædelig sti-prøve, der er bemærkelsesværdig nok i integrationspakken - vi bliver muligvis nødt til at omforme eller deltage og omforme et par
  • Fjern de resterende integrationstests

Michael Feathers dækker mange teknikker til at opnå dette og mere i at arbejde effektivt med Legacy Code.

5. Resume

I denne artikel havde vi en introduktion til integrationstests med fokus på forår.

Først talte vi om vigtigheden af ​​integrationstest, og hvorfor de er særlig relevante i forårsprogrammer.

Derefter opsummerede vi nogle værktøjer, der kan være nyttige til visse typer integrationstests i Web Apps.

Endelig gennemgik vi en liste over potentielle problemer, der sænker vores testudførelsestid samt tricks til at forbedre den.