pondělí 6. srpna 2007

JUnit 4.4 kladivo na testy

Kam až rozšiřovat možnosti testovacích frameworků? Tuhle otázka jsem si položil během zkoumání nedávno uvolněné verze 4.4 populárního frameworku pro psaní testů JUnit. Tato verze přináší dvě celkem zásadní novinky, první je assertThat a tou druhou, po mém soudu zajímavější, jsou předpoklady a teorie (assumptions and theories).

Obě novinky nejsou původně z dílny JUnit, ale jsou převzaté (absorbované se tomu teď říká ;-) z testovacího udělátka jMock resp. z Popper

assertThat

Nově připravená metoda assertThat dostává jako první argument aktuální hodnotu a jako druhý matcher (vyhodnocovač?), podle kterého se assert vyhodnotí. Pokud vám tahle definice zamotala hlavu, nevadí následující kód vše vrátí do správných kolejí.


assertThat(something, eq("Hello"));
assertThat(something, eq(true));
assertThat(something, isA(Color.class));
assertThat(something, contains("World"));
assertThat(myList, hasItem("3"));
    

Kromě toho, že je možné definovat si vlastní matchery, tak je lze také řetězit.

assertThat(something, not(contains("Cheese")));    
assertThat(responseString, either(containsString("color")).or(containsString("colour")));
    

Hlavní výhodou assertThat oproti původním assert metodám je lepší čitelnost, alespoň tedy v některý případech. Dalším plusem jsou implicitní zprávy při selhání testu.

assertThat(responseString, anyOf(containsString("color"), containsString("colour")));
// ==> failure message:
// java.lang.AssertionError: 
// Expected: (a string containing "color" or a string containing "colour")
//      got: "Please choose a font"    
    

Předpoklady a teorie

Předpoklady slouží k explicitnímu vyjádření podmínek, za jakých test musí projít. Takovou podmínkou může být například rozsah vstupních dat testu, dostupnost databázového připojení, nastavení určité systémové property. Tedy obecně závislostí mimo rozsah vlastního testu. Teorie se skládá s ze vstupních testovacích dat, předpokladu za kterého je schopen test s daty pracovat a vlastního testu.

Díky teorii může vývojář říci, za těchto a těchto vstupních podmínek se testovaný kód musí chovat takto. Právě díky podmínkám je možné nechat automatizované nástroje generovat vstupní data, která jdou za rozsah toho, jak by očekával vývojář případně toho, co by bylo pracné vyjádřit.

Malá citace z práce Davida Saffa a Marata Boshernitsana The Practice of Theories: Adding “For-all” Statements to “There-Exists” Tests.

Traditional unit tests in test-driven development compare a few concrete example executions against the developer’s definition of correct behavior. However, a developer knows more about how a program should behave than can be expressed through concrete examples. These general insights can be captured as theories, which precisely express software properties over potentially infinite sets of values. Combining tests with theories allows developers to say what they mean, and guarantee that their code is intuitively correct, with less effort. The consistent format of theories enables automatic tools to generate or discover values that violate these properties, discovering bugs that developers didn’t think to test for.

Jak taková teorie vypadá, ukazuje následující kód z dokumentace JUnit 4.4.


@RunWith(Theories.class)
public class UserTest {
  @DataPoint public static String GOOD_USERNAME = "optimus";
  @DataPoint public static String USERNAME_WITH_SLASH = "optimus/prime";

  @Theory public void filenameIncludesUsername(String username) {
    assumeThat(username, not(containsString("/")));
    assertThat(new User(username).configFileName(), containsString(username));
  }
}    
    

Anotace DataPoint definuje vstupní data teorie. Spouštěč testu krmí testovací metodu filenameIncludesUsername všemi kompatibilními (souhlasí datový typ proměnné a parametru testovací metody) veřejnými proměnnými, které jsou označeny anotací DataPoint. Předpoklad je vyjádřený pomocí assumeThat.

Celá teorie by se dala převyprávět asi takhle:Za předpokladu, že username neobsahuje /, musí platit, že jméno konfiguračního souboru uživatele obsahuje jméno daného uživatele.

Pokud je test spuštěn a nějaká vstupní data porušují definovaný předpoklad, jsou tiše ignorována. Takhle to sice stojí v dokumentaci, ale mě osobně to vždycky způsobilo, že test neprošel. Samozřejmě anotace DataPoint by nemusela být úplně vhodná pro generování vstupních dat a tak je možné naimplementovat potomka třídy ParameterSupplier.

Ač jsou třídy vztahující se k předpokladům a teoriím v package obsahujícím slovo experimental, přijde mi tento koncept jako v užitečný. Nebude sice automaticky aplikovatelný na všechny testy, ale pro některé typy testů bude představovat velký přínos. Typickým případem budou testy, kde potřebujeme větší rozsah a rozmanitost vstupních dat.