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.
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.
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í).
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.
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);
}
}
public final class SimpleStackTest {
private SimpleStack<String> emptyStack;
private SimpleStack<String> fullStack;
@Before
public void setUp() {
emptyStack = new SimpleStack<>();
fullStack = new SimpleStack<>();
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());
}
...
}
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.
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.
assertEquals
, assertNotEquals
,
assertSame
, assertNotSame
,
assertNull
, assertNotNull
,
assertFalse
, assertTrue
assertThrows
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.
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ášť.
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.
new
v konstruktoru nebo deklaraci atributůnew
v konstruktoru/*
* 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. // ... } }
new
v konstruktoruclass 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.
// ...
}
}