|
J Unit 3
Testování - příklad s římskými číslicemi
V tomto příkladu se používají pokročilejší techniky testování:
- testování výjimek,
- vytváření pomocných struktur pro testování,
- spouštění testů uvnitř cyklů,
1. Zadání příkladu
V této kapitole vytvoříme třídu, která bude převádět čísla na/z římská čísla. Římská čísla ze zapisují pomocí
velkých písmen, které zastupují jednotlivá čísla:
I = 1
V = 5
X = 10
L = 50
C = 100
D = 500
M = 1000
Pro skládání římských čísel se používají následující pravidla:
- Hodnoty písmen se až na výjimku popsanou v pravidle 2 sčítají, např. I je 1, II je 2, III jsou 3, VI je 6, VII je 7, VIII je 8.
- Písmena vyjadřující mocniny 10 (I, X, C, M) se mohou za sebou opakovat maximálně 3. Pokud by měly být za sebou čtyři, vyjádří se pomocí odečtení hodnoty od nejbližší vyšší číslice. Číslo 4 nelze vyjádřit jako IIII, ale jako IV, 40 se zapisuje jako XL (50 bez 10), 41 se zapíše jako XLI, 44 jako XLIV (50 bez 10 a poté 5 bez 1). Obdobně 9 se zapíše jako IX, 90 jako XL, 900 jako CM.
- Písmena vyjadřující 5, 50 a 500 se neopakují.
- Písmena římského čísla se zapisují od největšího k nejmenšímu, na pořadí záleží. DC označuje hodnotu 600, číslo CD vyjadřuje úplně odlišnou hodnotu (400). CI znamená 101, zápis IC není přípustný (nelze odečítat 1 od 100, hodnota 99 se zapisuje jako XCIX – 100 bez 10 a k tomu 10 bez 1).
- Pro každou hodnotu existuje pouze jeden správný zápis římskými číslicemi.
- Pokud je zápis římského čísla správný, vyjadřuje právě jednu hodnotu.
- Pomocí římských číslic lze zapsat pouze omezený rozsah hodnot – celá čísla od 1 do 3999
2. Vytvoření kostry třídy Roman
Prvním krokem je navržení rozhraní třídy - navrhnout metody a konstruktor. V našem případě vytvoříme třídu Roman, která bude mít dvě statické metody public static String toRoman(int arabInput) a public static int fromRoman(String romanInput).
- metoda toRoman převede celočíselný parametr na řetězec (String), ve kterém bude číslo vyjádřeno pomocí římských číslic,
- pokud vstupní parametr metody toRoman bude mimo přípustný rozsah (1 až 3999), vyvolá výjimku IllegalArgumentException,
- metoda fromRoman převede hodnotu zadanou pomocí římských číslic ve vstupním parametru (String) na hodnotu typu int,
- pokud vstupní parametr fromRoman nebude obsahovat přípustnou hodnotu, bude vyvolána výjimka NumberFormatException.
API třídy Roman bude vypadat následovně:
public class Roman {
public static int fromRoman (String romanInput) throws NumberFormatException {
return 0;
}
public static String toRoman (int arabInput) throws IllegalArgumentException {
return "";
}
}
3. Návrh testů
Po navržení API pokračujeme vytvořením testů, které můžeme rozdělit do tří skupin:
- testy správných hodnot. Budeme ověřovat, zda metody toRoman a fromRoman správně převádějí vybrané hodnoty.
- ověření, že metody toRoman a fromRoman správně ošetřují chybné hodnoty parametrů,
- test konzistence ověří, že pokud číslo převedu na římské číslice a poté zpět, tak získáme původní číslo,
Teoreticky můžeme napsat testy pro všech 3999 hodnot. Ale nedokážit si přestavit, že by někdo prakticky naprogramoval těchto 2x3999 testů (jeden pro každou metodu). Proto vybereme jenom několik hodnot, na kterých ověříme správnost převodu v obou směrech:
1 -> I 31 -> XXXI 1485 -> MCDLXXXV 3743 -> MMMDCCXLIII
2 -> II 148 -> CXLVIII 1509 -> MDIX 3844 -> MMMDCCCXLIV
3 -> III 294 -> CCXCIV 1607 -> MDCVII 3888 -> MMMDCCCLXXXVIII
4 -> IV 312 -> CCCXII 2499 -> MMCDXCIX 3940 -> MMMCMXL
5 -> V 421 -> CDXXI 2574 -> MMDLXXIV 3999 -> MMMCMXCIX
6 -> VI 528 -> DXXVIII 2646 -> MMDCXLVI
7 -> VII 621 -> DCXXI 2723 -> MMDCCXXIII
8 -> VIII 782 -> DCCLXXXII 2892 -> MMDCCCXCII
9 -> IX 870 -> DCCCLXX 2975 -> MMCMLXXV
10 -> X 941 -> CMXLI 3051 -> MMMLI
50 -> L 1043 -> MXLIII 3313 -> MMMCCCXIII
100 -> C 1110 -> MCX 3408 -> MMMCDVIII
500 -> D 1226 -> MCCXXVI 3501 -> MMMDI
1000 -> M 1301 -> MCCCI 3610 -> MMMDCX
|
Následující tabulka obsahuje skupiny chybných vstupních hodnot.
metoda | typ chyby | chybné vstupní hodnoty |
toRoman | vstupní parametr mimo rozsah 1-3999 | -1, 0, 4000 |
fromRoman | nepřípustné znaky v parametru | "I ", "i", "a", "mm", "d", "MCi" |
fromRoman | příliš mnoho opakování římských číslic | "MMMM", "VV", "LL", "CCCC", "DD", "IIII" |
fromRoman | opakující se dvojice | "CMCM", "CDCD", "IVIV", "IXIX", "XLXL" |
fromRoman | chybné "odčítání" římských číslic | "IIMMCC", "VX", "DCM", "CMM", "CMD", "IXIV", "MCMC", "XCX", "IVI", "LM", "LD", "LC" |
4. Jak naprogramovat testy?
4.1 Pro každé porovnání samostatná testovací metoda
První možnost je pro každou hodnotu vytvořit samostatnou testovací metodu (v případě správných hodnot dvě metody), např.
@Test
public void testToRoman621() {
assertEquals("DCXXI", Roman.toRoman(621));
}
@Test
public void testFromRoman621() {
assertEquals(621, Roman.fromRoman("DCXXI"));
}
Výhody:
- název metody popisuje test, v assertEquals není potřeba psát komentáře,
- vyzkouší se všechny testy (porovnejte s následujícími přístupy pro zápis testů),
|
Nevýhody:
- velmi pracné napsat více než 100 testovacích metod,
- více kódu - větší pravděpodobnost chyb, testovací třída je nepřehledná,
|
4.2 Více porovnání v jedné testovací metodě
Druhá možnost, sloučit podobné testy do jedné testovací metody:
@Test
public void testToRomanZname() {
assertEquals("I", Roman.toRoman(1));
assertEquals("II", Roman.toRoman(2));
assertEquals("III", Roman.toRoman(3));
assertEquals("IV", Roman.toRoman(4));
assertEquals("V", Roman.toRoman(5));
......
}
Výhody:
- méně kódu, než v předchozím přístupu,
- stejné testy seskupeny k sobě,
- lze ověřit, že počáteční stav odpovídá předpokladům či že kroky k nastavení testované situace proběhnou správně. Toto je ukázáno na
|
Nevýhody:
- když se zjistí chybný výsledek, tak se ukončí testovací metoda, ostatní porovnání v metodě se již nezkouší,
- ve výpisu s popisem chyby se obtížněji orientuje - není přímo zobrazeno, při kterém porovnání vznikla chyba. V BlueJ je možné si zobrazit řádek, na kterém došlo k chybě (tlačítko "Zobrazit zdrojový kód").
|
Následuje ukázka testu metody celkemBoduUtoku třídy Hra z domácího úkolu Hra?. První porovnání ověří počáteční stav - že metoda na začátku správně vrací body útoku hráče. Další dvě porovnání slouží k ověření, že se podařilo nasadit zbraň a brnění. Poslední porovnání ověří, že se správně sečtou body útoku.
@Test
public void testCelkovePoctyBodu() {
Hrac hrac = new Hrac ("trpaslík", 5, 2, 3);
Brneni brneni = new Brneni ("ocelové brnění", 3, 2, 1);
Zbran zbran = new Zbran ("meč", 3, 2, 1);
// otestování počáteční stavu před nasazením brnění a zbraně
assertEquals(5, hrac.celkemBoduUtoku());
// ověřím, zda se podaří vzít zbraň a brnění
assertEquals(true, hrac.vemZbran(zbran));
assertEquals(true, hrac.vemBrneni(brneni));
// ověřím celkový počet bodů po nasazení brnění a zbraně,
assertEquals(11, hrac.celkemBoduUtoku());
}
4.3 Hodnoty k testování v datových strukturách
Třetí možnost je připravit si datové struktury (např. pole) s jednotlivými hodnotami/dvojicemi a tyto struktury procházet v testovacích metodách. Pro uložení dvojice arabské-římské číslo si vytvoříme pomocnou třídu Dvojice, která bude uvnitř testovací třídy RomanTest:
private static class Dvojice {
final int arab;
final String roman;
Dvojice (int arab, String roman) {
this.arab=arab;
this.roman=roman;
}
};
Poté vytvoříme pole, které bude obsahovat instance této třídy, každá s arabským a římským číslem:
private Dvojice znameHodnoty [] = {
new Dvojice(1,"I"),
new Dvojice(2,"II"),
new Dvojice(3,"III"),
new Dvojice(4,"IV"),
new Dvojice(5,"V"),
//......
Po nadefinování hodnot v této tabulce je snadné napsat obě metody pro kontrolu správnosti výsledků. Vzhledem k tomu, že v rámci metody probíhá velké množství testů v cyklu, je potřeba věnovat velkou pozornost komentáři, který se zobrazí v případě chyby.
@Test
public void testToRomanZnameHodnoty () {
for (Dvojice dvojice : znameHodnoty) {
String vysl = Roman.toRoman(dvojice.arab);
String komentar = "toRoman(" + (dvojice.arab) + ") : ";
assertEquals(komentar, dvojice.roman, vysl);
}
}
@Test
public void testFromRomanZnameHodnoty () {
for (Dvojice dvojice : znameHodnoty) {
int vysl = Roman.fromRoman(dvojice.roman);
String komentar = "fromRoman(\""+ dvojice.roman + "\") : ";
assertEquals(komentar, dvojice.arab, vysl);
}
}
Výhody:
- méně kódu pokud testujeme větší množství rozdílných vstupních hodnot,
- pole s hodnotami pro porovnání je přehlednější než předchozí varianty,
- při rozšiřování vkládá programátor jenom hodnoty, nemusí psát kód s větší pravděpodobností udělat chybu,
|
Nevýhody:
- vytvářet pomocné datové struktury má smysl v případě, že mám větší množství stejných testů s odlišnými parametry,
- když se zjistí chybný výsledek, tak se ukončí testovací metoda, ostatní porovnání v metodě se již nezkouší,
- ve výpisu s popisem chyby není přímo zobrazeno, při kterém porovnání vznikla chyba. Nepomůže ani zobrazení zdrojového řádku, neboť všechny porovnání probíhají na stejném místě uvnitř cyklu. Pro přehlednost je nutné do metody s porovnáním doplnit výstižný komentář.
- je náročnější vymyslet potřebné struktury a naprogramovat testovací metody. Není to pro začátečníky.
|
Spuštění testů skončí chybou, neboť jsme metody ve třídě Roman nenaprogramovali. Můžeme se ale podívat, zda komentář k testu je dostatečné výstižný:
5. Testy chybných hodnot (testy výjimek)
Vedle testů správnosti výsledků je potřeba též napsat testy, zda v případě chybných vstupních parametrů zahlásí
metody chybu.
5.1 Test jedné výjimky v metodě
Pokud chceme ověřit, že při chybném vstupu vznikne výjimka, tak nejjednodušší je do anotace doplnit element expected s očekávanou výjimkou. Pokud výjimka nevznikne či vznikne jiná výjimka, tak test skončí chybou:
@Test(expected=IllegalArgumentException.class)
public void testToRomanException0() {
Roman.toRoman(0);
}
@Test(expected=IllegalArgumentException.class)
public void testToRomanException_1() {
Roman.toRoman(-1);
}
@Test(expected=IllegalArgumentException.class)
public void testToRomanException4000() {
Roman.toRoman(4000);
}
Element expected v anotaci vede k jednoduchým testovacím metodám na výjimky, neboť je možné testovat pouze jednu výjimku. Za řádek, na kterém má vzniknout výjimka, nemá smysl uvádět další kód, neboť se neprovede. Nelze též testovat obsah zprávy předané výjimkou.
5.2 Ošetření chybných arabských čísel
Pro ověření vzniku výjimky lze též použít konstrukci try ... catch pro odchytávání výjimek. Pokud výjimka nevznikne, tak se pomocí
metody fail(String textChyby) ohlásí chyba.
Následuje test ošetření chybných vstupních arabských čísel v metodě toRoman - v cyklu se projde seznam chybných hodnot a pro každou se zavolá metoda toRoman.
@Test
public void testToRomanException () {
int chybneArabske [] = { 0, -1, 4000 };
for (int hodnota : chybneArabske) {
try {
String vysl=Roman.toRoman(hodnota);
fail ("Očekávána výjimka IllegalArgumentException pro Roman.toRoman("
+ hodnota + ")");
}
catch (IllegalArgumentException e) {
continue;
}
}
}
Otázky:
- jak skončí test, pokud by v metodě toRoman() vznikla jiná výjimka než IllegalArgumentException?
- jsou přehlednější tři samostatné metody (5.1) nebo toto řešení s cykly?
- jak byste ověřili text zprávy (message) předané výjimkou?
|
5.3 Ošetření chybných římských čísel
Pro test ošetření chybných vstupních parametrů metody fromRoman (chybných římských čísel) se vytvoří pomocná mapa, která se naplní v konstruktoru:
private Map<String, String []> chybneRimske;
public RomanTest() {
chybneRimske = new TreeMap<>();
chybneRimske.put("nepřípustné znaky v římském čísle",
new String[] { "I ", "i", "a", "mm", "d", "MCi" });
chybneRimske.put("přiliš mnoho opakování znaků",
new String[] { "MMMM", "VV", "LL", "CCCC", "DD", "IIII" });
chybneRimske.put("nepřípustně se opakující dvojice",
new String[] { "CMCM", "CDCD", "IVIV", "IXIX", "XLXL" });
chybneRimske.put("chybné \"odčítání\"",
new String[] { "IIMMCC", "VX", "DCM", "CMM", "CMD", "IXIV", "MCMC",
"XCX", "IVI", "LM", "LD", "LC" });
}
Metoda pro testování ošetření chybných vstupů obsahuje dva vnořené cykly:
@Test
public void testFromRomanException () {
for (String typ : chybneRimske.keySet()) {
for (String hodnota : chybneRimske.get(typ)) {
try {
int vysl=Roman.fromRoman(hodnota);
}
catch (NumberFormatException e) {
continue;
}
fail("Očekávána výjimka NumberFormatException pro Roman.fromRoman(\""
+ hodnota + "\"); typ chyby: " + typ);
}
};
}
6. Test konzistence
V testech správných převodů se neověřili všechny přípustné hodnoty. Částečně to lze dohnat následujícím testem, ve kterém všechna čísla od 1 do 3999 převedu na římské a zpět. Poté zkontroluji, zda výsledek odpovídá vstupní hodnotě, ověřím vztah:
i = fromRoman(toRoman(i));
pro hodnoty i v rozsahu od 1 do 3999.
@Test
public void testKonzistence () {
for (int i=1; i < 4000; i++) {
int vysl = Roman.fromRoman(Roman.toRoman(i));
assertEquals(i, vysl);
};
}
7. Metoda toRoman
Nyní je čas začít psát vlastní třídu (v praxi se většinou střídá psaní testů s psaním vlastního kódu, náš případ je
poměrně jednoduchý a tudíž i z pedagogických důvodů jsme popsali nejdříve všechny testy). Začneme třídu
toRoman, která je jednodušší (je rychle vidět pokrok v počtu úspěšných testů):
Nejdříve doplníme test přípustného intervalu hodnot:
public static String toRoman (int arabInput) throws IllegalArgumentException {
if ((arabInput > 3999) || (arabInput <= 0)) {
throw new IllegalArgumentException("Přípustná jsou čísla z intervalu 1-3999");
}
else {
return "";
}
}
Program přeložíme a spustíme test – zjistíme, že test testToRomanException úspěšně prošel.
Nyní do metody toRoman doplníme konverzi na římské číslice. Nejdříve si vytvoříme pomocné pole, které bude obsahovat dvojice hodnot, ze kterých lze skládat římské číslice. Nejsou zde uvedeny pouze jednotlivá písmena, ale i dvojice znaků, které mají speciální význam (4, 9, 40 ...). Pro uložení dvojic si nadefinujeme vnitřní třídu Dvojice (stejná třída, jakou používáme v testovací třídě).
private static class Dvojice {
final int arab;
final String roman;
Dvojice (int arab, String roman) {
this.arab=arab;
this.roman=roman;
}
}
private static Dvojice tabulka [] = {
new Dvojice (1000, "M" ),
new Dvojice (900, "CM" ),
new Dvojice (500, "D" ),
new Dvojice (400, "CD" ),
new Dvojice (100, "C" ),
new Dvojice ( 90, "XC" ),
new Dvojice ( 50, "L" ),
new Dvojice ( 40, "XL" ),
new Dvojice ( 10, "X" ),
new Dvojice ( 9, "IX" ),
new Dvojice ( 5, "V" ),
new Dvojice ( 4, "IV" ),
new Dvojice ( 1, "I" )
};
Čísle v tabulce jsou uvedena sestupně. Při převodu se vyhledá první řádek v tabulce, kde arabská číslice je větší či stejná jako převáděné číslo.
TODO: obrázek s výpočtem např. pro číslo 146.
Nyní je již poměrně snadné napsat celou metodu toRoman:
public static String toRoman (int arabInput) throws IllegalArgumentException {
if ((arabInput > 3999) || (arabInput <= 0)) {
throw new IllegalArgumentException("Přípustná jsou čísla z intervalu 1-3999");
}
else {
int cislo = arabInput;
String vysl="";
while (cislo > 0) {
for (int j=0; j < tabulka.length; j++) {
if (cislo >= tabulka[j].arab) {
vysl=vysl+tabulka[j].roman;
cislo=cislo-tabulka[j].arab;
break;
};
};
};
return vysl;
}
}
Nyní budou úspěšné dva testy - testToRomanZnameHodnoty a testToRomanException.
8. Úkoly
- Napište metodu fromRoman.
- Při psaní římských číslic se někdy připouští možnost psaní čtyř písmen M za sebou, tj je možno pomocí římských číslic zapsat čísla od 1 do 4999. Upravte testy a třídu Roman tak, aby toto umožňovala.
|
|