Spustit prezentaci

Doporučené postupy v programování

Testování

Unit testing · Testovatelný kód

Lubomír Bulej

KDSS MFF UK

Cíle testování

Hlavní cíl

Vedlejší cíle (často důsledek testování)

Testovat ručně nebo automaticky?

Všechny testy by měly být automatické

Jednoznačný výsledek je buď "test prošel", nebo "test neprošel".

Vyplatí se psát testy?

Záleží na ceně chyb a jejich nápravy

V případě obyčejné webové aplikace se formalizace testování pravděpodobně nevyplatí – data v aplikaci obyčejně nebývají důležitá, chyby nebývají fatální, uživatelé je poměrně rychle odhalí a jejich oprava je snadná. Samozřejmě, situaci může ovlivnit např. fakt, že aplikace je placená (uživatelé ze sebe nenechají dělat pokusné králíky a utečou ke konkurenci) a nebo se v ní manipuluje s důležitými údaji (často peníze).

U jakékoliv komponenty, která je používána na více místech už je cena chyb větší, protože je automaticky budou obsahovat všechny programy, které komponentu používají, a po opravě nalezených chyb se všechny tyto programy budou muset opravit také. Důkladné testování se zde už pravděpodobně vyplatí.

V případě software kardiostimulátoru, nebo obecně jakéhokoliv jiného software, kde jsou v sázce zdraví či životy lidí a nebo velké finanční částky, mnohdy investice do testování přesahují investice do vývoje samotného programu. Často se používají metody z kategorie formálních specifikací a model checkingu.

Rozdělení testů

Rozsah

Náplň

Unit testy testují malé jednotky programu, typicky jednotlivé třídy a metody v nich. Testy komponent testují větší funkční celky, integrační testy pak jejich vzájemnou interakci. Systémové testy testují celý vyvíjený produkt jako celek v jeho finální konfiguraci a cílovém prostředí.

Regresní testy často vznikají implicitně, každý test spouštěný automaticky se totiž stává testem regresním.

Rozdělení testů

Black box testing

White box testing

Problémy testování

Opačný cíl než psaní programu

Nedokazuje správnost/bezchybnost programu

Nezlepšuje kvalitu, jen indikátor

Není až tak efektivní, jak si lidé myslí

Opačný cíl než psaní programu je hlavně problém psychologický. Protože programátor chce, aby program fungoval, může mít (zejména je-li pod tlakem) tendenci testy vynechávat nebo odbývat, případně se v nich záměrně vyhnout některým situacím, o nichž ví, že v programu způsobí problémy.

Unit testing

Pokud si budete chtít unit testy opravdu vyzkoušet v praxi, a budete při tom využívat nástroj JUnit, projděte si nejdříve následující odkazy (nejlépe v uvedeném pořadí).

Unit testing

Testování funkčnosti malých jednotek kódu

Větší celky primárně složeny z menších

Proč je důležité testovat?

Řada pozitivních důsledků

Co testování brání?

K podpoře změn: Nejčastější důvod ke snaze neměnit, co funguje, je riziko zanesení chyb do programu. Pokud používáme unit testy, vytvářejí pod programem jakousi záchrannou síť a nově zanesené chyby většinou odhalí. "Většinou" samozřejmě není "vždy", ale pokud jsou testy dobře napsané, je pravděpodobnost zanesení obvykle dostatečně malá, aby změnám nebránila. Celkovým výsledkem používání unit testů jsou obvykle častější refaktorizace a tedy i čistější kód.

Jak vypadá unit test?

Testovaný kód: SimpleStack.java

public final class SimpleStack <T> {
  private static final String STACK_EMPTY_MESSAGE = "Stack is empty.";

  private List <T> items = new LinkedList <T> ();

  public boolean isEmpty () {
    return items.isEmpty ();
  }

  public T top() {
    if (items.isEmpty ()) {
      throw new IllegalStateException (STACK_EMPTY_MESSAGE);
    }

    return items.get (0);
  }

  public T pop() {
    if (items.isEmpty ()) {
      throw new IllegalStateException (STACK_EMPTY_MESSAGE);
    }

    return items.remove (0);
  }

  public void push (T item) {
    items.add (0, item);
  }
}       

Jak vypadá unit test?

Testovací kód: SimpleStackTest.java

public final class SimpleStackTest {

  private SimpleStack <String> emptyStack;
  private SimpleStack <String> fullStack;

  @Before
  public void setUp () {
    emptyStack = new SimpleStack <String> ();

    fullStack = new SimpleStack <String> ();
    fullStack.push ("A");
    fullStack.push ("B");
    fullStack.push ("C");
  }

  @Test
  public void emptyStackIsEmpty() {
    assertTrue(emptyStack.isEmpty());
  }

  @Test
  public void fullStackNotEmpty() {
    assertFalse(fullStack.isEmpty());
  }

  @Test(expected=IllegalStateException.class)
  public void topThrowsExceptionOnEmptyStack() {
    emptyStack.top();
  }

  @Test
  public void topReturnsTopItem() {
    assertEquals("C", fullStack.top());
  }

  ...
}       

Co bylo dřív? Vejce nebo slepice?

Testovaný kód nebo test?

Jedna z možností: Test-driven development

  1. Napsat test
  2. Spustit test – ověřit nesplnění
  3. Napsat kód – minimum pro splnění testu
  4. Spustit test – ověřit splnění
  5. Refaktorovat kód

Výhody TDD (pokud jej dokážete aplikovat)

Několik zajímavých odkazů k tématu:

Doslovná interpretace TDD může být kontraproduktivní, pokud chceme vyzkoušet více variant řešení nějakého problému, zhodnotit je podle nějakého kritéria a následně jednu z variant použít a jiné zahodit. TDD může omezit experimentování s kódem v tom smyslu, že pokud máme k různým variantám kódu rovnou psát i testy, zvětší se náklady na každou variantu, což může vést až k tomu, že se experimentování nevyplatí. Někdy může být tedy výhodnější metodiku porušit, ozkoušet víc variant a teprve dodatečně k nim dopsat testy.

Pro inteligentního vývojáře může být také nepřirozené přemýšlet v tak malých úsecích návrhu/kódu, jak často demonstruje/vyžaduje TDD; lépe by se mu pracovalo s většími celky a možná i na abstraktnější úrovni, než je kód. Pro takového vývojáře může být TDD ztráta času a tedy i produktivity.

Oba předchozí odstavce lze shrnout pod obecnou poučku, že žádná metodika není dokonalá a vhodná pro všechny situace.

Pokrytí

Definice pokrytí

Usilovat o 100% pokrytí?

Co patří do unit testů

Hlavní náplní jsou ...

Navíc převeďte na unit test...

Co nepatří do unit testů

Test není unit test, pokud...

Testy, které toto dělají jsou také potřeba, a můžou být napsány s pomocí unit-testing frameworku, ale nejsou to unit testy.

Jak se nepatřičným věcem vyhnout?

Stubs/Mock objects

Odkazy:

Zásady pro psaní testů

Testy musí být kompletně automatizované

Nevynalézejte kolo: použijte existující frameworky

Zásady pro psaní testů

Obecná struktura testu (AAA)

  1. příprava situace (arrange)
  2. provedení testované operace a získání výsledků (act)
  3. test shody očekávaných a skutečných výsledků (assert)

Obecné vlastnosti testů

Zásady pro psaní testů

Snažte se kód co nejvíc "provětrat"

Používejte správně asserty

Neodchytávejte výjimky

K používání správných assertů: Nebojte se si sadu assertů rozšířit, pokud zjistíte, že používáte nějaký assert spolu s pomocným kódem okolo na více místech.

Zásady pro psaní testů

1 třída kódu ~ 1 třída testů

1 metoda kódu ~ sada testovacích metod

Často opomíjené: kód testů je taky kód!

Zdrojový kód testů držte blízko testovaných tříd

K třídám kódu a testů: Toto pravidlo nemusí platit vždy, můžeme mít víc tříd testů na jednu třídu kódu. Pozor ale, je to často signál, že třída má víc různých zodpovědností. Podobné pravidlo o metodách neplatí – metody mají často víc různých aspektů, které je vhodné testovat zvlášť.

Testovatelný kód

Ne všechen kód je dobře testovatelný. Aby tomu tak bylo, je potřeba tomu uzpůsobit design, aby neobsahoval velké "slitky", do kterých není vidět a není možné jejich chování testovat po malých částech. Základní pravidla hezky sepsal Miško Hevery ve svém blogu.

Jak psát testovatelný kód

V kódu musí zůstat "švy"

Časté chyby

Možno aplikovat i na metody

Konstruktor dělá opravdovou práci

Jakou práci?

Práce v konstruktoru odstraňuje "švy" potřebné pro testování.

Konstruktor dělá opravdovou práci

Varovná znamení (code smells)

Konstruktor dělá opravdovou práci

Proč to vadí?

Příklad: používání new v konstruktoru

Obtížně testovatelný kód

/*
 * Basic new operators called directly in the class' constructor.
 * (Forever preventing a seam to create different kitchen and
 * bedroom collaborators).
 */
class House {
  Kitchen kitchen = new Kitchen();
  Bedroom bedroom;

  House() {
    bedroom = new Bedroom();
  }

  // ...
}
/*
 * An attempted test that becomes pretty hard.
 */
class HouseTest extends TestCase {
  public void testThisIsReallyHard() {
    House house = new House();
    // Darn! I'm stuck with those Kitchen and Bedroom
    // objects created in the  constructor.

    // ...
  }
}
        

Příklad: používání new v konstruktoru

Testovatelný a flexibilní design

class House {
  Kitchen kitchen;
  Bedroom bedroom;

  // Have Guice create the objects and pass them in
  @Inject
  House (Kitchen k, Bedroom b) {
    kitchen = k;
    bedroom = b;
  }
  // ...
}
/*
 * New and Improved is trivially testable, with any
 * test-double objects as collaborators.
 */
class HouseTest extends TestCase {
  public void testThisIsEasyAndFlexible() {
    Kitchen dummyKitchen = new DummyKitchen();
    Bedroom dummyBedroom = new DummyBedroom();

    House house =
        new House(dummyKitchen, dummyBedroom);

    // Awesome, I can use test doubles that
    //   are lighter weight.

    // ...
  }
}

Spolupracující objekty je nutné "dolovat"

Odkud?

Varovná znamení (code smells)

Spolupracující objekty je nutné "dolovat"

Proč to vadí?

Globální stav a nadužívání singletonů

Skrývá závislosti

Varovná znamení (code smells)