Svet je zaplavený príliš veľkými testami. Som si istý, že ste ich už všetci videli a väčšina z vás aj napísala. Je určite áno. Časté dôvody existencie veľkých testov sú:
Aký je najväčší problém veľkého testu? Podľa mňa je to jeho nečitateľnosť. Z nej vyplýva to, že nevieme čo test robí. Aké komponenty treba pripraviť, s akými dátami to robí, čo robí a prípadne ako. A keď sme postavení pred úlohu niečo zmeniť v testovanom kóde tak sa nám tento test vypomstí — zlyhá a my ho nevieme (rýchlo) opraviť.
Alebo máme pridať novú funkcionalitu a stojíme pred dilemou, či celý test zduplikovať a pridať ďalšie asserty v kópii, alebo zmeniť existujúci test. Najlákavejšia možnosť je samozrejme sa na test vykašľať a nechať novú funkcionalitu neotestovanú…
Jeden assert v teste
Jednoduché pravidlo, ktoré pomôže znížiť pravdepodobnosť vytvorenia veľkého testu je Single assert per test. To znamená, že sa nesnažíme aby test robil príliš veľa. Naopak, snažíme sa aby toho robil čo najmenej. Príklad — namiesto testovania viacerých vstupov a výstupov zásobníka ako je ukázané na tomto zlom teste:
@Test
public void isEmptyShouldReactOnCallingPushAndPop() {
Stack tested = new Stack();
assertTrue("should be empty before pushing anything", tested.isEmpty());
tested.push("content");
assertFalse("should not be empty after push", tested.isEmpty());
tested.pop();
assertTrue("should be empty after popping whole content", tested.isEmpty());
}
… je lepšie rozdeliť ho na 3 nezávislé testy:
private Stack tested;
@Before
public void setUp() {
tested = new Stack();
}
@Test
public void newStackShouldBeEmpty() {
assertTrue(tested.isEmpty());
}
@Test
public void populatedStackShouldNotBeEmpty() {
tested.push("an item");
assertFalse(tested.isEmpty());
}
@Test
public void poppingLastElementMakesStackEmpty() {
tested.push("an item");
tested.pop();
assertFalse(tested.isEmpty());
}
Asi to nevyzerá ako zlepšenie, lebo máme celkovo viac riadkov, ale podľa mňa je lepší z nasledujúcich dôvodov:
Jeden testovaný koncept v teste
Jeden assert v teste je príliš veľké obmedzenie na väčšinu testov. Ale treba sa na to pozerať skôr ako na princíp. Ten vieme uplatniť aj keď potrebujeme otestovať, či všetky atribúty nejakej entity spĺňajú požadované kritéria.
Majme takúto metódu, ktorá naformátuje adresu do žiadaného stavu:
public Contact getBillingAddress();
Existuje viacej spôsobov ako vieme schovať viac assertov do jedného. Niekto preferuje equals
:
Contact returned = tested.getBillingAddress();
Contact expected = ContactBuilder.contact()
.salutation('Mr.')
.name('John Doe')
.street("5 High Street")
.city('London')
// ...
.build();
assertEquals(expected, returned);
Ja osobne mám radšej custom assert (netuším ako to preložiť). Dôvod je ten, že equals
má dosť inej roboty (identita v kolekciách, identita v Hibernate, …). Načo mu ešte pridávať zodpovednosť za kvalitu testov? Keď spravíme vlastnú metódu, tá môže robiť čokoľvek čo považujeme za dôležité a nikto, ani autori Javy, nám nemôže vyčítať, že sme zle implementovali hashCode
.
Custom assert môže, napríklad, vyzerať takto:
private void assertEqualContacts(Contact expected, Contact checked) {
assertEquals("Incorrect salutation", expected.getSalutation(), checked.getSalutation());
assertEquals("Incorrect name", expected.getName(), checked.getName());
assertEquals("Incorrect street", expected.getStreet(), checked.getStreet());
assertEquals("Incorrect city", expected.getCity(), checked.getCity());
// ...
}
a potom ju už len zavoláme v teste namiesto assertEquals
:
// ...
assertEqualContracts(expected, returned);
Sú aj iné možnosti. Jedna z nich čo sa mi pozdáva je použiť knižnicu AssertJ, v ktorej môžeme vygenerovať tester pre Contact
a použiť napríklad takto:
assertThat(returned)
.hasSalutation('Mr.')
.hasName("John Doe")
.hasStreet("5 High Street")
// ...
Toto riešenie sa dá kombinovať s custom assert a možnosti AssertJ ďaleko prekračujú tento príklad (testovanie obsahu kolekcií, výnimiek, …). Určite stojí za to, sa na túto knižnicu pozrieť.
Najčastejšia príčina veľkých testov
Skoro všetky veľké testy čo som v živote videl patrili ku veľkému komponentu. Takže ak sa vám predchádzajúce nápady na krátke testy zdajú nerealizovateľné, treba zmeniť niečo iné — skúste začať robiť menšie komponenty. Triedy alebo aj metódy budú robiť len jednu vec. A táto jedna vec zvyčajne vedie k výrazne jednoduchším testom.
Vypozoroval som také pravidlo, ak si chcem niekde v produkčnom kóde ušetriť námahu (napr. na asynchrónnej infraštruktúre) a spojím do jednej metódy viac rôznych úloh, tak testy budú hrozné.
Keď si to uvedomím a rozdelím kód na nezávislé volania, tak sa testy okamžite zjednodušia. Hoci ich bude viac. To ale nie je problém, lebo každý test je nezávislý a na jeho pochopenie treba prečítať len ten jeden test.
Dobrá správa na koniec: TDD vedie programátorov k tomu, aby vytvárali testy zamerané na jednu vec. Takže keď začnete programovať štýlom TDD, tak tento problém sám vymizne.
Záver
Nie je nič viac frustrujúce ako keď zlyhá obrovský test a ja neviem prísť na to, čo sa stalo. Prečo zlyháva? Musím prečítať celý test a snažiť sa pochopiť čo a ako robí, lenže to vôbec nie je jednoduché — je plný mockovaných metód, pripravuje a skladá komplikované dátové štruktúry, z ktorých sú niektoré použité ako parametre do mockov a iné si podelia medzi sebou volania rôznych metód testovanej triedy, no a nakoniec máme ešte asserty.
Ak ste predchádzajúcu vetu nedočítali, tak som dosiahol svoj cieľ. Chcel som napísať dlhú vetu. A neminúť pri tom energiu na jej rozdelenie na menšie časti, aby som uľahčil čitateľom ich prácu. Ak ste ju preskočili, tak riskujem, že stratím čitateľa — mám smolu. Ale ak nedokážete prečítať a pochopiť dlhý test, tak máte smolu vy. A ten test sa vypomstí nielen kolegom autora toho testu ale po čase aj autorovi.
Preto považujem princípy z tohto článku za základ. Je to návyk na správnu hygienu tvorby unit testov. Dajú sa tvoriť bez tohto návyku, ale smrdia.
- Dobré testy a ich mená môžu slúžiť ako špecifikácia komponentu. Vypísaním zoznamu mien všetkých metód vieme získať pekný prehľad o schopnostiach komponentu. ↩
„Vďaka mojim dlhoročným skúsenostiam s tvorbou unit testov a refaktoringom pomáham programátorom zmeniť ich postoj z musím na oveľa lepší — baví ma a dokážem.”
1 komentár k “Testujte len jednu vec”
Nie je možné pridávať komentáre.