Kto netestuje, nech ani neje! Unit testy fungujú aj v Androide, takže žiadne výhovorky neplatia. Ukážeme si to na jednoduchej aplikácii konvertora (prevodníka) kurzov medzi eurom a okolitými menami Slovenska: českými korunami (CZK), forintami (HUFmi), zlotým (PLN) a … tradičným dolárom (USD).
Napíšeme si takúto aplikáciu, ukážeme ako možno zapísať unit testy v klasike žánru, v JUnite.
Článok stojí a padá na filozofii striedavého písania testov a kódu a prepokladá, že ste aspoň z rýchlika videli JUnit. Pretože ak viete JUnit máte polovicu námahy za sebou.
Ak to všetko vieme, poďme si nakresliť vizuál.
Aplikácia si požiada sumu, používateľ si z rozbaľovátka (po slovensky rozbaľovadla alias combo boxu alias spinnera) vyberie menu a vzápäť uvidí celkovú hodnotu a vie, že kolbász sa mu oplatí viac než v COOP Jednote.
Architektúra
Hoci je to smiešna aplikácia, i tu má zmysel uvažovať o rozumnom návrhu, či vzletne povedané, architektúre.
Budeme potrebovať jednu aktivitu (MainActivity
) s príslušným XML layoutom. Kto bude riešiť samotný výpočet?
Ak by sme mali jednovrstvovú aplikáciu, všetko by sme natrieskali do kódu v aktivite: všetky prevody, zoznamy známych mien (nominatív mena) a prevodné kurzy. Lenže zákazník chce viac a viac featúr (alebo featuridíta posadne vývojára) a zrazu sa budú kurzy ťahať dynamicky z webu Národnej banky, tu hľa pribudne možnosť pridávať vlastné meny a trieda MainActivity
speje cestou božskosti — a je z nej učebnicový príklad božieho objektu.
Nehovoriac o tom, že sa to zle udržiava a teda zle testuje. (Čo bolo skôr? Zlý dizajn alebo zlý test? ;-)).
Všetku aplikačnú (= biznis, lebo ide o peniaze) logiku vytiahnime do samostatnej triedy. Lebo používateľské rozhranie nemá čo prepočítavať kurzy a hrajkať sa s číslami: to nie je jeho zodpovednosť (má len zobrazovať údaje, a získavať ich od používateľa.)
Hľa, obrázok:
Príprava implementácie, hlavný projekt
Založme si klasický androidný projekt: a pokrsťme ho ako Currencr. Balíček si dajme aký chceme: u nás:
sk.upjs.ics.currencr
Nechajme si vygenerovať jednu aktivitu i layout súbor.
Unit testy najsamprv
Príprava
Poďme teraz vytvárať testy. Najjednoduchší spôsob je vytvoriť druhý projekt, určený len na testovanie. Existuje aj možnosť vytvárať všetko v jednom projekte, ale na začiatku je s tým množstvo zápasenia, ktoré vie byť frustrujúce a dva projekty môžu byť chaotické, ale v skutočnosti je práve v oddeľovaní základ ordnungu.
Klikajme:
- z hlavného menu Eclipse: File | New | Other vyberme Android Test Project
- zvoľme projekt, ktorý budeme testovať: v našom prípade Currencr
- vyberme verziu Androida.
Čo je inšô oproti bežnému projektu?
Nie ste technickí šprtači? Preskočte túto časť. Inak si všimnite balíček: ADT vygenerovalo projekt s balíčkom
sk.upjs.ics.currencr.test
Áno, na konci je .test
. Dôležité upozornenie: ak ste zvyknutí z iných projektov mať testy a kód v rovnakom balíčku a budete chcieť presunúť testy do sk.upjs.ics.currencr
, spôsobíte si tým veľa bolesti, ba priam nespustiteľných projektov. Nechajte to na začiatok radšej podľa tohto návrhu ADT.
Druhá odlišnosť je zavedenie instrumentation, teda nástrojov, ktoré sa dokážu zavesiť na beživú aplikáciu a zbierať informácie, ktoré sa potom používajú pri vyhodnocovaní unit testov.
V manifeste AndroidManifest.xml
sa nachádza element
<instrumentation
android:name="android.test.InstrumentationTestRunner"
android:targetPackage="sk.upjs.ics.currencr" />
Druhý vec je deklarácia uses-library
, ktorá zavedie do projektu testovací framework JUnit
.
<uses-library android:name="android.test.runner" />
Naozaj už píšme test
Kliknime pravým na názov balíčka a dajme New | JUnit Test Case. Android, súc kultúrnou platformou vychádzajúcou z open source, využíva klasika unit testov: framework JUnit. (Bohužiaľ, v starej verzii 3.8. Žiadne anotácie sa teda nekonajú.)
Urobme si jednoduchý unit test
package sk.upjs.ics.currencr.test;
import java.math.BigDecimal;
import sk.upjs.ics.currencr.EuroCurrencyConverter;
import junit.framework.Assert;
import junit.framework.TestCase;
public class EuroCurrencyConverterTest extends TestCase {
private EuroCurrencyConverter currencyConverter;
@Override
protected void setUp() throws Exception {
super.setUp();
currencyConverter = new EuroCurrencyConverter();
}
public void testConvert() throws Exception {
Assert.assertEquals(new BigDecimal("131.1900"), currencyConverter.convert("USD", new BigDecimal(100)));
Assert.assertEquals(new BigDecimal("2594.200"), currencyConverter.convert("CZK", new BigDecimal(100)));
}
}
V metóde setUp()
si pripravíme inštanciu ešte neexistujúceho konvertera a v metóde testConvert()
oskúšame jednoduché prípady. Statická metóda assertEquals()
na zabudovanej triede Assert
overuje podmienku, ktorá musí platiť, ak má test zbehnúť.
Poznámka pre inovatívnych používateľov novšieho JUnitu: nepoužívajú sa anotácie: trieda musí dediť od TestCase
a každá testovacia metóda sa musí volať testXXX()
. Metódy ako setUp()
a tearDown()
sú prekryté.
V tomto prípade test zbehne, ak prevodník správne prevedie sto dolárov pri pomyselnom kurze 1.3119 na 131 dolárov a 19 centov. Dve nuly na konci sú povinné: je to totiž implementačná záležitosť triedy BigDecimal
súvisiaca s presnosťou — možno to nie je dokonalé, ale pre jednoduchosť to musí stačiť.
Obnosy sú BigDecimal
Kde sa točia peniaze, nie je vhodné používať double
a float
. Zaokrúhľovacie chyby totiž vedia robiť divy. Na evidenciu obnosu preto použijeme radšej java.util.BigDecimal
. Nerobí sa s nimi ktovieako pohodlne, ale rozhodne je to bezpečný spôsob, ktorý nevedie k sumám ako 3.79999999999999999999
.
Implementácia
Triede EuroCurrencyConverter
postačia dve metódy: jedna, ktorá získa kód meny a obnos a vráti prevedenú hodnotu. Voliteľná je metóda, ktorá zistí, či tento konverter podporuje prevod z EURa do danej meny — hodí sa to neskôr.
Triedu vytvorme v pôvodnom projekte CurrencR
(nie v testovacom!) v balíčku sk.upjs.ics.currencr
.
package sk.upjs.ics.currencr;
import java.math.BigDecimal;
import java.util.HashMap;
import java.util.Map;
public class EuroCurrencyConverter {
private Map<String, BigDecimal> currencyRatios = new HashMap<String, BigDecimal>();
public EuroCurrencyConverter() {
currencyRatios.put("USD", new BigDecimal("1.3119"));
currencyRatios.put("CZK", new BigDecimal("25.942"));
currencyRatios.put("HUF", new BigDecimal("297.79"));
currencyRatios.put("PLN", new BigDecimal("4.1124"));
}
public BigDecimal convert(String currencyCode, BigDecimal amount) {
if(!supportsCurrency(currencyCode)) {
throw new IllegalArgumentException("Currency code '" + currencyCode + "' is not supported");
}
BigDecimal ratio = currencyRatios.get(currencyCode);
return ratio.multiply(amount);
}
private boolean supportsCurrency(String currencyCode) {
return currencyRatios.containsKey(currencyCode);
}
}
Implementácia bude veľmi hlúpa: v mape držíme zoznam kódov mien a prevodné kurzy. Ak sa ktosi snaží previesť euro na vatikánsku menu, dostane za trest výnimku.
Spustenie prvého testu
Ak máme unit test a kód, môžeme testovať! Pravým kliknime na EuroCurrencyConverterTest
a zvoľme Run | Run As a Android JUnit Test. Pozor, nie JUnit Test! Ten nielenže bude chcieť špeciálnu konfiguráciu, ale v prípade nesprávnej voľby povedie k hrozným pádom. Ste v Androide, spúšťate androidné JUnity.
Spúšťanie testu nebude rozhodne okamihom: ADT totiž potrebuje mať spustený emulátor, do ktorého nainštaluje celú aplikáciu vrátane unit testu, spustí ich, vďaka inštrumentácii zistí stav a po dobehnutí vyhodnotí úspešnosť.
To celé môže trvať aj pol minúty: nezľaknite sa. Moment na trpké slzy:
Čas je meraný len od chvíle spustenia testu do dobehnutia vo vnútri emulátora. Čas inštalácie a pod. sa neberie do úvahy.
Ďalšie implementácie
Pokojne môžeme dodať do unit testu ďalšiu metódu:
public void testConvertUnsupportedCurrency() throws Exception {
try {
String weirdCurrencyCode = "XXX";
currencyConverter.convert(weirdCurrencyCode, BigDecimal.TEN);
fail("Conversion of '" + weirdCurrencyCode + "' failed");
} catch (IllegalArgumentException e) {
// this is expected behaviour
}
}
Tá indikuje, že vo chvíli, keď sa snažíme o prevod podivnej meny, máme dostať výnimku. Ak ju nedostaneme, niečo je zle a test nesmie prejsť — preto ten fail()
na konci.
Spustenie oboch testov
Spustenie všetkých testov je opäť jednoduché: pravý klik na EuroCurrencyConverterTest
a zvoľme Run | Run As a Android JUnit Test. Spustia sa oba testy a vyhodnotí ich výsledok. Ak chceme spustiť len jeden konkrétny test, kliknime naň na karte Outline a spusťme ho takým istým spôsobom.
Sumár tejto časti
Jednou vetou: v Androide existuje JUnit. Stačí založiť nový projekt a biznis logiku testujeme ako v prípade akejkoľvek inej aplikácie.
Do budúcna
V ďalšom dieli si ukážeme, ako testovať samotné aktivity. Z papierového obrázka prekreslíme škatule do widgetov aktivity a otestujeme to.
Ďalšie zdroje
- JUnit Test Infected: Programmers Love Writing Tests (http://junit.sourceforge.net/doc/testinfected/testing.htm#Beck, K. Smalltalk Best Practice Patterns)
- dokumentácia k základom testovania v Androide
- dokumentácia k základom testovania aktivít v Androide