Keresés tartalomra
Kategóriák
Címkék
- Java
- Spring
- Python
- IoC
- Android
- DI
- Dagger
- Thymeleaf
- Markdown
- JDK11
- AOP
- Aspect
- Captcha
- I18n
- JavaSpark
- Microframework
- Testing
- JUnit
- Security
- JWT
- REST
- Database
- JPA
- Gépház
- WebFlux
- ReactiveProgramming
- Microservices
- Continuous Integration
- CircleCI
- Deployment Pipeline
- Docker
- Mocking
- LogProcessing
- PlantUML
- UML
- Modellezés
- OAuth2
- Node.js
- DevOps
- Websocket
Haladó unit tesztelés és mockolás
Először is tisztázzuk, milyen eszközökkel fogunk dolgozni. Java nyelven kifejezetten sok testrunner és mocking framework elérhető, talán a legáltalánosabban elterjedt azonban a JUnit, illetve annak a jelenlegi legfrissebb 5-ös verziója, amivel leginkább "Jupiter" néven lehet találkozni, ugyanis ez az alatta futó engine platform neve. A JUnit önmagában csak a tesztek futtatására és az eredmények ellenőrzésére alkalmas, bár utóbbira is vannak kényelmesebb és alapvetően fejlettebb libraryk, mint a Hamcrest vagy az AssertJ. A JUnit engine-je szempontjából teljesen mindegy melyiket használjuk, minden ilyen úgynevezett "assertion" framework vagy "csendben" hagyja lefutni a tesztmetódust, ezzel az engine számára azt jelezve, hogy a tesztelt kód helyes (vagy legalábbis a teszt nem hozott ki semmi problémát), vagy egy kivétel eldobásával megállítja a teszt futását, ezzel eltörve azt. Személy szerint én a Hamcrestet szoktam használni, mostanság többször is volt azonban szerencsém az AssertJ-hez, amiben van egy kifejezetten hasznos assertion mód, ami deeply nested objektumokban tudja úgy ellenőrizni a benne levő minden kollekció tartalmát, hogy a sorrendjüket közben ignorálja - erre sajnos a Hamcrest nem képes.
Szóval mindenekelőtt nézzük az alapokat, kezdve azzal, hogy hogyan néz ki egy unit teszt, milyen lépésekből áll, mi a célja. Egy unit teszt, mint a neve is mutatja, valamilyen könnyen megfogható, kicsi "egység" szemantikai/logikai helyességének tesztelését végzi. A tipikus cél az, hogy legyen minél több, kicsi, gyorsan végrehajtható tesztünk, amivel egy-egy adott code path működését ellenőrizzük, minél változatosabb bemeneti adatokkal, olyanokkal is, amik szándékosan hibára futtatnák a kódot. Bármennyire is meglepően hangzik ez talán, az is fontos a unit tesztek esetén, hogy lássuk, hogyan reagál a kódunk a hibás vagy akár hiányzó bemeneti adatokra, és hogyan tud abból helyreállni. Emellett az is nagyon fontos, hogy a tesztelt egység képes legyen önállóan futni, ne támaszkodjon más, "külső" egységekre, mivel unit tesztekkel nem az egységek egymásra gyakorolt hatását teszteljük (arra integrációs teszteket használunk, ez talán egy másik cikk témája lehet), illetve két tesztfuttatásnak ugyanazon a tesztelt kódon, ugyanazokat az eredményeket kell okoznia. A fenti szempontokat egyébként a FIRST betűszóval szokták jellemezni, mely a Fast (=gyors), Independent/Isolated (=független/elkülönített), Repeatable (=megismételhető), Small (=kicsi) és Timely (=időszerű ~ az élő kód fejlesztésével egyidőben elkészített) vagy az utóbbi helyett néha Thorough (=alapos) jellemzőket takarja.
A mockolás specifikusan a FIRST-I jellemzőt, tehát a teszt függetlenségét hivatott biztosítani. Képzeljünk el egy olyan komponenst, ami meghív egy külső servicet, vagy egy adatbázissal kommunikál. A tesztelt komponens innentől kezdve erős függésekkel rendelkezik, a service és az adatbázis is, nem csak hogy elérhetőek kell legyenek, de ugyanazokat az adatokat kell tudniuk biztosítani, amire a tesztünk számít. Ezt nyilván nem lehetetlen megoldani, de nem is indokolt abban az esetben, ha nem a külső service vagy az adatbázis tartalmának ellenőrzése a célunk, márpedig azt már lefektettük, hogy a unit tesztek kizárólag a kód helyességét hivatottak tesztelni. Ennek megfelelőn a függéseket mockokkal fogjuk leválasztani, a viselkedésüket pedig szimulálni fogjuk a mockolt függés bemeneti adatainak ellenőrzésével (hiszen azt a kódunk generálja a legtöbb esetben, tehát az a logika helyességén múlik), illetve a kimenetének előkészítésével (tehát a mock úgy "válaszol", mintha az eredeti függés tenné).
A tesztek szerkezete, bár egyáltalán nem kötelező, de az olvashatóság, érthetőség és a könnyű karbantarthatóság érdekében egy meghatározott struktúrát követ, mely az Arrange-Act-Assert (AAA) vagy a Given-When-Then (lényegében ugyanaz, mint az AAA, csak a Behavior Driven Development (BDD) mintára építő teszt frameworkökben) szegmensekre épül. Nem kell semmilyen komplex dologra gondolni itt, ezek egyszerű full-line-comment-ekként megjelenő címkék a tesztmetódus törzsében. Tipikusan szoktak alkalmazni emellett a tesztmetódusok elnevezésére is konvenciókat, amit bizonyos frameworkök meg is követelnek (a Python unittest nevű frameworkjében például test_
prefixszel kell kezdődnie a tesztmetódus nevének), de például a JUnit-nak erre nincs szüksége. Amivel eddig a legtöbbet találkoztam, az a should<tesztelt-metódus-neve><elvárt-eredmény-rövid-leírása><opcionális-feltételek>
struktúra, például shouldMapResponseCreatePopulatedResponseOnSuccess
. Ezzel maga a teszt neve ad információt arról, hogy mi is lenne annak a tesztesetnek a célja. Személy szerint a JS/TS nyelvű Jest framework megoldása jobban tetszik, abban nem kell metódusokat létrehozni (funkcionális paradigmájú megközelítése miatt), hanem teljes szöveges leírásokat lehet adni. Azt hiszem, ennyi elmélet elég is lesz, nézzünk néhány példát, kezdve egyszerűbbekkel, egészen a haladóbb tesztesetekig.
Egyszerű Mockolás
Először is elő kell készítenünk a teszt-osztályt, amely nagyjából minden esetben ugyanúgy történik: JUnit 5 alatt az @ExtendWith(MockitoExtension.class)
annotációval ellátva a teszt-osztályt, a frameworköt arra utasítjuk, hogy dolgozza fel a Mockito annotációkat. A @Mock
annotációval a tesztelt osztály függéseit jelöljük meg, míg az @InjectMocks
segítségével "beszúrjuk" a tesztelt osztályba azokat. Kezdésnek nézzünk egy olyan tesztesetet, ahol még nem lesz szükség mockokra, mivel az alábbi teszteset egy statikus választ visszaadó controller működését ellenőrzi.
@Test
public void shouldRenderLoginFormReturnPopulatedModelAndView() {
// when
ModelAndView result = authenticationController.renderLoginForm();
// then
assertThat(result.getViewName(), equalTo("views/login"));
}
A // when
címkével jelezzük, hogy itt történik a tesztelt osztály meghívása, a választ a result
változóban tároljuk. Végül a // then
címke alatt ellenőrizzük a választ, ez esetben a Hamcrest assertion library segítségével.
A következő példában a tesztelt kód egy külső függésre támaszkodik, mégpedig egy API hívásra (amit a documentBridgeService
-en keresztül tenne meg).
@Override
public Optional<WrapperBodyDataModel<DocumentDataModel>> getContent(String contentRequestParameter) {
WrapperBodyDataModel<DocumentDataModel> response = null;
try {
response = documentBridgeService.getDocumentByLink(contentRequestParameter);
} catch (DefaultNonSuccessfulResponseException | CommunicationFailureException e) {
LOGGER.error("Failed to retrieve static page content for link [{}]", contentRequestParameter, e);
}
return Optional.ofNullable(response);
}
A metódus tesztje az alábbiak szerint alakul majd:
@ExtendWith(MockitoExtension.class)
public class StaticPageContentRequestAdapterTest {
// ...
@Mock
private DocumentBridgeService documentBridgeService;
@InjectMocks
private StaticPageContentRequestAdapter adapter;
@Test
public void shouldGetContentReturnWithSuccess() throws CommunicationFailureException {
// given
given(documentBridgeService.getDocumentByLink(DOCUMENT_LINK)).willReturn(WRAPPED_DOCUMENT_DATA_MODEL);
// when
Optional<WrapperBodyDataModel<DocumentDataModel>> result = adapter.getContent(DOCUMENT_LINK);
// then
assertThat(result.isPresent(), is(true));
assertThat(result.get(), equalTo(WRAPPED_DOCUMENT_DATA_MODEL));
}
// ...
}
A // given
címke alatt állítjuk be a mock viselkedését, mégpedig a BBDMockito
osztály statikus given()
metódusának meghívásával. A viselkedés beállításához, a már példányosított mock (néhány sorral feljebb a @Mock
annotáció miatt) getDocumentByLink()
metódusát meghívjuk azzal az értékkel, amivel a kódunk is fogja, a willReturn()
metódussal pedig beállítjuk, hogy ha tényleg megtörténik a hívás, milyen választ "hazudjon be" a testrunner. Végül már csak assertáljuk a választ, amire a kódunktól számítunk.
Előfordulhat olyan eset is, amikor a mockokat nem tudjuk @InjectMocks
annotációval beszúrni. Ilyen például, amikor a tesztelt osztály konstruktora azonnal ráhív a mock egyik metódusára, egy statikus értéket akarunk átadni a tesztelt osztály konstruktorának a mockok mellett, vagy ha több azonos típusú mockot akarunk listaként átadni a konstruktornak. Ezekben az esetekben csak hozzuk létre a tesztelt osztály fieldjét @InjectMocks
nélkül, majd használjunk egy @BeforeEach
annotációval ellátott úgynevezett "setup" metódust.
@Mock
private GrantFlowProcessor grantFlowProcessor1;
@Mock
private GrantFlowProcessor grantFlowProcessor2;
@Mock
private TokenHandler tokenHandler;
@Mock
private AccessTokenDAO accessTokenDAO;
@Mock
private OAuthRequestContextFactory oAuthRequestContextFactory;
private OAuthAuthorizationServiceImpl oAuthAuthorizationService;
@BeforeEach
public void setup() {
given(grantFlowProcessor1.forGrantType()).willReturn(GrantType.CLIENT_CREDENTIALS);
given(grantFlowProcessor2.forGrantType()).willReturn(GrantType.AUTHORIZATION_CODE);
oAuthAuthorizationService = new OAuthAuthorizationServiceImpl(Arrays.asList(grantFlowProcessor1, grantFlowProcessor2),
tokenHandler, accessTokenDAO, oAuthRequestContextFactory);
}
A teszt tehát be fog szúrni két GrantFlowProcessor
implementációt listaként, illetve mivel azok forGrantType()
metódusát azonnal meghívja a konstruktor, még azelőtt beállítjuk a viselkedésüket, hogy a tesztelt osztály konstruktorának átadnánk őket. A setup metódus egyébként a mockok kézi inicializálására is használható, így ha valamilyen okból nem akarjuk vagy nem tudjuk az @ExtendWith()
annotációval elindítani a mockokat, a Mockito
osztály statikus mock()
metódusával ugyanúgy megtehetjük (annak csak a mockolni kívánt osztály vagy interface class-át kell átadnunk).
Teszteljük, hogy elszáll a kód
Az alábbi teszteset egy olyat esetet szimulál, amikor a felhasználó frissíteni próbálja a felhasználói profilját, de az access tokenje már lejárt. Ebben az esetben nem szeretnénk, hogy bármi mást csináljon a rendszer, így megállítjuk a feldolgozást egy kivétellel, amely kivétel viszont nem azonos az eredetileg eldobottal (nyilván egy try-catch "lefordította" azt):
@Test
public void shouldGetContentReturnThrowUserSessionInvalidationRequiredException() throws CommunicationFailureException {
// given
doThrow(UnauthorizedAccessException.class).when(userBridgeService).updateProfile(USER_ID, UPDATE_PROFILE_REQUEST_MODEL);
// when
Assertions.assertThrows(UserSessionInvalidationRequiredException.class, () -> adapter.getContent(WRAPPED_UPDATE_PROFILE_REQUEST_MODEL));
// then
// exception expected
}
Ebben az esetben given()
helyett a doThrow()
metódussal kezdjük a mock viselkedésének definiálását, amiben azt mondjuk, dobjon egy UnauthorizedAccessException
kivételt, ha a userBridgeService
updateProfile
metódusát meghívjuk. Az ebben az esetben elvárt viselkedést az assertThrows()
verifikálja, ami egy UserSessionInvalidationRequiredException
kiváltódását fogja elvárni az adapter.getContent()
híváskor.
Nyilván a kódtól függ, hogy egy adott kivétel vagy szimplán hibás érték fogadása esetén mi fog történni, így például a kiváltott kivétel után vissza is térhet a metódus valamilyen hasznos válasszal, mint az az alábbi példában is történik.
@Test
public void shouldProcessAccountRequestReturnFailureStatusOnUnexpectedException() {
// given
given(conversionService.convert(SIGN_UP_REQUEST_MODEL, User.class)).willReturn(CONVERTED_USER);
doThrow(IllegalArgumentException.class).when(userDAO).save(CONVERTED_USER);
// when
SignUpResult result = signUpAccountRequestHandler.processAccountRequest(SIGN_UP_REQUEST_MODEL);
// then
assertThat(result.redirectURI(), equalTo(PATH_SIGNUP));
assertThat(result.signUpStatus(), equalTo(SignUpStatus.FAILURE));
verifyNoInteractions(notificationAdapter);
}
Ebben az esetben még mindig szeretnénk, hogy a felhasználó értesüljön a hibáról, így a sikertelen mentés után azt várjuk, hogy a signUpAccountRequestHandler
térjen vissza egy SignUpStatus.FAILURE
státusszal.
Mock verifikálás
Szemfülesebbek az előző példában észrevehettek egy különös metódushívást a teszteset végén: verifyNoInteractions()
. Ezzel a metódussal azt tudjuk ellenőrizni, hogy egy adott mock a tesztfutás közben soha nem hívódott meg. A fenti példában a notificationAdapter
meghívása nem volna kifejezetten ildomos a sikertelen felhasználói regisztráció esetén (gyakorlatilag nem akarjuk értesíteni róla a felhasználót, hogy sikerült a regisztráció, mikor nyilvánvalóan nem), így verifikáljuk, hogy az soha nem hívódott meg. Ezen metódus "párja" a verifyNoMoreInteractions()
, ami annyiban különbözik, hogy bizonyos hívásokat elvárunk a mockon, de annál többet nem. Ezeket vagy előre definiáljuk a given()
vagy doThrow()
metódusokkal, vagy a teszthívás végén a verify()
metódus használatával ellenőrizzük a hívásokat (mindjárt rátérek), majd a végén arra utasítjuk a Mockitot, hogy ennél több hívást ne fogadjon el a mockon, ellenkező esetben tegye sikertelenné a tesztfutást. Az alábbi példában a userDAO
mocktól elvárjuk, hogy kérje fel az adatbázisból a felhasználó adatait jelszóvisszaállítás előtt, viszont mivel az külső fiók, a jelszó visszaállítása nem lehetséges, így a további hívások a userDAO
-n nem történhetnek meg:
@Test
public void shouldProcessAccountRequestFailSilentlyIfUserAccountIsExternal() {
// given
given(userDAO.findByEmail(EMAIL)).willReturn(Optional.of(EXTERNAL_USER));
// when
passwordResetRequestAccountRequestHandler.processAccountRequest(PASSWORD_RESET_REQUEST_MODEL);
// then
verifyNoMoreInteractions(userDAO);
verifyNoInteractions(passwordResetConfig, tokenHandler, notificationAdapter);
}
Ez esetben a kód csendben nyugtázni (logolni) fogja, hogy a felhasználó valami nem odaillőt csinál, de nem engedi neki. :)
Eddig olyan példákat láttunk, ahol a mock futásának látványos eredménye volt, azonban a mockok háttérfolyamatok elindítását is szimulálhatják, ami sokszor legalább annyira fontos, mint bármi más a tesztelt metódusban. Ilyenkor jön képbe a verify()
metódus, ami a mockhívások mennyiségét és "milyenségét" ellenőrzi. Például megadhatjuk, hogy egy adott mockot hányszor várunk meghívni a metóduson belül, illetve azokat a hívásokat milyen paraméterrel várjuk. Az alábbi tesztben a tesztelt metódusnak nincs visszatérése, így abból nem tudjuk megállapítani, mit csinált a futása közben, azonban meghív egy repositoryt, amit viszont tudunk verifikálni:
@Test
public void shouldUpdateStatusByJTIPassCallToRepository() {
// when
accessTokenDAO.updateStatusByJTI(JTI, TokenStatus.REVOKED);
// then
verify(accessTokenRepository).updateStatusByJTI(JTI, TokenStatus.REVOKED);
}
Eddig viszonylag egyszerű dolgunk volt, de mi van akkor, ha bizonytalanok vagyunk abban, hogy mi lesz a hívás helyén, vagy adott esetben például egyáltalán nem is érdekes az. A Mockito API-ja siet a segítségünkre, ugyanis a verificationben a konkrét paraméterek helyett úgynevezett argument matchereket is használhatunk. Az alábbi esetben például csak az a fontos, hogy a response
objektum HTTP 403-at kapjon, a hozzá társított üzenet nem számít:
verify(response).sendError(eq(HttpServletResponse.SC_FORBIDDEN), anyString());
Az argument matchereket egyébként ugyanilyen módon a given()
definíciókban is használhatjuk, ha további kontrollra van szükségünk a paraméterek felett. Van egy (kettő) azonban, amit személy szerint, ha meglátok egy pull requestben, azonnal kérem rá a változtatást, az pedig az any()
és az anyClass()
: szó szerint azt jelentik, hogy "nem érdekel mi van ott, valamivel legyen meghívva". Régebbi Mockito verziókban a null
is matchelt ezekre, amit szerencsére azóta már javítottak. Mindenesetre ezeket a mai napig nem javaslom használni, mivel gyakorlatilag megöli a teszt célját.
ArgumentCaptor-ok
Mi van akkor, ha különösen fontos, hogy milyen objektummal történik meg a mock hívása és mindenképp ellenőrizni szeretnénk azt? Alapesetben a given()
definícióban megadhatunk akár komplex objektumot is, sok esetben ez egy elégséges megoldás is lesz. Előfordulhat azonban, hogy annyira speciális objektumot rakunk össze (kezdve olyan triviális "hibákkal" is, mint egy külső féltől származó API-ban levő data class equals metódus nélkül), amit nem fog tudni illeszteni a Mockito, így utólagosan kellene belenéznünk az objektumba. Akkor nincs is gond, ha a tesztelt metódus visszaadja ezt az objektumot, ellenkező esetben viszont... Ilyenkor jönnek szóba az ArgumentCaptor-ok, amik képesek "elkapni" a hívás pillanatában a mocknak átadott objektumot, majd a captorból kikérve azt, assertálni tudjuk a tartalmát. A captorokat szintén be tudjuk állítani a given()
definícióban, de tipikusan inkább a verify()
hívás részeként szokott megjelenni, mint az alábbi példában is:
@Captor
private ArgumentCaptor<StoreAccessTokenInfoRequest> storeAccessTokenInfoRequestArgumentCaptor;
// ...
@Test
public void shouldGenerateTokenSuccessfullyCreateJWTTokenWithCustomExpiration() throws IOException {
// given
int customExpirationInSeconds = 600;
// when
OAuthTokenResponse result = jwtTokenHandler.generateToken(O_AUTH_TOKEN_REQUEST, CLAIMS, customExpirationInSeconds);
// then
verify(tokenTracker).storeTokenInfo(storeAccessTokenInfoRequestArgumentCaptor.capture());
StoreAccessTokenInfoRequest request = storeAccessTokenInfoRequestArgumentCaptor.getValue();
// ...
assertThat(request.getExpiresAt().getTime() - request.getIssuedAt().getTime() == customExpirationInSeconds * 1000L, is(true));
// ...
}
Először is létrehozzuk a captort a @Captor
annotációval, aminek a típusa egy generikus ArgumentCaptor
, a konkrét típusa pedig az objektum típusa, amit el akarunk vele kapni. A captor elkapja a híváskor átadott objektumot a capture()
metódussal, majd a getValue()
-val elkérjük azt (ha több hívás van a mockon, használhatjuk a getValues()
-t, ami a hívások sorrendjében adja vissza az elkapott objektumokat). A fenti példában arra voltam kíváncsi, hogy a lejárati időt helyesen állítja be a metódus, de a dátum létrehozását nem mockoltam, így az persze minden tesztfuttatásnál más értéket eredményez, nem tudok egy előre elkészített objektummal való egyenlőséget ellenőrizni. A captorok különösen hasznosak még olyan mockok esetén, amik háttérfolyamatok indítását szimulálják, mivel azoknak az eredménye tipikusan nem látszik a tesztelt metóduson kívül. Fontos megjegyezni, hogy a verify()
és a given()
metódus is érzékeny a paramétereire, kevert paramétertípusokkal nem működik. Mit értek ez alatt: a capture()
metódus egy argument matcher, így amennyiben ilyet használunk, a többi paraméternek is wrapelve kell lennie argument matcherekbe:
given(bridgeClient.call(restRequestCaptor.capture(), eq(GITHUB_API_EMAIL_ENDPOINT_GENERIC_TYPE))).willReturn(GITHUB_EMAIL_ITEMS);
Az eq()
hívás nélkül a fenti definíció nem működne, a Mockito hibát dobna.
Statikus mockolás
Sokan nem tudják, de a Mockito néhány éve elkezdte támogatni a statikus metódusok mockolását is, amire korábban csak a PowerMock volt képes. Bár személyes véleményem szerint nem a legelegánsabb módon oldották meg, de egy valóban nagyon hasznos bővítése a frameworknek. Az alábbi példában a UUID.randomUUID()
statikus metódust mockoljuk, mely így mindig pontosan ugyanazt az eredményt adja majd. A szerkezet mindig ugyanaz, try-with-resources blockban a mockStatic()
hívással aktiváljuk a mockot, majd a törzsében meghívjuk a tesztelt metódust, ami aztán meg fogja hívni a mockolt statikus metódust. (A fentiekkel ellentétben ez most nem egy valós példa, jelenleg saját élő kódbázisomban nem találtam UUID-s példát, de ez a teszt így működne mindenesetre.)
UUID expectedUUID = UUID.fromString("699aca11-c67b-4275-8df0-e02e0e68f49a");
try (MockedStatic<UUID> mockedUUID = mockStatic(UUID.class)) {
// given
mockedUUID.when(UUID::randomUUID).thenReturn(expectedUUID);
// when
UUID result = UUID.randomUUID();
// then
assertThat(result, equalTo(expectedUUID));
}
Lenient mockok
A lenient mockok egy érdekes témakör: akkor használunk ilyet, ha előkészítünk egy mock-hívást (given()
-nel), de aztán megengedjük a tesztnek hogy mégse legyen szükséges meghívni azt a mockot. Ellenkező esetben a teszt egy UnnecessaryStubbingException
-nel fog zárulni még akkor is, ha a teszt egyébként hibátlanul lefutott. Személy szerint igyekszem kerülni a lenient mockok használatát, legfeljebb akkor hagyok néhányat a kódban, ha nincs elég időm foglalkozni a teszt részletesebb előkészítésével, Összességében nem ajánlom a használatát, már csak azért sem, mert sokszor valamilyen teszthibát jelez, rosszabb esetben kódhibát. Bekapcsolása egyébként nagyon egyszerű: @Mock(lenient = true)
, és már nyugodtan (félre is) használhatjuk a mockot.
Parametrizált teszt
A JUnit 4-ben még egy rettenetes megoldás volt a parametrizált tesztek elkészítésére, ami miatt akkoriban tipikusan TestNG-t használtunk az ilyen esetekben. Szerencsére ezt a JUnit fejlesztői is érezték, és az 5-ös verzióba egy nagyon kényelmes megoldás került, a @ParameterizedTest
annotáció formájában. Az ilyen tesztek két dologban különböznek a "sima tesztektől": egyrészt az ilyen tesztmetódusoknak adhatunk - és kell is - paramétereket adni, ezeken keresztül fogja a tesztparamétereket beszúrni a framework. Másrészt kell nekik egy adatforrás, ami többféle is lehet, személy szerint a @MethodSource
-ot használom a legtöbbet, de van például @ValueSource
(egyszerű literál értékeket tud beszúrni), @EnumSource
(enum konstansokat szúr be), @CsvSource
(CSV formátumú literálokat tördel fel és szúrja be azokat oszloponként), stb. A @MethodSource
a legkomplexebb, egy speciális statikus metódussal tudunk komplex értékcsoportokat lepasszolni a tesztesetnek.
@ParameterizedTest
@MethodSource("supportDataProvider")
public void shouldSupport(FrontEndRouteType type, boolean expectedResult) {
// given
FrontEndRouteVO frontEndRouteVO = FrontEndRouteVO.getBuilder()
.withType(type)
.build();
// when
boolean result = entryRouteMaskProcessor.supports(frontEndRouteVO);
// then
assertThat(result, is(expectedResult));
}
private static Stream<Arguments> supportDataProvider() {
return Stream.of(
Arguments.of(FrontEndRouteType.ENTRY_ROUTE_MASK, true),
Arguments.of(FrontEndRouteType.CATEGORY_ROUTE_MASK, false),
Arguments.of(FrontEndRouteType.TAG_ROUTE_MASK, false),
Arguments.of(FrontEndRouteType.HEADER_MENU, false),
Arguments.of(FrontEndRouteType.FOOTER_MENU, false),
Arguments.of(FrontEndRouteType.STANDALONE, false),
Arguments.of(null, false)
);
}
A teszteset tehát a @MethodSource
annotációban a számára az adatokat biztosító Stream<Arguments>
visszatérésű metódus nevét várja. A metódusban az Arguments.of()
hívásokkal létrehozzuk az argumentum-csoportokat, amiket aztán a teszt önálló metódus paraméterekként kap meg. Ez esetben valójában nem 1, hanem 7 tesztesetünk lesz, mindegyik a stream 1-1 eleme által biztosított értékekkel fog futni. Kiváló eszköz olyan tesztek készítésére, ahol kritikusan fontos, hogy többféle paraméterrel is teszteljük a kód futását.
Összefoglalás
Az igazat megvallva ezt a cikket ennél jóval rövidebbnek szántam, aztán a végére rájöttem, hogy még mindig csak a felszínt karcolgatom. :) Mindenesetre remélem hasznos tudást tudtam átadni, vagy legalábbis stabil alapokat a folytatáshoz. Amit mindenképpen tudnék javasolni itt a cikk végén, az az, hogy egyrészt merjünk sokat kísérletezni a unit tesztekkel, mivel a legtöbb problémára amibe belefuthatunk, általában van megoldás, és a végén sokkal többet fog érni maga a teszt, több problémát fog felfedni, illetve szánjuk rá az időt, hogy tényleg hasznos részévé váljanak a kódbázisnak. Akármennyire is tűnik időpazarlásnak, egy jó unit teszt készlet egy szoftver projekt "életét" is meg tudja menteni.
Komment írásához jelentkezz be
Bejelentkezés
Még senki nem szólt hozzá ehhez a bejegyzéshez.