fbpx

Testujte len jednu vec

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ú:

test testuje niečo komplikované na prípravu vstupných dát alebo spolupracujúcich komponentov
test sa snaží otestovať príliš veľa vecí naraz. Napríklad viac scenárov. Toto môže byť dôsledok aj predchádzajúceho bodu.

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:

testy sú kratšie
testy sú jednoduchšie na pochopenie, čo robia
prečítaním tela testu je jednoduchšie overiť, že názov metódy špecifikuje, čo test naozaj robí1
test v prvom príklade vie zlyhať z rôznych dôvodov. Druhý príklad umožňuje lepšiu identifikáciu problému, lebo je menej dôvodov ktoré môžu spôsobiť zlyhanie individuálneho testu.

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 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.


  1. 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.

1 komentár k “Testujte len jednu vec”

Vložiť komentár