A tavaszi integrációs tesztek optimalizálása

1. Bemutatkozás

Ebben a cikkben holisztikus vitát folytatunk a Spring segítségével végzett integrációs tesztekről és azok optimalizálásáról.

Először röviden megvitatjuk az integrációs tesztek fontosságát és helyüket a modern szoftverekben, különös tekintettel a tavaszi ökoszisztémára.

Később több forgatókönyvre is kitérünk, különös tekintettel a webalkalmazásokra.

Ezután megvizsgálunk néhány stratégiát a tesztelés sebességének javítására, különféle megközelítések megismerésével, amelyek befolyásolhatják mind a tesztek formálását, mind pedig magát az alkalmazást.

A kezdés előtt fontos szem előtt tartani, hogy ez egy tapasztalatokon alapuló véleménycikk. Néhány ilyen dolog megfelelhet neked, néhány nem.

Végül, ez a cikk a Kotlin-t használja a kódmintákhoz, hogy a lehető legtömörebbek legyenek, de a fogalmak nem csak erre a nyelvre vonatkoznak, és a kódrészleteknek értelmesnek kell lenniük a Java és a Kotlin fejlesztők számára is.

2. Integrációs tesztek

Az integrált tesztek az automatizált tesztkészletek alapvető részét képezik. Bár nem lehet olyan sok, mint egységvizsgálat, ha egy egészséges tesztpiramist követünk. Az olyan keretrendszerekre támaszkodva, mint például a Spring, elegendő mennyiségű integrációs tesztre van szükségünk ahhoz, hogy kockáztassuk rendszerünk bizonyos viselkedését.

Minél jobban leegyszerűsítjük kódunkat a Spring modulok (adatok, biztonság, szociális stb.) Használatával, annál nagyobb szükség van az integrációs tesztekre. Ez különösen akkor válik igazsá, amikor infrastruktúránk bitjeit és bobjait áthelyezzük @ Konfiguráció osztályok.

Nem szabad „tesztelni a keretrendszert”, de mindenképpen ellenőriznünk kell, hogy a keretrendszer úgy van-e konfigurálva, hogy megfeleljen az igényeinknek.

Az integrációs tesztek segítenek a bizalom kialakításában, de ennek ára van:

  • Ez lassabb végrehajtási sebesség, ami lassabb összeépítést jelent
  • Az integrációs tesztek tágabb tesztkört is magában foglalnak, ami a legtöbb esetben nem ideális

Ezt szem előtt tartva megpróbálunk néhány megoldást találni a fent említett problémák enyhítésére.

3. Webalkalmazások tesztelése

A tavasz néhány lehetőséget kínál a webalkalmazások tesztelésére, és a Spring fejlesztők többsége ismeri ezeket:

  • MockMvc: A szervlet API kigúnyolása, hasznos a nem reaktív webalkalmazásokhoz
  • TestRestTemplate: Alkalmazásunkra mutatva használható, hasznos a nem reaktív webalkalmazásokhoz, ahol nem kívánatos a csúfolt szervlet
  • WebTestClient: Tesztelő eszköz a reaktív webalkalmazások számára, mind csúfolt kérésekkel / válaszokkal, mind valódi kiszolgálóval

Mivel már vannak cikkek, amelyek ezeket a témákat tárgyalják, nem fogunk időt tölteni róluk.

Vessen egy pillantást, ha mélyebbre szeretne ásni.

4. A végrehajtási idő optimalizálása

Az integrációs tesztek nagyszerűek. Jó fokú bizalmat adnak nekünk. Megfelelő megvalósítás esetén is nagyon világos módon, kevésbé gúnyolódással és beállítási zajjal tudják leírni alkalmazásunk szándékát.

Ahogy azonban az alkalmazásunk beérik és a fejlesztés halmozódik, az építési idő elkerülhetetlenül felmegy. A beépítési idő növekedésével kivitelezhetetlenné válik az összes teszt minden egyes futtatása.

Ezt követően befolyásolja a visszacsatolási ciklusunkat, és eljut a legjobb fejlesztési gyakorlatok útján.

Ezenkívül az integrációs tesztek eredendően drágák. Valamilyen kitartás indítása, kérelmek továbbítása (még akkor is, ha soha nem mennek el) helyi kiszolgáló), vagy valamilyen IO elvégzése egyszerűen időt vesz igénybe.

Rendkívül fontos, hogy figyelemmel kísérjük a gyártási időnket, beleértve a teszt végrehajtását is. És van néhány trükk, amelyet tavasszal alkalmazhatunk, hogy alacsonyan tartsuk.

A következő szakaszokban bemutatunk néhány pontot, amelyek segítenek optimalizálni az építési időnket, valamint néhány buktatót, amelyek befolyásolhatják annak sebességét:

  • A profilok intelligens használata - hogyan befolyásolják a profilok a teljesítményt
  • Átgondolás @MockBean - hogy gúnyolva éri el a teljesítményt
  • Refaktorálás @MockBean - alternatívák a teljesítmény javítására
  • Alaposan átgondolva a @DirtiesContext - hasznos, de veszélyes megjegyzés és hogyan ne használjuk
  • Tesztszeletek használata - egy jó eszköz, amely segíthet vagy utunkra állhat
  • Az osztályos öröklés használata - a tesztek biztonságos megszervezésének módja
  • Államigazgatás - bevált gyakorlatok a pelyhes tesztek elkerülésére
  • Átdolgozás egység tesztekbe - ez a legjobb módszer a szilárd és frappáns felépítés megszerzésére

Kezdjük el!

4.1. A profilok intelligens használata

A profilok nagyon ügyesek. Mégpedig olyan egyszerű címkék, amelyek lehetővé teszik vagy letilthatják alkalmazásunk bizonyos területeit. Akár jellemző zászlókat is megvalósíthatnánk velük!

Amint a profiljaink egyre gazdagabbak, csábító az időnként cserélgetés integrációs tesztjeink során. Ehhez vannak kényelmes eszközök, például @ActiveProfiles. Azonban, valahányszor új profilú, új tesztet húzunk ApplicationContext létrejön.

Alkalmazáskörnyezet létrehozása elcsépelhet egy vanília rugós csomagtartó alkalmazást, amelyben nincs semmi. Adjon hozzá egy ORM-et és néhány modult, és gyorsan megemelkedik 7+ másodpercig.

Adjon hozzá egy csomó profilt, és szétszórja őket néhány teszten, és gyorsan megszerezzük a 60+ másodperces összeállítást (ha feltételezzük, hogy teszteket futtatunk az összeállításunk részeként - és ezt kellene is tennünk).

Miután elég összetett alkalmazással nézünk szembe, ennek kijavítása félelmetes. Ha azonban előre gondosan megtervezzük, elenyészővé válik az ésszerű gyártási idő megtartása.

Néhány trükköt tudunk szem előtt tartani, amikor az integrációs tesztek profiljairól van szó:

  • Hozzon létre összesített profilt, azaz teszt, minden szükséges profilt tartalmazzon belül - tartsa be a tesztprofilunkat mindenhol
  • Tervezze meg profiljainkat a tesztelhetőség szem előtt tartásával. Ha végül profilokat kell váltanunk, lehet, hogy van egy jobb módszer
  • Közölje központunkban tesztprofilunkat - erről később beszélünk
  • Kerülje az összes profilkombináció tesztelését. Alternatív megoldásként rendelkezhetnénk környezetenként e2e tesztcsomaggal, amely az alkalmazást az adott profilkészlettel tesztelné

4.2. A problémák @MockBean

@MockBean egy elég hatékony eszköz.

Amikor szükségünk van egy kis tavaszi varázslatra, de meg akarunk csúfolni egy adott komponenst, @MockBean nagyon jól jön. De ezt áron teszi.

Mindig @MockBean egy osztályban jelenik meg, a ApplicationContext a gyorsítótár piszkosként kerül megjelölésre, ezért a futó megtisztítja a gyorsítótárat a tesztosztály elvégzése után. Ez ismét extra csomó másodpercet ad az építkezésünkhöz.

Ez ellentmondásos, de segíthet, ha megpróbálja kipróbálni a tényleges alkalmazást ahelyett, hogy kigúnyolná az adott forgatókönyvet. Természetesen itt nincs ezüst golyó. A határok elmosódnak, ha nem engedjük meg magunknak a függőségek kigúnyolását.

Gondolhatnánk: Miért tartanánk fenn, amikor csak a REST rétegünket akarjuk tesztelni? Ez igazságos szempont, és mindig van kompromisszum.

Néhány elv figyelembevételével azonban ez valóban előnnyé válhat, amely mind a tesztek, mind az alkalmazásunk jobb megtervezéséhez vezet, és csökkenti a tesztelés idejét.

4.3. Refaktorálás @MockBean

Ebben a szakaszban megpróbálunk átalakítani egy „lassú” tesztet @MockBean hogy újra felhasználja a gyorsítótárat ApplicationContext.

Tegyük fel, hogy tesztelni szeretnénk egy felhasználót létrehozó POST-ot. Ha gúnyolódnánk - használnánk @MockBean, egyszerűen ellenőrizhetjük, hogy szolgáltatásunkat szépen sorosított felhasználóval hívták-e meg.

Ha megfelelően teszteltük szolgáltatásunkat, akkor ennek a megközelítésnek elegendőnek kell lennie:

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

El akarjuk kerülni @MockBean bár. Így végül kitartunk az entitás mellett (feltéve, hogy a szolgáltatás ezt teszi).

A legnaivabb megközelítés itt a mellékhatás tesztelése lenne: A POST-ot követően a felhasználóm a DB-n van, példánkban ez a JDBC-t használja.

Ez azonban sérti a tesztelési határokat:

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

Ebben a konkrét példában megsértjük a tesztelési határokat, mert az alkalmazást HTTP fekete dobozként kezeljük a felhasználó elküldéséhez, de később a megvalósítás részleteinek felhasználásával állítunk, vagyis a felhasználónk továbbra is fennmaradt néhány DB-ben.

Ha az alkalmazásunkat HTTP-n keresztül gyakoroljuk, akkor az eredményt HTTP-n keresztül is érvényesíthetjük?

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

Van néhány előnye, ha az utolsó megközelítést követjük:

  • A tesztünk gyorsabban indul (vitathatatlanul egy kicsit tovább tarthat a végrehajtása, de meg kell térülnie)
  • Tesztünk nem ismeri a HTTP-határokkal, azaz a DB-kkel nem kapcsolatos mellékhatásokat
  • Végül tesztünk világosan kifejezi a rendszer szándékát: Ha POST-ot küld, akkor megszerezheti a felhasználókat

Természetesen ez nem mindig lehetséges különböző okok miatt:

  • Előfordulhat, hogy nincs meg a „mellékhatás” végpont: Itt lehetőség lehet megfontolni a „tesztelési végpontok” létrehozását.
  • A komplexitás túl nagy ahhoz, hogy elérje az egész alkalmazást: Itt lehetőség van a szeletek figyelembevételére (később beszélünk róluk)

4.4. Gondosan gondolkodni @DtiesContext

Előfordulhat, hogy módosítanunk kell a ApplicationContext tesztjeinkben. Ennél a forgatókönyvnél @DtiesContext pontosan ezt a funkciót biztosítja.

Ugyanezen okok miatt @DtiesContext rendkívül költséges erőforrás, amikor a végrehajtási időről van szó, és mint ilyen, vigyáznunk kell.

Néhány visszaélés @DtiesContext tartalmazza az alkalmazás gyorsítótárának visszaállítását vagy a memóriában a DB visszaállításokat. Vannak jobb módszerek ezeknek a szcenárióknak az integrációs tesztekben történő kezelésére, és néhányukkal a következő szakaszokban foglalkozunk.

4.5. Tesztszeletek használata

A Test Slices egy Spring Boot funkció, amelyet az 1.4-es verzióban vezettek be. Az ötlet meglehetősen egyszerű, a Spring csökkentett alkalmazási kontextust hoz létre az alkalmazás egy adott szeletéhez.

Ezenkívül a keretrendszer gondoskodik a minimális beállításáról.

Ésszerű számú szelet kapható a Spring Boot dobozában, és mi is létrehozhatunk sajátot:

  • @JsonTest: Regisztrálja a JSON releváns összetevőit
  • @DataJpaTest: Regisztrálja a JPA babot, beleértve a rendelkezésre álló ORM-et is
  • @JdbcTest: Hasznos a nyers JDBC tesztekhez, gondoskodik az adatforrásról és a memória DB-kről, ORM sallangok nélkül
  • @DataMongoTest: Megpróbálja biztosítani a memóriában a mongo tesztelés beállításait
  • @WebMvcTest: Az ál-MVC tesztelő szelet az alkalmazás többi része nélkül
  • … (Ellenőrizhetjük a forrást, hogy megtaláljuk mindet)

Ez az okos használat, ha okosan használjuk, segíthet számunkra szűk tesztek készítésében, ekkora büntetés nélkül, különösen a kis / közepes méretű alkalmazások esetében a teljesítmény szempontjából.

Ha azonban alkalmazásunk folyamatosan növekszik, akkor az is halmozódik, mivel szeletenként egy (kis) alkalmazási kontextust hoz létre.

4.6. Osztály-öröklés használata

Egyetlen felhasználásával AbstractSpringIntegrationTest osztály az összes integrációs tesztünk szülőjeként, egyszerű, hatékony és gyakorlati módszer a gyors felépítés fenntartására.

Ha biztos beállítást biztosítunk, csapatunk egyszerűen kiterjeszti azt, tudván, hogy minden „csak működik”. Így kevésbé aggódhatunk az állapot kezelése vagy a keretrendszer konfigurálása miatt, és összpontosíthatunk a problémára.

Az összes tesztkövetelményt ott állíthatnánk fel:

  • A tavaszi futó - vagy lehetőleg szabály, ha később más futókra lenne szükségünk
  • profilok - ideális esetben az összesítésünk teszt profil
  • kezdeti konfiguráció - az alkalmazás állapotának beállítása

Vessünk egy pillantást egy egyszerű alaposztályra, amely gondoskodik az előző pontokról:

@SpringBootTest @ActiveProfiles ("test") absztrakt osztály AbstractSpringIntegrationTest {@Rule @JvmField val springMethodRule = SpringMethodRule () társobjektum {@ClassRule @JvmField val SPRING_CLASS_RULE = SpringClassRule ()}}

4.7. Államigazgatás

Fontos megjegyezni, hogy az Unit Test honnan származik. Egyszerűen fogalmazva, ez azt jelenti, hogy egyetlen tesztet (vagy részhalmazot) bármikor lefuttathatunk, és következetes eredményeket érhetünk el.

Ezért az állapotnak minden teszt megkezdése előtt tisztának és ismertnek kell lennie.

Más szavakkal, a teszt eredményének konzisztensnek kell lennie, függetlenül attól, hogy azt külön-külön vagy más tesztekkel együtt hajtják-e végre.

Ez az ötlet ugyanúgy vonatkozik az integrációs tesztekre. Az új teszt megkezdése előtt meg kell győződnünk arról, hogy alkalmazásunk ismert (és megismételhető) állapotban van-e. Minél több komponenst használunk fel a dolgok felgyorsítására (alkalmazáskörnyezet, DB-k, sorok, fájlok ...), annál nagyobb az esély az állami szennyezésre.

Feltéve, hogy az osztály öröklésével mindent összeszedtünk, központi helyünk van az állam kezelésére.

Fejlesszük absztrakt osztályunkat, hogy a tesztek futtatása előtt megbizonyosodjunk arról, hogy alkalmazásunk ismert állapotban van-e.

Példánkban feltételezzük, hogy több tároló létezik (különféle adatforrásokból), és a Wiremock szerver:

@SpringBootTest @ActiveProfiles ("test") @AutoConfigureWireMock (port = 8666) @AutoConfigureMockMvc absztrakt osztály AbstractSpringIntegrationTest {// ... a tavaszi szabályok itt vannak konfigurálva, az egyértelműség kedvéért átugrva @Autowired protected lateinit var wireMockServer: WT JdbcTemplate @Autowired lateinit var repos: Set @Autowired lateinit var cacheManager: CacheManager @Before fun resetState () {cleanAllDatabases () cleanAllCaches () resetWiremockStatus ()} fun cleanAllDatabases () {JdbcTestUtils.deleteFromTables (jdbcTated "table1 (table1" tábla1 table1 ALTER COLUMN id RESTART WITH 1 ") repos.forEach {it.deleteAll ()}} fun cleanAllCache () {cacheManager.cacheNames .map {cacheManager.getCache (it)} .filterNotNull () .forEach {it.clear () }} fun resetWiremockStatus () {wireMockServer.resetAll () // alapértelmezett kérelmek beállítása, ha vannak}}

4.8. Újrafeldolgozás az egység tesztjein

Valószínűleg ez az egyik legfontosabb pont. Újra és újra rátalálunk néhány integrációs tesztre, amelyek valójában az alkalmazásunk valamilyen magas szintű politikáját gyakorolják.

Valahányszor találunk néhány integrációs tesztet, amelyek az alap üzleti logika egy csomó esetét tesztelik, itt az ideje átgondolni a megközelítésünket és egységtesztekre bontani őket.

A sikeres megvalósítás lehetséges mintája itt lehet:

  • Azonosítsa az integrációs teszteket, amelyek az alapvető üzleti logika több forgatókönyvét tesztelik
  • Másolja le a csomagot, és alakítsa át a másolatot az egység Tesztjeire - ebben a szakaszban előfordulhat, hogy le kell bontanunk a gyártási kódot is, hogy tesztelhető legyen
  • Minden tesztet zöld színnel töltsön el
  • Hagyjon elég figyelemre méltó boldog út mintát az integrációs csomagban - lehet, hogy át kell alakítanunk, vagy csatlakoznunk kell, és át kell alakítanunk néhányat
  • Távolítsa el a fennmaradó integrációs teszteket

Michael Feathers számos technikát felölel ennek elérésére, és még többet is a Hatékony munkával a Legacy Code-val.

5. Összefoglalás

Ebben a cikkünkben bemutattuk az integrációs teszteket, különös tekintettel a tavaszra.

Először beszélgettünk az integrációs tesztek fontosságáról és arról, hogy ezek miért különösen relevánsak a tavaszi alkalmazásokban.

Ezt követően összefoglaltunk néhány eszközt, amelyek hasznosak lehetnek a Web Apps bizonyos típusú integrációs tesztjeihez.

Végül áttekintettük azokat a lehetséges problémákat, amelyek lelassítják a teszt végrehajtási idejét, valamint trükköket annak javítására.