Optimizacija proljetnih integracijskih testova

1. Uvod

U ovom ćemo članku imati holističku raspravu o integracijskim testovima pomoću Springa i kako ih optimizirati.

Prvo ćemo ukratko razgovarati o važnosti integracijskih testova i njihovom mjestu u modernom softveru koji se fokusira na ekosustav Proljeće.

Kasnije ćemo pokriti više scenarija, usredotočujući se na web-aplikacije.

Dalje ćemo razgovarati o nekim strategijama za poboljšanje brzine testiranja, učenjem o različitim pristupima koji bi mogli utjecati i na način oblikovanja testova i na način oblikovanja same aplikacije.

Prije nego što započnete, važno je imati na umu da je ovo članak koji se temelji na iskustvu. Neke od ovih stvari mogu vam odgovarati, neke možda ne.

Napokon, ovaj članak koristi Kotlin za uzorke koda kako bi bili što sažetiji, ali koncepti nisu specifični za ovaj jezik, a isječci koda trebali bi se osjećati smisleno i za programere Java i Kotlin.

2. Integracijski testovi

Integracijski testovi temeljni su dio automatiziranih programskih paketa. Iako ne bi trebali biti toliko brojni kao jedinični testovi ako slijedimo zdravu piramidu ispitivanja. Oslanjajući se na okvire poput Proljeća, trebamo priličnu količinu integracijskog testiranja kako bismo riskirali određena ponašanja našeg sustava.

Što više pojednostavljujemo svoj kod korištenjem Spring modula (podatkovni, sigurnosni, društveni ...), to je veća potreba za integracijskim testovima. To postaje osobito istinito kada preselimo dijelove naše infrastrukture @Konfiguracija razreda.

Ne bismo trebali "testirati okvir", ali svakako bismo trebali provjeriti je li okvir konfiguriran da zadovoljava naše potrebe.

Integracijski testovi pomažu nam u izgradnji povjerenja, ali imaju svoju cijenu:

  • To je sporija brzina izvršavanja, što znači sporije građe
  • Također, integracijski testovi podrazumijevaju širi opseg ispitivanja koji u većini slučajeva nije idealan

Imajući ovo na umu, pokušat ćemo pronaći neka rješenja za ublažavanje gore spomenutih problema.

3. Testiranje web aplikacija

Spring donosi nekoliko mogućnosti za testiranje web aplikacija, a većina programera Springa poznaje ih, a to su:

  • MockMvc: Ismijava API servleta, koristan za nereaktivne web-aplikacije
  • TestRestTemplate: Može se koristiti usmjeravanjem na našu aplikaciju, korisno za nereaktivne web aplikacije u kojima ismijani servleti nisu poželjni
  • WebTestClient: Je li alat za testiranje reaktivnih web aplikacija, s ismijanim zahtjevima / odgovorima ili pogađanjem pravog poslužitelja

Kako već imamo članke koji pokrivaju ove teme, nećemo trošiti vrijeme na razgovor o njima.

Slobodno pogledajte ako želite dublje kopati.

4. Optimiziranje vremena izvršenja

Integracijski testovi su izvrsni. Daju nam dobar stupanj samopouzdanja. Također ako se primjenjuju na odgovarajući način, oni mogu na vrlo jasan način opisati namjeru naše aplikacije, uz manje ruganja i buke prilikom postavljanja.

Međutim, kako naša aplikacija sazrijeva, a razvoj se gomila, vrijeme izrade neizbježno raste. Kako se vrijeme izrade povećava, moglo bi postati nepraktično svaki put izvoditi sve testove.

Nakon toga, utječući na našu petlju povratnih informacija i krećući se putem najboljih razvojnih praksi.

Nadalje, integracijski testovi sami su po sebi skupi. Pokretanje neke vrste upornosti, slanje zahtjeva (čak i ako nikad ne odu lokalnihost), ili za neko IO jednostavno treba vremena.

Najvažnije je pripaziti na vrijeme izrade, uključujući izvršenje testa. A postoje i neki trikovi koje možemo primijeniti u proljeće kako bismo bili niski.

U sljedećim odjeljcima pokrivat ćemo nekoliko točaka koje će nam pomoći da optimiziramo vrijeme izrade, kao i neke zamke koje bi mogle utjecati na njegovu brzinu:

  • Mudra uporaba profila - kako profili utječu na performanse
  • Preispitivanje @MockBean - kako ruganje pogađa izvedbu
  • Refaktoriranje @MockBean - alternative za poboljšanje performansi
  • Pažljivo razmišljajući o @DirtiesContext - korisna, ali opasna napomena i kako je ne koristiti
  • Korištenje testnih kriški - super alat koji vam može pomoći ili krenuti
  • Korištenje nasljeđivanja klasa - način za sigurnu organizaciju testova
  • Državno upravljanje - dobre prakse za izbjegavanje neuobičajenih testova
  • Refaktoriranje u jedinične testove - najbolji način za dobivanje čvrste i brze građe

Započnimo!

4.1. Mudro korištenje profila

Profili su prilično uredan alat. Naime, jednostavne oznake koje mogu omogućiti ili onemogućiti određena područja naše aplikacije. S njima bismo čak mogli implementirati i značajne zastavice!

Kako se naši profili obogaćuju, primamljivo je svako malo mijenjati se u našim integracijskim testovima. Postoje prikladni alati za to, poput @ActiveProfiles. Međutim, svaki put kad povučemo test s novim profilom, novim ApplicationContext stvara se.

Stvaranje konteksta aplikacije moglo bi biti brzo s aplikacijom za pokretanje vanilije s proljećem u kojoj nema ničega. Dodajte ORM i nekoliko modula i on će brzo narasti na 7+ sekundi.

Dodajte gomilu profila i raštrkajte ih kroz nekoliko testova, a mi ćemo brzo dobiti 60+ sekundi izrade (pod pretpostavkom da pokrećemo testove kao dio naše gradnje - i trebali bismo).

Jednom kad se suočimo s dovoljno složenom aplikacijom, popravljanje je zastrašujuće. Međutim, ako unaprijed pažljivo planiramo, postaće trivijalno zadržati razumno vrijeme izrade.

Postoji nekoliko trikova koje bismo mogli imati na umu kada je riječ o profilima u integracijskim testovima:

  • Stvorite zbirni profil, tj. test, uključite sve potrebne profile unutar - držite se našeg testnog profila svugdje
  • Dizajnirajte naše profile imajući na umu provjerljivost. Ako na kraju moramo zamijeniti profile, možda postoji bolji način
  • Navedite naš testni profil na centraliziranom mjestu - o tome ćemo kasnije
  • Izbjegavajte testiranje svih kombinacija profila. Alternativno, mogli bismo imati e2e test-suite po okruženju koji testira aplikaciju s tim određenim skupom profila

4.2. Problemi s @MockBean

@MockBean je prilično moćan alat.

Kada trebamo proljetnu čaroliju, ali želimo se rugati određenoj komponenti, @MockBean stvarno dobro dođe. Ali to čini po cijeni.

Svaki put @MockBean pojavljuje se u klasi, ApplicationContext predmemorija se označava kao prljava, stoga će pokretač očistiti predmemoriju nakon završetka test-klase. Što opet dodaje dodatnu hrpu sekundi našoj gradnji.

Ovo je kontroverzno, ali pokušaj vježbanja stvarne aplikacije umjesto ruganja ovom konkretnom scenariju mogao bi vam pomoći. Naravno, ovdje nema srebrnog metka. Granice postaju mutne kad si ne dopustimo rugati se ovisnostima.

Mogli bismo pomisliti: Zašto bismo ustrajali kad je sve što želimo testirati naš REST sloj? Ovo je pošteno i uvijek postoji kompromis.

No, imajući na umu nekoliko principa, ovo se zapravo može pretvoriti u prednost koja dovodi do boljeg dizajna i testova i naše aplikacije i smanjuje vrijeme testiranja.

4.3. Refaktoriranje @MockBean

U ovom ćemo odjeljku pokušati refaktorirati 'polagani' test pomoću @MockBean kako bi se ponovno koristilo predmemorirano ApplicationContext.

Pretpostavimo da želimo testirati POST koji stvara korisnika. Kad bismo se rugali - koristeći @MockBean, mogli bismo jednostavno provjeriti je li naša usluga pozvana s lijepo serializiranim korisnikom.

Ako smo ispravno testirali našu uslugu, ovaj pristup trebao bi biti dovoljan:

klasa UsersControllerIntegrationTest: AbstractSpringIntegrationTest () {@Autowired lateinit var mvc: MockMvc @MockBean lateinit var userService: UserService @Test fun links () {mvc.perform (post ("/ users") .contentType (MediaTypeJAPPL. "" {"name": "jose"} "" "))) .andExpect (status (). isCreated) verify (userService) .save (" jose ")}} UserService sučelja {fun save (name: String)}

Želimo izbjeći @MockBean iako. Tako ćemo na kraju ustrajati na entitetu (pod pretpostavkom da to usluga radi).

Najnaivniji pristup ovdje bio bi testiranje nuspojave: Nakon POSTINGA, moj se korisnik nalazi u mom DB-u, u našem primjeru ovo bi koristilo JDBC.

To, međutim, krši granice testiranja:

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

U ovom konkretnom primjeru kršimo ograničenja testiranja jer svoju aplikaciju tretiramo kao HTTP crni okvir za slanje korisnika, ali kasnije tvrdimo koristeći detalje implementacije, to jest, naš je korisnik ustrajan u nekom DB-u.

Ako svoju aplikaciju vježbamo putem HTTP-a, možemo li rezultat potvrditi i putem HTTP-a?

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

Nekoliko je prednosti ako slijedimo posljednji pristup:

  • Naš test započet će brže (možda će trebati malo više vremena da se izvrši, ali trebao bi se vratiti)
  • Također, naš test nije svjestan nuspojava koje nisu povezane s HTTP granicama, tj. DB-ovima
  • Konačno, naš test jasno izražava namjeru sustava: Ako POSTAVLJATE, moći ćete DOBITI korisnike

Naravno, to možda nije uvijek moguće iz različitih razloga:

  • Možda ne bismo imali krajnju točku "nuspojava": Ovdje je opcija razmotriti stvaranje "testiranja krajnjih točaka"
  • Složenost je previsoka da bi pogodila cijelu aplikaciju: Ovdje je opcija uzeti u obzir kriške (o njima ćemo kasnije)

4.4. Pažljivo razmišljajući o tome @DirtiesContext

Ponekad ćemo možda trebati izmijeniti ApplicationContext u našim testovima. Za ovaj scenarij, @DirtiesContext pruža upravo tu funkcionalnost.

Iz istih gore izloženih razloga, @DirtiesContext je izuzetno skup resurs što se tiče vremena izvršenja, i kao takvi, trebali bismo biti oprezni.

Neke zlouporabe @DirtiesContext uključuju resetiranje predmemorije aplikacije ili resetiranje DB memorije. Postoje bolji načini za rukovanje tim scenarijima u integracijskim testovima, a neke ćemo obraditi u daljnjim odjeljcima.

4.5. Korištenje testnih kriški

Test Slices su značajka Spring Boot predstavljena u verziji 1.4. Ideja je prilično jednostavna, Spring će stvoriti smanjeni kontekst aplikacije za određeni dio vaše aplikacije.

Također, okvir će se pobrinuti za konfiguriranje minimuma.

Dostupan je razuman broj kriški u kutiji u Spring Boot-u, a možemo stvoriti i svoje:

  • @JsonTest: Registrira JSON relevantne komponente
  • @DataJpaTest: Registrira JPA grah, uključujući dostupni ORM
  • @JdbcTest: Korisno za sirove JDBC testove, brine se o izvoru podataka i u memorijskim DB-ovima bez ORM naknada
  • @DataMongoTest: Pokušava pružiti postavke mongo testiranja u memoriji
  • @WebMvcTest: Lažni MVC odsječak za testiranje bez ostatka aplikacije
  • ... (možemo provjeriti izvor da bismo ih sve pronašli)

Ova se značajka ako se pametno koristi može nam pomoći da napravimo uske testove bez tako velike kazne u smislu izvedbe, posebno za male / srednje aplikacije.

Međutim, ako naša aplikacija neprestano raste, ona se također gomila jer stvara jedan (mali) kontekst aplikacije po krišku.

4.6. Korištenje nasljeđivanja klase

Koristeći singl AbstractSpringIntegrationTest klasa kao roditelj svih naših integracijskih testova jednostavan je, moćan i pragmatičan način održavanja brze gradnje.

Ako pružimo solidnu postavku, naš će je tim jednostavno proširiti, znajući da sve ‘samo funkcionira’. Na taj se način možemo manje brinuti oko upravljanja stanjem ili konfiguriranja okvira i usredotočiti se na problem koji je u pitanju.

Tamo bismo mogli postaviti sve zahtjeve za ispitivanje:

  • Proljetni trkač - ili po mogućnosti pravila, u slučaju da nam kasnije trebaju drugi trkači
  • profili - idealno naš agregat test profil
  • početna konfiguracija - postavljanje stanja naše aplikacije

Pogledajmo jednostavnu osnovnu klasu koja se brine o prethodnim točkama:

@SpringBootTest @ActiveProfiles ("test") sažetak klase AbstractSpringIntegrationTest {@Rule @JvmField val springMethodRule = SpringMethodRule () prateći objekt {@ClassRule @JvmField val SPRING_CLASS_RULE = SpringClassRule ()}}

4.7. Upravljanje državom

Važno je zapamtiti odakle dolazi "jedinica" u Unit Test-u. Jednostavno rečeno, to znači da možemo pokrenuti jedan test (ili podskup) u bilo kojem trenutku da bismo dobili dosljedne rezultate.

Stoga bi država trebala biti čista i poznata prije svakog ispitivanja.

Drugim riječima, rezultat testa trebao bi biti dosljedan bez obzira na to je li izveden izolirano ili zajedno s drugim testovima.

Ova ideja jednako se odnosi na integracijske testove. Moramo osigurati da naša aplikacija ima poznato (i ponovljivo) stanje prije pokretanja novog testa. Što više komponenata ponovno upotrijebimo za ubrzavanje stvari (kontekst aplikacije, DB-ovi, redovi, datoteke ...), to je više šansi za zagađenje države.

Pod pretpostavkom da smo svi išli s nasljeđivanjem razreda, sada imamo centralno mjesto za upravljanje državom.

Poboljšajmo našu apstraktnu klasu kako bismo bili sigurni da je naša aplikacija u poznatom stanju prije pokretanja testova.

U našem primjeru pretpostavit ćemo da postoji nekoliko spremišta (iz različitih izvora podataka) i a Wiremock poslužitelj:

@SpringBootTest @ActiveProfiles ("test") @AutoConfigureWireMock (port = 8666) @AutoConfigureMockMvc abstraktna klasa AbstractSpringIntegrationTest {// ... pravila proljeća ovdje su konfigurirana, preskočena za jasnost @Autowired protected lateinit varinMockToverMerckToverMerckOutMerckToverMerckOutMercVockMerckOutMerckOutMerCut JdbcTemplate @Autowired lateinit var repos: Postavljeno @Autowired lateinit var cacheManager: CacheManager @Before fun resetState () {cleanAllDatabases () cleanAllCaches () resetWiremockStatus ()} fun cleanAllDatabases () {JdbcTestUtils.deleteFromTables (jdbcTemplate, jdbcTemplate, jdbcTemplate, jdbcTemplate, jdbcTemplate, jdbcTemplate table1 ALTER COLUMN id RESTART WITH 1 ") repos.forEach {it.deleteAll ()}} fun cleanAllCaches () {cacheManager.cacheNames .map {cacheManager.getCache (it)} .filterNotNull () .forEach {it.clear () }} zabava resetWiremockStatus () {wireMockServer.resetAll () // postavlja zadane zahtjeve ako postoje}}

4.8. Refaktoriranje u jedinične testove

To je vjerojatno jedna od najvažnijih točaka. Naći ćemo se uvijek iznova s ​​nekim integracijskim testovima koji zapravo provode neke politike naše aplikacije na visokoj razini.

Kad god pronađemo neke integracijske testove koji testiraju gomilu slučajeva osnovne poslovne logike, vrijeme je da preispitamo svoj pristup i razbijemo ih na jedinstvene testove.

Mogući obrazac ovdje da se to uspješno postigne mogao bi biti:

  • Utvrdite integracijske testove koji testiraju više scenarija osnovne poslovne logike
  • Duplicirajte paket i prepravite kopiju u jedinstvene testove - u ovoj bismo fazi možda trebali razbiti i proizvodni kôd kako bismo ga učinili provjerljivim
  • Neka svi testovi budu zeleni
  • Ostavite uzorak sretnog puta koji je izvanredan u integracijskom paketu - možda ćemo trebati refaktorirati ili se pridružiti i preoblikovati nekoliko
  • Uklonite preostale integracijske testove

Michael Feathers pokriva mnoge tehnike kako bi se to postiglo, a i više u programu Učinkovit rad s naslijeđenim kodom.

5. Sažetak

U ovom smo članku imali uvod u integracijske testove s naglaskom na proljeće.

Prvo smo razgovarali o važnosti integracijskih testova i zašto su oni posebno relevantni u proljetnim aplikacijama.

Nakon toga saželi smo neke alate koji bi mogli dobro doći za određene vrste integracijskih testova u web aplikacijama.

Na kraju smo prošli popis potencijalnih problema koji usporavaju vrijeme izvršavanja testa, kao i trikove za njegovo poboljšanje.