Liigu peamise sisu juurde

Üksuste testimine

Sissejuhatus

Üksustest (unit test) kontrollib lühikest, isoleeritud osa koodist (tavapäraselt ühte meetodit), kutsudes seda ette antud sisenditega ja kontrollides, et väljund vastab ootustele.

Antud kontekstis üksus viitab väikseimale funktsionaalsuse osale, mida saab iseseisvalt testida. Javas on selleks tavaliselt meetod või klass.

Üksustesti struktuur

Iga üksustest järgib sama algelist struktuuri:

  1. Seadistamine (setup) - testi läbiviimiseks sobivate tingimuste loomine (objektide loomine, andmete ettevalmistus jne.).
  2. Testi läbiviimine (call) - kontrolli all oleva meetodi käivitamine eelnevat ette valmistatud andmetega.
  3. Kontroll (check) - Tulemuse kontrollimine ootuste vastu.
@Test
void testReverseString() {
// 1. Setup
StringUtils utils = new StringUtils();

// 2. Call
String result = utils.reverse("hello");

// 3. Check
assertEquals("olleh", result);
}

Seda struktuuri kutsutakse ka Arrange-Act-Assert (või Given-When-Then) nime järgi, millega tutvume lähemalt testimise heade tavade peatükis.

Mis teeb ühest üksusest hea testitava üksuse

Meetodil, mida on lihtne üksustestida, on mõned kindlad omadused:

  • Sellel on selged sisendid (parameetrid).
  • See annab selge väljundi (tagastusväärtuse) või tekitab jälgitava efekti.
  • See ei sõltu välistest süsteemidest (andmebaas, võrk, failisüsteem).

Võrdle neid kahte meetodit:

// Easy to test — pure input/output
public static int max(int a, int b) {
return a > b ? a : b;
}

// Harder to test — reads from database, prints to console
public void printTopCustomer() {
Customer top = database.getTopCustomer();
System.out.println(top.getName());
}

Esimest meetodit on väga lihtne testida: kutsu seda välja kahe arvuga ja kontrolli tulemust. Teine meetod sõltub andmebaasist ja annab väljundi printimise kaudu - mõlemad muudavad testimise keerulisemaks.

Kui koodi on raske testida, viitab see sageli sellele, et sellel on liiga palju vastutusi. Loogika eraldamine sisend/väljundi (I/O) käsitlemisest muudab mõlemad osad nii lihtsamini testitavaks kui ka paremini kasutatavaks.

Mida testida

Testi käitumist, mitte teostust. Keskendu sellele, mida meetod teeb, mitte seda, kuidas see sisemiselt töötab. Koodis teostusdetailide refaktoreerimisel peaksid eelnevalt loodud testid ikka läbima.

Testi piirjuhtumeid (edge cases). Need võivad näiteks olla:

  • Tühi sisend ("", tühi kogum, null)
  • Minimaalsed ja maksimaalsed väärtused (0, Integer.MAX_VALUE)
  • Täpselt üks element, täpselt piirväärtusel

Eesmärk on testida olukordi, kus sisend on piiripealselt vastu võetav, kuid peaks ikka õige tulemuse andma.

Testi veajuhtumeid. Kui meetod peaks vigase sisendi korral viskama erindi või tagastama eriväärtuse, kontrolli seda.

@Test
void testDivideByZeroThrowsException() {
assertThrows(ArithmeticException.class, () -> Calculator.divide(10, 0));
}

Testi happy path’i. Kõige tavalisem ja oodatud kasutusjuht peab samuti olema kaetud.

Mida mitte testida

Kõike samas testima ei peaks, näiteks:

  • Triviaalsed getter- ja setter-meetodid, mis ei tee muud kui tagastavad või omistavad välja väärtuse.
  • Raamistiku koodi - ei ole vaja testida, et Java ArrayList.add() töötab. Standardteeki (ja ka muid laia kasutusega teeke) võib usaldada.
  • Privaatsed meetodid - need on teostuse detailid. Testi neid kaudselt läbi avalike meetodite, mis neid kasutavad.

Väited (assertions)

Väited (assertions) on kontrollid, mis määravad, kas test läbib või kukub läbi. Testimisraamistikud pakuvad erinevaid väitemeetodeid eri tüüpi võrdluste jaoks:

assertEquals(expected, actual);          // checks equality
assertNotEquals(a, b); // checks inequality
assertTrue(condition); // checks that condition is true
assertFalse(condition); // checks that condition is false
assertNull(value); // checks that value is null
assertNotNull(value); // checks that value is not null
assertThrows(Exception.class, () -> { // checks that code throws exception
riskyMethod();
});

Ujukomaarvude kontroll

Ujukomaarvude aritmeetika ei ole täpne. Kuna double ja float esitatakse mälus ligikaudselt, võivad arvutustes tekkida väikseid ümardusvigu:

System.out.println(6 * 0.1);    // 0.6000000000000001, not 0.6

Seetõttu võib ujukomaarvude tulemuste otsene võrdlemine assertEquals-iga põhjustada ebastabiilseid teste. JUnit pakub assertEquals varianti, mis võtab argumendiks delta (ehk lubatud hälve):

assertEquals(0.6, 6 * 0.1, 0.0001);    // passes — difference is within tolerance

Kolmas argument määrab maksimaalse lubatud erinevuse oodatud ja tegeliku väärtuse vahel.

== vs equals()

Samuti peab meeles pidama, kuidas Javas == operaator ja equals() meetod käituvad.

Primitiivtüüpide puhul (int, double, char jne.) võrdleb == operaator väärtusi otse:

System.out.println(1 == 1);    // true
System.out.println(1 == 2); // false

Objektide puhul (String, Integer, enda poolt loodud klassid) kontrollib == operaator, kas kaks viidet osutavad samale objektile mälus, mitte kas nende sisu on sama:

System.out.println(new Person("Alice") == new Person("Alice"));    // false — two different objects

Objektide võrdlemiseks peab kasutama equals() meetodit:

Integer x = 128;
Integer y = 128;
System.out.println(x == y); // false — different objects
System.out.println(x.equals(y)); // true — same value

Ehk kuldreegel on, et primitiivide puhul kasutage == operaatorit, objektide puhul equals() meetodit. Raamistike meetodid nagu assertEquals järgivad seda reeglit automaatselt.

null-viide võrdlemisel

Kui kasutad koodis otse equals() meetodit, ole ettevaatlik null-väärtustega:

String value = null;
value.equals("test"); // NullPointerException!
"test".equals(value); // false — safe

equals() kasutamine teadaolevalt mitte-null väärtusega konstandi peal aitab vältida NullPointerException-it. assertEquals kasutamisel see probleemiks ei ole, kuna testimisraamistik käsitleb null-võrdlusi turvaliselt.