Optimalizácia testov integrácie pružiny

1. Úvod

V tomto článku budeme viesť holistickú diskusiu o integračných testoch využívajúcich jar a o tom, ako ich optimalizovať.

Najprv stručne prediskutujeme dôležitosť integračných testov a ich miesto v modernom softvéri so zameraním na jarný ekosystém.

Neskôr sa budeme venovať viacerým scenárom so zameraním na webové aplikácie.

Ďalej prediskutujeme niekoľko stratégií na zlepšenie rýchlosti testovaniatým, že sa dozvieme o rôznych prístupoch, ktoré by mohli ovplyvniť spôsob formovania našich testov aj spôsob formovania samotnej aplikácie.

Predtým, ako začnete, je dôležité mať na pamäti, že toto je názorový článok založený na skúsenostiach. Niektoré z týchto vecí by vám mohli vyhovovať, iné nie.

Nakoniec tento článok používa Kotlin na vzorkovanie kódov, aby boli čo najstručnejšie, ale koncepty nie sú špecifické pre tento jazyk a útržky kódu by mali mať pre vývojárov Java a Kotlin zmysel.

2. Integračné testy

Integračné testy sú základnou súčasťou automatizovaných testovacích balíkov. Aj keď by nemali byť také početné ako jednotkové testy, ak sledujeme zdravú testovaciu pyramídu. Spoliehanie sa na rámce ako jar nás vedie k tomu, že potrebujeme značné množstvo testov integrácie, aby sme znížili riziko určitého správania nášho systému.

Čím viac zjednodušujeme náš kód pomocou jarných modulov (dátové, bezpečnostné, sociálne ...), tým väčšia je potreba integračných testov. To sa stáva obzvlášť pravdivým, keď presunieme kúsky a prvky našej infraštruktúry do @ Konfigurácia triedy.

Nemali by sme „testovať rámec“, ale mali by sme si určite overiť, či je rámec nakonfigurovaný tak, aby vyhovoval našim potrebám.

Testy integrácie nám pomáhajú budovať dôveru, ale majú svoju cenu:

  • To je pomalšia rýchlosť vykonávania, čo znamená pomalšie vytváranie
  • Integračné testy tiež znamenajú širší rozsah testovania, čo nie je vo väčšine prípadov ideálne

V tejto súvislosti sa pokúsime nájsť niekoľko riešení na zmiernenie vyššie uvedených problémov.

3. Testovanie webových aplikácií

Jar prináša niekoľko možností na testovanie webových aplikácií a väčšina vývojárov Spring ich pozná, sú to:

  • MockMvc: Vysmieva sa servletovému API, čo je užitočné pre nereaktívne webové aplikácie
  • TestRestTemplate: Dá sa použiť na našu aplikáciu, užitočné pre nereaktívne webové aplikácie, kde nie sú žiaduce vysmievané servlety
  • WebTestClient: Je testovací nástroj pre reaktívne webové aplikácie, a to ako s falošnými požiadavkami, tak s odpoveďami alebo so zásahom do skutočného servera

Pretože už máme články týkajúce sa týchto tém, nebudeme o nich tráviť čas rozhovorom.

Ak sa chcete prehĺbiť, neváhajte sa pozrieť.

4. Optimalizácia času vykonania

Integračné testy sú skvelé. Dodávajú nám dobrú mieru dôvery. Ak sú vhodne implementované, môžu veľmi zreteľne opísať zámer našej aplikácie s menším zosmiešňovaním a nastavením.

S vývojom našej aplikácie a pribúdajúcim vývojom sa však nevyhnutne zvyšuje čas budovania. S pribúdajúcim časom budovania sa môže stať nepraktickým pokračovať v testovaní zakaždým.

Potom ovplyvnenie našej spätnej väzby a nastúpenie na cestu najlepších vývojových postupov.

Integračné testy sú navyše zo svojej podstaty drahé. Naštartovanie nejakej perzistencie, zasielanie žiadostí prostredníctvom (aj keď nikdy neodídu localhost), alebo vykonanie nejakej IO jednoducho chce čas.

Je prvoradé sledovať náš čas na zostavenie vrátane vykonania testu. A na jar môžeme použiť niekoľko trikov, aby sme to udržali na nízkej úrovni.

V ďalších častiach sa venujeme niekoľkým bodom, ktoré nám pomôžu optimalizovať čas potrebný na zostavenie a tiež niektoré úskalia, ktoré môžu mať vplyv na jeho rýchlosť:

  • Múdre používanie profilov - aký vplyv majú profily na výkon
  • Prehodnotenie @MockBean - ako výsmech zasahuje výkon
  • Refaktoring @MockBean - alternatívy na zlepšenie výkonu
  • Dobre si premysli @DirtiesContext - užitočná, ale nebezpečná anotácia a ako ju nepoužívať
  • Používanie testovacích rezov - skvelý nástroj, ktorý vám môže pomôcť alebo sa vydať na cestu
  • Používanie dedičnosti tried - spôsob bezpečného usporiadania testov
  • Hospodárenie štátu - osvedčené postupy, ako zabrániť testom vločiek
  • Refaktoring do jednotkových testov - najlepší spôsob, ako získať solídne a pohotové zostavenie

Začnime!

4.1. Rozumné používanie profilov

Profily sú celkom elegantný nástroj. Konkrétne jednoduché značky, ktoré môžu povoliť alebo zakázať určité oblasti našej aplikácie. Dokonca by sme s nimi mohli implementovať vlajky funkcií!

Keď sa naše profily zbohatnú, je lákavé každú chvíľu vymeniť v našich integračných testoch. Existujú na to napríklad praktické nástroje @ActiveProfiles. Avšak zakaždým, keď urobíme test s novým profilom, novým ApplicationContext sa vytvorí.

Vytváranie kontextov aplikácií by mohlo byť svižné s aplikáciou na zavedenie vanilkovej pružiny, ktorá by neobsahovala nič. Pridajte ORM a niekoľko modulov a bude rýchlo stúpať na viac ako 7 sekúnd.

Pridajte veľa profilov a rozptýlite ich cez niekoľko testov. Získame rýchlo zostavenie viac ako 60 sekúnd (za predpokladu, že testy spustíme ako súčasť nášho zostavenia - a mali by sme).

Keď sa stretneme s dostatočne zložitou aplikáciou, opraviť to je skľučujúce. Ak však plánujeme vopred starostlivo, stane sa triviálne udržiavať rozumný čas na zostavenie.

Existuje niekoľko trikov, ktoré by sme mohli mať na pamäti, pokiaľ ide o profily v integračných testoch:

  • Vytvorte súhrnný profil, t.j. test, zahrňte všetky potrebné profily dovnútra - všade sa držte nášho testovacieho profilu
  • Navrhnite naše profily s ohľadom na testovateľnosť. Ak by sme nakoniec museli zmeniť profily, možno existuje lepšia cesta
  • Uveďte náš testovací profil na centralizovanom mieste - o tom si ešte povieme
  • Vyskúšajte všetky kombinácie profilov. Prípadne by sme mohli mať testovaciu sadu e2e pre každé prostredie testujúce aplikáciu s touto konkrétnou sadou profilov

4.2. Problémy s @MockBean

@MockBean je dosť silný nástroj.

Keď potrebujeme jarnú mágiu, ale chceme sa vysmievať konkrétnej súčasti, @MockBean príde naozaj vhod. Robí to však za cenu.

Vždy @MockBean sa objaví v triede, ApplicationContext cache bude označená ako špinavá, preto bežec vyčistí cache po vykonaní testovacej triedy. Čo opäť dodáva našej stavbe ďalších pár sekúnd.

Je to kontroverzný problém, ale mohlo by pomôcť pokus o použitie skutočnej aplikácie namiesto zosmiešňovania tohto konkrétneho scenára. Samozrejme tu nie je žiadna strieborná guľka. Hranice sa stierajú, keď si nedovolíme zosmiešňovať závislosti.

Mohli by sme si myslieť: Prečo by sme vytrvali, keď všetko, čo chceme testovať, je naša REST vrstva? Toto je spravodlivá otázka a vždy existuje kompromis.

Avšak s ohľadom na niekoľko princípov by sa to mohlo skutočne zmeniť na výhodu, ktorá povedie k lepšiemu dizajnu oboch testov a našej aplikácie a skracuje čas potrebný na testovanie.

4.3. Refaktoring @MockBean

V tejto časti sa pokúsime refaktorovať „pomalý“ test pomocou @MockBean aby bolo znova použité v medzipamäti ApplicationContext.

Predpokladajme, že chceme otestovať POST, ktorý vytvorí používateľa. Keby sme sa posmievali - pomocou @MockBean, mohli by sme jednoducho overiť, že naša služba bola zavolaná s pekne serializovaným používateľom.

Ak sme našu službu správne otestovali, tento prístup by mal stačiť:

trieda UsersControllerIntegrationTest: AbstractSpringIntegrationTest () {@Autowired lateinit var mvc: MockMvc @MockBean lateinit var userService: UserService @Test zábavné odkazy () {mvc.perform (post ("/ users") .contentType (MediaType.APPLIC) "" {"name": "jose"} "" ")). andExpect (status (). isCreated) verify (userService) .save (" jose ")}} rozhranie UserService {fun save (name: String)}

Chceme sa tomu vyhnúť @MockBean predsa. Takže nakoniec vydržíme entitu (za predpokladu, že to služba robí).

Najnaivnejším prístupom by tu bolo vyskúšať vedľajší efekt: Po POSTingu je môj používateľ v mojom DB, v našom príklade by to používalo JDBC.

To však porušuje hranice testovania:

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

V tomto konkrétnom príklade porušujeme hranice testovania, pretože s našou aplikáciou zaobchádzame ako s čiernou skrinkou HTTP, ktorá používateľa pošle, ale neskôr tvrdíme pomocou podrobností implementácie, to znamená, že náš používateľ bol v niektorých databázach pretrvávajúci.

Ak našu aplikáciu cvičíme prostredníctvom protokolu HTTP, môžeme výsledok presadiť aj prostredníctvom protokolu HTTP?

@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)}

Ak použijeme posledný prístup, má to niekoľko výhod:

  • Náš test začne rýchlejšie (je pravdepodobné, že jeho vykonanie bude trvať o niečo dlhšie, malo by sa vám však vrátiť)
  • Náš test tiež nevie o vedľajších účinkoch, ktoré nesúvisia s hranicami HTTP, tj. S databázami
  • Nakoniec náš test s jasnosťou vyjadruje zámer systému: Ak POST, budete môcť GET Users

To nemusí byť vždy možné z rôznych dôvodov:

  • Možno nebudeme mať koncový bod „vedľajšieho účinku“: Tu je možnosťou zvážiť vytvorenie „testovacích koncových bodov“.
  • Zložitosť je príliš vysoká na to, aby zasiahla celú aplikáciu: Tu je možné zvážiť segmenty (o nich si povieme neskôr).

4.4. Starostlivo premýšľať @DirtiesContext

Niekedy bude možno potrebné upraviť ApplicationContext v našich testoch. Pre tento scenár @DirtiesContext poskytuje presne túto funkcionalitu.

Z rovnakých dôvodov, ktoré sú uvedené vyššie, @DirtiesContext je mimoriadne drahý zdroj, pokiaľ ide o čas vykonania, a ako taký by sme mali byť opatrní.

Niektoré zneužitia @DirtiesContext zahŕňajú resetovanie medzipamäte aplikácie alebo do obnovenia pamäte DB. Existujú lepšie spôsoby, ako tieto scenáre zvládnuť v integračných testoch, a niektorým sa budeme venovať v ďalších častiach.

4.5. Používanie testovacích rezov

Testovacie rezy sú funkciou Spring Boot predstavenou v 1.4. Myšlienka je pomerne jednoduchá, Spring vytvorí redukovaný kontext aplikácie pre konkrétny výrez vašej aplikácie.

Rámec sa tiež postará o konfiguráciu minima.

V krabici je v Spring Boot k dispozícii rozumné množstvo plátkov a môžeme si vytvoriť aj vlastné:

  • @JsonTest: Registruje príslušné komponenty JSON
  • @DataJpaTest: Registruje fazule JPA vrátane dostupných ORM
  • @JdbcTest: Užitočné pre nespracované testy JDBC, stará sa o zdroj údajov a v pamäťových databázach bez ozdôb ORM
  • @DataMongoTest: Pokúša sa poskytnúť nastavenie testovania monga v pamäti
  • @WebMvcTest: Falošný testovací rez MVC bez zvyšku aplikácie
  • ... (môžeme skontrolovať zdroj, aby sme ich našli všetky)

Táto konkrétna vlastnosť, ak je používaná múdro, nám môže pomôcť zostaviť úzke testy bez tak veľkého trestu z hľadiska výkonu, najmä pre malé a stredné aplikácie.

Ak sa však naša aplikácia neustále rozrastá, hromadí sa tiež, pretože vytvára jeden (malý) kontext aplikácie na jeden rez.

4.6. Používanie dedenia triedy

Pomocou jediného AbstractSpringIntegrationTest triedy ako rodič všetkých našich integračných testov je jednoduchý, výkonný a pragmatický spôsob, ako udržať rýchle zostavenie.

Ak zabezpečíme spoľahlivé nastavenie, náš tím ho jednoducho rozšíri s vedomím, že všetko „jednoducho funguje“. Takto sa nebudeme môcť starať o správu stavu alebo konfiguráciu rámca a sústrediť sa na daný problém.

Mohli by sme tam nastaviť všetky požiadavky na test:

  • Jarný bežec - alebo radšej vládne, ak by sme neskôr potrebovali ďalších bežcov
  • profily - ideálne náš agregát test profilu
  • initial config - nastavenie stavu našej aplikácie

Pozrime sa na jednoduchú základnú triedu, ktorá sa stará o predchádzajúce body:

@SpringBootTest @ActiveProfiles ("test") abstraktná trieda AbstractSpringIntegrationTest {@Rule @JvmField val springMethodRule = SpringMethodRule () sprievodný objekt {@ClassRule @JvmField val SPRING_CLASS_RULE = SpringClassRule ()}}

4.7. Štátny manažment

Je dôležité pamätať na to, odkiaľ pochádza jednotka v teste jednotky. Jednoducho povedané, znamená to, že môžeme kedykoľvek spustiť jeden test (alebo podmnožinu), aby sme dosiahli konzistentné výsledky.

Štát by preto mal byť čistý a známy pred začiatkom každej skúšky.

Inými slovami, výsledok testu by mal byť konzistentný bez ohľadu na to, či sa vykonáva izolovane alebo spolu s inými testami.

Táto myšlienka platí rovnako pre integračné testy. Pred spustením nového testu sa musíme ubezpečiť, že naša aplikácia má známy (a opakovateľný) stav. Čím viac komponentov znova použijeme na zrýchlenie (kontext aplikácie, databázy, fronty, súbory ...), tým viac šancí na znečistenie štátu.

Za predpokladu, že sme šli všetci do triedy s dedičstvom, máme teraz centrálne miesto na správu stavu.

Poďme vylepšiť našu abstraktnú triedu, aby sme sa pred spustením testov ubezpečili, že naša aplikácia je v známom stave.

V našom príklade budeme predpokladať, že existuje niekoľko úložísk (z rôznych zdrojov údajov) a Wiremock server:

@SpringBootTest @ActiveProfiles ("test") @AutoConfigureWireMock (port = 8666) @AutoConfigureMockMvc abstraktná trieda AbstractSpringIntegrationTest {// ... tu sú nakonfigurované jarné pravidlá, pre presnosť vynechané @Autowired protected lateinit var wireMockServer: WireMockServer: WireMockServer: JdbcTemplate @Autowired lateinit var repos: Set @Autowired lateinit var cacheManager: CacheManager @Before zábava resetState () {cleanAllDatabases () cleanAllCaches () resetWiremockStatus ()} zábava cleanAllDatabases () {JdbcTestUtils.deleteFromTables (jdbcTemplate, "table1" "" table1 "" tabuľka1 ALTER COLUMN id RESTART WITH 1 ") repos.forEach {it.deleteAll ()}} zábava cleanAllCaches () {cacheManager.cacheNames. mapa {cacheManager.getCache (it)} .filterNotNull () .forEach {it.clear () }} zábava resetWiremockStatus () {wireMockServer.resetAll () // nastaviť predvolené požiadavky, ak existujú}}

4.8. Refaktoring do jednotkových testov

Toto je pravdepodobne jeden z najdôležitejších bodov. Znovu a znovu sa ocitneme pri niektorých integračných testoch, ktoré skutočne uplatňujú určitú politiku našej aplikácie na vysokej úrovni.

Kedykoľvek nájdeme nejaké integračné testy testujúce množstvo prípadov základnej obchodnej logiky, je čas prehodnotiť náš prístup a rozdeliť ich do jednotkových testov.

Možným vzorom, ako to úspešne dosiahnuť, by mohol byť:

  • Identifikujte integračné testy, ktoré testujú viaceré scenáre základnej obchodnej logiky
  • Duplikujte balík a prekopírujte kópiu na jednotku Testy - v tejto fáze bude pravdepodobne potrebné rozdeliť produkčný kód, aby bol testovateľný
  • Získajte všetky testy zelené
  • V integračnom balíku nechajte ukážku šťastnej cesty, ktorá je dostatočne pozoruhodná - možno budeme musieť niekoľko refaktorovať alebo sa pripojiť a zmeniť tvar
  • Odstráňte zvyšné testy integrácie

Michael Feathers popisuje mnoho techník na dosiahnutie tohto a ešte viac v časti Efektívna práca so starým kódom.

5. Zhrnutie

V tomto článku sme mali úvod do integračných testov so zameraním na jar.

Najprv sme hovorili o dôležitosti integračných testov a o tom, prečo sú obzvlášť dôležité v jarných aplikáciách.

Potom sme zhrnuli niektoré nástroje, ktoré by sa mohli hodiť pre určité typy testov integrácie vo webových aplikáciách.

Nakoniec sme prešli zoznamom potenciálnych problémov, ktoré spomaľujú čas vykonávania testu, a tiež trikmi na jeho zlepšenie.


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