Groovy: řekni prostě, co chceš
My vývojáři už jsme si na nějakou tu nesrozumitelnost v kódu za ty roky zvykli. Víme, co dělá ta která funkce, a taky nám dlouho trvalo, než jsme jim vymysleli ta dlouhá a zcela nepochopitelná jména. Rovněž jsme dřív psali mnohastránkové elaboráty s popisem rozhraní, ale teď jedeme agilně, a tak pro jistotu nepíšeme vůbec nic.
Na bedra nebohých testerů pak nezřídka připadá úkol napsat integrační a systémové testy za použití našeho zdivočelého API. Pokud jsou naši testeři zběhlí v programování, máme napůl vyhráno. Ale co když ne? Potom nezbývá než obrnit se trpělivostí a také se pokusit psaní testů co nejvíce zjednodušit.
Jednou z možností je přiblížit zápis testů přirozené řeči, nejlépe mateřské. Tak, aby to zvládla i vaše matka. Nebo Jan Tleskač.
S pomocí Groovy je to možné.
Motivace
Pokusím se to demonstrovat na reálném příkladu. Na jednom projektu máme embedded zařízení (prodejní automat), které je možné řídit s pomocí PC přes sériovou linku. Na PC běží v Javě napsaný jednoduchý testovací framework, který posílá příkazy do zařízení a loguje odpovědi.
Základní příkazy:
- Inicializace -
setPower()
,flashFw()
,activateFw()
,flashCfg()
,activateCfg()
- Uživatelské akce -
pressButton()
,insertCoin()
- Servisní akce -
selftest()
- Kontrola stavu -
getCoinAccount()
,getOperationState()
,getDisplayString()
Berte to jako převeliké zjednodušení, ve skutečnosti jsou těch příkazů mraky a většinou se jmenují dost prapodivně, například yOnAppPiezo()
pro simulaci stisku tlačítka. Bez dokumentace je člověk zkrátka ztracen.
Při přípravě testovacího frameworku pro integrační a systémové testy jsem si stanovil několik cílů:
- Testy musí simulovat chování skutečných uživatelů
- Psaní testů musí zvládnout i testeři, kteří dosud neprogramovali
- Testy musí být srozumitelné i bez komentářů nebo dokumentace
- Automatické spuštění po každém úspěšném buildu
Zajímalo mě, zda lze testové sekvence typu Zapni, počkej půl minuty, stiskni zelené tlačítko a zkontroluj text na displeji vhodně algoritmizovat, aniž bych se příliš vzdálil od původního textového popisu. Nainstaloval jsem si tedy Eclipse-Groovy plugin a začal experimentovat.
Testujeme s Groovy
Groovy je krásné v tom, že odstraňuje většinu syntaktického balastu, takových těch středníků, závorek a hvězdiček, čímž kód nabývá na čitelnosti. Jinak v zásadě je Groovy dynamicky typovaný skriptovací jazyk, který lze ovšem volně kombinovat s Javou a výsledné class objekty jsou bez problémů použitelné v jakékoli Java VM. (Pokud je něco jinak, tak mě prosím opravte.)
Testovací třídu jsem nazval Machine
(soubor Machine.groovy
) a přidal do ní několik metod, které ukrývají volání low-level funkcí pro komunikaci se zařízením. Využil jsem pojmenovaných argumentů funkcí, které jsou uvnitř funkce převedeny na mapu<název parametru, hodnota>
, chovají se tedy podobně jako například kwargs
v Pythonu. Předpokládám, že mám k dispozici nějakou třídu CommLib
, která implementuje low-level API.
class Machine { private CommLib lib public final on = 1 public final off = 0 def power(state) { lib.setPower (state == on) } void wait(times) { if (times.seconds) { sleep times.seconds * 1000 } } public final ok = 3 public final cancel = 7 def press(btn) { lib.pressButton(btn) } def insert(what) { if (what.coin) { lib.insertCoin(what.coin) } } public final state = 1 public final coins = 1 def get(what) { if (what instanceof LinkedHashMap) { if (what.account && (what.account == coins)) { lib.getCoinAccount() } } else if (what == state) { lib.getOperationState() } } }
Díky Groovy nemusím řešit datové typy, stačí vědět, kam přijde číslo a kam text. Také nemusím psát return
, funkce jednoduše vrací výsledek posledního volaného příkazu. A abych nemusel při každém volání uvádět jméno instance testovaného objektu, připravil jsem si jednoduchý test runner:
public class MachineRunner { def Machine machine @Rule public TestName name = new TestName() MachineRunner() { this.machine = new Machine() super } @Before void beforeTest() { machine.power on } @After void afterTest() { machine.power off } void run(closure) { println "Testcase: " + name.getMethodName() closure.delegate = this.machine closure() println "Testcase finished" sleep 2000 // wait 2 secs } }
Importy samozřejmě uvádět musím, pro jednoduchost je zde vynechávám.
Celý test case pak uzavřu do closure a předhodím test runneru ke zpracování:
class MachineTest extends MachineRunner { @Before void setUp() { // some init } @Test(timeout = 10000L) void 'Insert 2.50 EUR and confirm'() { def testcase = { def coinsBefore = get account: coins insert coin: 0.50 wait seconds: 1 insert coin: 2.00 wait seconds: 1 press ok wait seconds: 3 def coinsAfter = get account: coins println "Coins before: " + coinsBefore + ", after: " + coinsAfter assertEquals coinsBefore + 2.50, coinsAfter, DELTA } run testcase } }
Dále už nezbývá než pustit MachineTest
jako JUnit test, ať už ručně v Eclipse, nebo například automaticky Jenkinsem na testovacím serveru, s náležitě nastaveným logováním.
Testerům teď stačí vyplnit tělo uzávěru testcase
příkazy, které se příliš neliší od přirozených povelů: press ok
, insert coin
, read coin account
... A aby to měli ještě jednodušší, dostanou Eclipse s následující předpřipravenou šablonou:
class ${Name}${cursor}Test extends MachineRunner { @Before void setUp() { // some init } @Test(timeout = ${10000}L) void '${testname}'() { def testcase = { // ${todo} test steps } run testcase } }
Doplnění: Eclipse bohužel naše příkazy uvnitř testcase
tvrdohlavě podtrhává, jako že je nezná. Nelze je tedy doplňovat ani pomocí našeptávače (content assist). To můžeme napravit nadefinováním DSL deskriptorů. Zde je soubor Machine.dsld
(ve stejném adresáři jako Machine.groovy
) s ukázkou deskriptoru:
contribute(enclosingClosure()) { provider "Machine" method name:'press', declaringType: "Machine", params: [button: int], doc: """<code>press ok | cancel</code> - Press a button""" method name:'insert', declaringType: Machine, namedParams: [coin: float], doc: """<code>insert coin: <i>value</i></code> Insert a coin of given value""" } contribute(enclosingCallName("press")) { provider "Machine.press()" property name: "ok" property name: "cancel" }
Not understood?
Uvážení, kdy se něco takového může hodit a kdy je human friendly vrstva navíc jen ztráta času, nechám na vás. Samozřejmě záleží na povaze vyvíjeného produktu i na samotném vývojovém a testovacím týmu.
Místo toho ještě uvedu příklad, jak by vypadal náš test case v jazyce českém:
@Test(timeout = 10000L) void 'Vlož 2.50 EUR and potvrď'() { def testcase = { def mincePředtím = přečti konto: mince vlož minci: 0.50 počkej sekund: 1 vlož minci: 2.00 počkej sekund: 1 stiskni ok počkej sekund: 3 def mincePotom = přečti konto: mince println "Mince předtím: " + mincePředtím + ", potom: " + mincePotom assertEquals mincePředtím + 2.50, mincePotom, DELTA } run testcase }
Groovy tohle spolkne v pohodě. Co by na to ovšem řekl váš tester? A Jan Tleskač?
comments powered by Disqus
Arabské klikyháky snadno a rychle
Dostal jsem poměrně exotický úkol: naučit naše automaty mluvit arabsky a hebrejsky. To by nemuselo být zas tak těžké, myslel jsem si, jenže jde o embedded zařízení optimalizovaná pro velmi nízkou spotřebu a to s sebou nese spoustu omezení.
Web na zelené louce IV.
Výborně, sajta nám jede, stěrače stírají a stránky v oblíbeném prohlížeči najíždějí jako po másle. Nezbývá, než se přihlásit do administrace a sepsat nějaké ty duchaplné texty pro ctihodné čtenáře.
Stop-motion akční trhák
Venku prší, v televizi nic není, pastelky a knížky už omrzely, a tak se drazí potomci nudí a demolují byt. Nezbývá než zakročit a vymyslet opravdickou zábavu, jak to dělají správní geekové: natočit s dětmi stop-motion film. Ale pořádný trhák, rozumíte.
Oblačná lokalizace s MemSource
Minule jsme tvořili uživatelský manuál a dnes bychom ho rádi přeložili do jiného jazyka. Ale jak na to? Datlovat ručně větu po větě je už v dnešní době pasé, neboť existuje spousta šikovných CAT nástrojů, které dokáží práci výrazně zjednodušit.