Testimise head tavad
Sissejuhatus
Korrektselt läbivate testide kirjutamine on lihtne. Testide kirjutamine, mis on loetavad, hooldatavad ja sellised mis tegelikult vigu päevavalgele toovad, nõuab veidi rohkem läbimõtlemist. See peatükk käsitleb tavasid ja praktikaid, mis tulevad testide kirjutamisel kasuks.
Testide nimetamine
Testi eesmärk peaks meetodi nimest selgelt välja tulema. See tähendab, et mida testitakse, millistel tingimustel ning mis on eeldatav tulemus. Teine arendaja peaks testi nimest aru saama, miks midagi katki läks.
Näited halbadest nimedest:
@Test void test1() { ... }
@Test void testController() { ... }
@Test void testStuff() { ... }
Näited headest nimedest:
@Test void testFindCarsInPriceRange_returnsMatchingCars() { ... }
@Test void testFindCarsInPriceRange_invalidArguments_returnsErrorMessage() { ... }
@Test void testDeleteCar_carNotFound_returnsNotFoundMessage() { ... }
Testide nimetamiseks on kokkulepitud mitmes erinevas konventsioonis. Järgnevad on mõned näited:
| Konventsioon | Näide |
|---|---|
test{Feature}_{scenario}_{expected} | testDeposit_negativeAmount_throwsException |
test{Feature}{Scenario} | testDepositNegativeAmountThrowsException |
should{Expected}_when{Scenario} | shouldThrowException_whenAmountIsNegative |
Vali üks nimetamise konventsioon ja kasuta seda kogu projektis järjepidevalt. Täpsest vormingust olulisem on see, et nimed oleksid kirjeldavad ja ühtsed.
Arrange-Act-Assert
Kõik testid peaksid järgima Arrange-Act-Assert (AAA) mustrit, kus iga testifaas eraldatakse selgelt ära.
Näiteks:
@Test
void testFindBooksByAuthor_returnsOnlyMatchingBooks() {
// Arrange — set up the test scenario
BookRepository repository = mock(BookRepository.class);
BookService service = new BookService(repository);
List<Book> martinBooks = List.of(
new Book("Clean Code", "Robert Martin", 2008, new BigDecimal("35"))
);
when(repository.findByAuthor("Robert Martin")).thenReturn(martinBooks);
// Act — call the method being tested
String result = service.findBooksByAuthor("Robert Martin");
// Assert — verify the result
assertEquals(martinBooks.toString(), result);
}
Selline struktuur teeb testidest arusaamise kergeks:
- Arrange: Mis tingimustel antud testi läbi viiakse?
- Act: Kuidas testi teostatakse (ehk mis meetodeid käivitatakse)
- Assert: Mis tulemusi oodatakse?
Võrreldes toodangus oleva koodiga, testid ei pea kompaktsed olema. Eelistage selget koodi lühikeste one-liner'ite asemel, koodile peale vaadates peaks eesmärk selge olema.
Testi ühte asja korraga
Iga test peaks keskenduma ainult ühele spetsiifilisele funktsionaalsusele/käitumisele. Kui testi skoop on liiga lai, siis selle katki minemisel on raske aru saada, mis täpselt selle põhjustas.
Halb näide - test, mis kontrollib mitut asja korraga
@Test
void testBookService() {
// tests getAllBooks
// tests findBooksByAuthor
// tests deleteBookByTitleAndAuthor
// ...
}
Hea näide - Iga funktsionaalsuse jaoks eraldi test
@Test
void testGetAllBooks_returnsAllBooksAsString() { ... }
@Test
void testFindBooksByAuthor_returnsMatchingBooks() { ... }
@Test
void testDeleteBook_bookExists_returnsSuccessMessage() { ... }
@Test
void testDeleteBook_bookNotFound_returnsNotFoundMessage() { ... }
Paljude väikeste ja fokusseeritud testide olemasolu on parem kui mõni üksik suur test. Kui väike test ebaõnnestub, on kohe selge, mis katki läks.
Testide sõltumatus
Testid ei tohiks üksteisest sõltuda. Kõik testid peaksid olema oma enda olekuga ning kõrvalmõjud eelnevatest testidest ei tohiks järgmist testi mõjutada.
Halb näide - testid sõltuvad käivitusjärjekorra järgi:
private List<String> items = new ArrayList<>();
@Test void testAddItem() {
items.add("apple");
assertEquals(1, items.size());
}
@Test void testRemoveItem() {
// Assumes testAddItem ran first!
items.remove("apple");
assertEquals(0, items.size());
}
Hea näide - igal testil oma olek:
@Test void testAddItem() {
List<String> items = new ArrayList<>();
items.add("apple");
assertEquals(1, items.size());
}
@Test void testRemoveItem() {
List<String> items = new ArrayList<>(List.of("apple"));
items.remove("apple");
assertEquals(0, items.size());
}
Antud juhul tuleb @BeforeEach kasuks:
class ShoppingCartTest {
private ShoppingCart cart;
@BeforeEach
void setup() {
cart = new ShoppingCart(); // fresh cart for every test
}
@Test void testAddItem() { ... }
@Test void testRemoveItem() { ... }
}
Maagilised arvud testides
Kui tavaliselt ollakse maagiliste arvude vastu, siis testides need on aktsepteeritavad. Selle õigustus on see, et testi nimest omaette peaks eesmärk ja kontekst selge olema.
Näiteks:
@Test
void testFindCarsNewerThan_2015_returnsNewerCars() {
when(repository.findNewerThan(2015)).thenReturn(List.of(car2020));
String result = service.findCarsNewerThan("2015");
assertEquals(List.of(car2020).toString(), result);
}
Arvu 2015 eraldamine konstandiks nagu YEAR_THRESHOLD ei parandaks siin loetavust.
Testi nimi selgitab juba selle eesmärki.
Testide kattuvus
Testikattuvus (code coverage) mõõdab, kui suur osa koodist käivitatakse testide jooksutamisel. Levinud mõõdik on rea kattuvus (line coverage) - protsent koodiridadest, mida vähemalt üks test puudutab.
Kattuvus on kasulik näitaja, kuid mitte eesmärk omaette:
- Kõrge kattuvus ei taga häid teste. Test, mis kutsub kõiki meetodeid, kuid ei kontrolli tulemusi, võib saavutada kõrge kattuvuse ilma ühtegi viga avastamata.
- 100% kattuvus ei ole tavaliselt praktiline. Mõnda koodi (nt ebatõenäoliste olukordade veakäsitlus või raamistiku boilerplate) ei pruugi olla mõistlik testida.
- Püüdle saavutada sisuline kattuvus. Kata ära oluline loogika, piirjuhtumid ja veateed. Kui mingi funktsionaalsus ei ole testidega kaetud, võib arvestada, et seda pole kontrollitud.
Mõtle kattuvusest kui tööriistast testimata koodi leidmiseks, mitte kui skoorist, mida maksimeerida. Eesmärk ei ole testida iga rida - eesmärk on testida iga olulist käitumist.