neděle 23. ledna 2011

Co mě testy naučily o mém kódu

Návrat ke kořenům anebo někam kde jsem možná nebyl. Možná to bude znít jako klišé, a do jisté míry to i klišé i je, ale ukaž mi tvoje testy řeknu jaký jsi programátor. Jestli jsem někdy mluvil o tom, že většina testů je ze své podstaty povahy integračními a to pro většinu enterprise aplikací, které jsem psal, pak jsem se šeredně spletl. Dneska bych své tvrzení poupravil na, ano i integrační testy jsou potřeba, ale jejich počet by měl být alespoň u javového kódu výrazně v neprospěch tech jednotkových (unitových). Celý problém integračních testů, které vytváříme na úkor testů jednotkových je, že se nám zřejmě píší jednodušejí neboť náš kód je procedurální.

Když jsem pracoval na HP SOA Sysyinet, měli jsme celé baterie integračních testů. Tam bych odhadoval poměr 95% ve prospěch integračních testů. Integrační test je pro mě pochopitelně i ten typ testu, který potřebuje ke své činnosti nastartování nějakého kontejneru např. aplikační kontext Springu. Díky bohu za ty testy, ale nejenom tyhle, ale i jiné integrační testy mají jinou vypovídající schopnost o kódu, který píšeme. Musel jsem napsat hodně kódu, abych v hlavě udělal tenhle mentální kotrmelec.

Proč preferovat jednotkové testy

Jednotkové testy nás nutí programovat objektově. Naopak integrační testy nás nenutí vůbec k ničemu. Vezmeme prostě tu nejvyšší vrstvu špaget a tu otestuju. To má samozřejmě za následek, že ten kód ve spod je hůře otestovaný, protože píšeme proti vrstvě horní. Ve chvíli kdy píšeme jednotkový test, tak nás to nutí mnohem více k tomu, abychom ty špagety rozpletly do objektů s vlastní odpovědností a ty potom jednoduše mockovali.

Vedlejším produktem jednotkových testů je i to, že mnohem více přemýšlíte a jste nuceni přemýšlet o rozhraních mezi vašimi objekty. To jsou věci, které se vám při špagetovém programovaní poměrně zatemňují.

Řekněme, že máme třidu, která bude stahovat RSS soubor z dané URL a bude vracet titulky příspěvků. Zjednodušený kód by možná vypadal nějak takhle.

 
public class RssDownloader {
  public String[] getTitles(Url rssUrl) {
      HttpClient httpClient = new HttpClient();
      Xml xml = new Xml(httpClient.get(rssUrl)); 
      String titles[] = new String[];
      Node items[] = xml.getElements("Items");
      for(Node item : items) {
          titles.add(item.getTitle());
      }
      httpClient.close();
      return titles;  
  }    
}

Problém tohle třídy je v tom, že míchá dohromady dvě věci. Stažení RSS souboru a jeho zpracování. Správně bychom měli zpracování vlastního kódu držet pryč od toho odkud to XML pochází. Co když to RSS budeme najednou číst třeba z filesystému a nebo z databáze. Špatný objektový návrh, ale to si možná neuvědomíme dokud nebudeme ten kód chtít otestovat. Protože budete muset napsat integrační test a někde si nastartovat HTTP server. Ve skutečnosti, ale nechcete testovat HTTP klienta, ale otestovat, že z daného XML kód načte to co si představujeme.

Správné řešení je proto zobrazeno na dalším obrázku.

Pak je možné ono zpracování otestovat bez nutnosti mít integrační test. Můžete si dokonce, a měli byste si mockovat tu XML abstrakci. Samozřejmě ještě by šel HTTP klient přesunout jako instanční proměná třídy, zamockovat jej a pak testovat, že jej RssDownloader dobře používá (viz volání close metody).

Na tomhle jednoduchém příkladu jsem ukázal, že objektové programování, v tomhle případě demonstrované na správném oddělení odpovědností objektů, vede k tomu, že kód můžeme pohodlně testovat jednotkovými testy.