Databáze v testech?

Včera jsem měl na první české PyCON konferenci přednášku o pytestu. Na Twitteru se rozjela diskuze o tom, že můj ukázkový test není unittest. Což je teda úplně mimo moji přednášku, protože jsem mluvil o unittestu jako toolu, nikoliv typu testu, vs. pytestu. A integrační test jsem vybral, protože je to prostě dobrá ukázka pro běžnou praxi.

Každopádně bych chtěl sdílet můj názor na toto téma. V Twitter vlákně se řeší několik věcí – udržovatelnost takových testů, zda mít různé typy separátně, spolehlivost, schopnost najít co nejvíce chyb a hlavně teorie. A jak já rád říkám, teoreticky mezi teorií a praxí není rozdíl, ale prakticky… :-)

Samozřejmě, nejlepší možný test je unittest, který nepotřebuje nic. Žádné cizí rozhraní, žádnou databázi, žádný přístup na disk ani nikam jinam, prostě nic. Pokud něco takového lze udělat a otestuje se tím vše potřebné, pak máte vyhráno. Pak vás nemusí zajímat boj s integračními testy. I to je důvod, proč Selenium je pro puristy tak děsivé.

Jenže v praxi to tak nefunguje. V praxi často něco čteme a zapisujeme. Můžeme takovou aplikaci otestovat tak, že vrstvu zařizující komunikaci s databází či něčím jiným nějak nahradíme/vyřadíme. Jenže, co jsme tím získali? Nic moc ve skutečnosti. Máme otestované algoritmy a provázanost naší aplikace, ale vůbec nevíme, zda funguje. Mohu otestovat, že moje aplikace správně vygenerovala XML a nikam ho neodeslat. Jenže po nasazení mi mohou klidně poslat neskutečné množství uhlí

OK, špatný příklad. Ale ten příběh mne fakt pobavil. :-) Vezměme jiný příklad: chci otestovat statistiky. Ano, mohu při testování podsunout aplikaci data, která chci, a podívat se, zda se čísla přelila takhle tam, tohle zase tam, a výsledek je takovýhle. Jenže… jenže po nasazení do provozu zjistím, že v SELECTu je překlep v jednom případě. Který sice testuji, ale bez databáze.

A takhle to je se spoustou aplikací. Proto, abychom měli větší jistotu, testujeme s databází. A pokud je to pouze s databází, říkáme tomu stále interně unittest. Pokud je to unittest bez databáze, říkáme tomu pure unittest. Pokud voláme i cizí rozhraní, zapisujeme na disk, …, říkáme tomu integrační test.

Řešili jsme, jak takové testy rozdělit. Původně jsme je měli rozdělené na „unittesty“ a na integrační testy. Ale byl v tom guláš, co kde vlastně testujeme a proč. Někdy bylo prostě vhodnější nechat integrační test, někde zase naopak. Takže testování nějaké funkcionality máme v jednom souboru a rozdíl mezi testy řešíme označením pomocí dekorátoru @pytest.mark.*. Tak hned vidím všechny testy pohromadě.

A jak nám to funguje? Velice dobře. Testy máme rychlé, v průměru 125 ms na test (s tím, že to hodně zvedá především pár integračních testů kvůli účetnictví pod Windowsem). S databází nemáme problémy. Dokonce CI tool (u nás Jenkins) si pro každý běh nejprve vytvoří novou databázi (cca deset vteřin). Tedy máme pod kontrolou, co v databázi je. Najde nám to více chyb. Nespočet chyb bylo v SQLku… Nemluvě o tom, že nemusíme složitě mockovat.

Abych to shrnul: integračních testů se nezbavíme a jsou potřeba. S knihovnou unittest je těžké takové testy udržovat a měli jsme s tím opravdu problémy. Ale díky pytestu a tipům, o kterých jsem mluvil a dříve i psal, už problémy žádné nemáme, ba dokonce je to stejně snadné jako jednotkové testy.

P.S.: Omlouvám se všem puristům za zemřelá koťátka. :-)

Matka moudrosti: jak testovat

Nic z následujícího textu není nové. Určitě to někdo v podobném znění už někde sepsal. Ale pořád se na to naráží a opakování je matka moudrosti. :-)

Extrémní TDD říká, že by se měl napsat minimální test, počkat až selže, poté napsat minimální kód pro průchod testu, ověřit funkčnost, rozšířit test a tak pokračovat do nekonečna. Například:

  1. Napíšu test na zavolání funkce add.
  2. Test selže, protože funkce add neexistuje.
  3. Napíšu funkci add. Jen definici, aby ji šlo zavolat.
  4. Test projde.
  5. Napíšu test, že funkce vrací pro parametry 2 a 3 hodnotu 5.
  6. Test selže, protože funkce nic nevrací.
  7. Napíšu return 5.
  8. Test projde.
  9. Napíšu test, že funkce vrací správné výsledky i pro jiné hodnoty.
  10. Test selže, protože vrací stále samé číslo.
  11. Napíšu algoritmus pro běžná čísla.
  12. Test projde.
  13. Napíšu test na mezní hodnoty.
  14. Test selže.

Kvůli tomu často lidi na TDD zanevřou a buď testují špatně nebo vůbec. Aspoň mi to tak přijde. Přitom, pokud se to nevezme tak extrémně, je programování mnohem lepší. Mám totiž svoji verzi TDD: zůstávám u psaní testu předem, ale neiteruji tolik vášnivě. Testy spíš beru jako takový popis, co vlastně chci dělat. Dokumentaci, dá se říct. Ono pokud žádná dokumentace neexistuje, dá se právě z dobrých testů něco pořádného vyčíst. Můj styl je:

  1. Napíšu všechny testy, co mne na začátku napadnou, pro funkci add.
  2. Napíšu celou implementaci funkce add pod dozorem testů.
  3. Při psaní samotné funkce (či potom) dopíšu další testy, co mne při vývoji napadnou.

Nojo, jenže stále tam je ten test na začátku. Jak mám napsat test, pokud nevím co chci programovat? Otázka, kterou slýchám často. Na to snad jen, pokud nevím, co chci programovat, tak snad raději neprogramovat vůbec. :-)

Na test předem lze nahlížet jako na ukázku předem. Nejprve si tedy udělám kód jak chci asi danou funkci používat. Samozřejmě kód nebude funkční, jde jen o to podívat se, jak skutečně chci funkci využívat. Jakmile zjistím, jak chci funkci využívat, mohu začít psát implementaci. A pokud mám ukázku, upravit ji na test je už drobnost.

Tím si zároveň ušetříte práci. Udělat dobrý návrh na první dobrou vyjde málokdy. Proto je lepší se nejprve zamyslet nad použitím a podle toho udělat API. Jednodušší upravovat ukázku, než udělat celou implementaci a pak zjistit, že se to používá špatně a je třeba celé přepsat.

Dokážu si však představit situaci, kdy nejde napsat test dřív. Například při psaní Selenií. Nebo dělám prototyp čehokoliv a opravdu nemám představu, jaký výsledek bude, a nejde mi o to mít po celou dobu plně funkční kód (který z většiny stejně smažu). Pak je potřeba mít na paměti dvě věci:

  1. Test musím vidět selhat. Pokud napíšu test po kódu, musím kód naschvál rozbít, abych viděl, že test opravdu testuje. Nejde nijak jinak podchytit testy, co nic netestují, než ověřením, že opravdu selhávají. A že jsem už na takové narazil!
  2. Pokud programuji z nějakého důvodu bez testu, napíšu kód nejhloupěji, jak dovedu. Abych při pohledu viděl, že tam není chyba. Poté napíšu testy. Nakonec kód mohu zrefaktorovat a případně i zoptimalizovat. Nejvíce chyb totiž nastává při zhezčování kódu, refaktoringu či optimalizaci. Proto je dobré tuto fázi udělat až s testy.

To jest vše. Kolem testování se dá filosofovat nad více věcmi, ale tohle jsou nejpalčivější otázky, které dostávám a které jsem kdysi sám řešil. Snad jsem vám nějak pomohl. :-)

Testování webových aplikací Seleniem

O tomto tématu jsem už něco napsal. Například jak nainstalovat a začít či nějaké best practices. Dneska chci napsat o tom, jak vlastně testujeme u nás, co jsem už párkrát školil.

Ukázky budou v Pythonu. Knihovna pro komunikaci se Seleniem je WebDriver Wrapper, kterou jsem sepsal pro jednodušší ovládání prohlížeče. Aby se nemuselo myslet na všechny ty detaily. A samotné testy budou ukázány s pytestem, který jsem si v nedávné době oblíbil.

Pohyb po webu

Takový základ na webu je přesun mezi stránkami. Selenium nabízí metodu get. Nevýhoda je, že se musí pokaždé předávat absolutní cesta. Proto jsem přidal motodu go_to, díky které stačí předat relativní cesta. Například:

driver.go_to('http://seznam.cz')
driver.go_to('prihlaseni')

Musí se ale myslet na to, že někde se musí začít s absolutní. Nejvhodnější hned po vytvoření driveru. Aby to bylo ještě jednodušší, lze předávat parametry jako slovník!

driver.go_to('search', {'q': 'Selenium'})

Což přejde na URL http://seznam.cz/search?q=Selenium.

Často padá dotaz, zda lze stránky testovat na status kód. Nejde. Protože to prý k tomuto toolu nepatří. Samozřejmě teoreticky mají pravdu, prakticky se mýlí. Kluci ze Zboží si s tím poradili tak, že nastartovali proxy, přes kterou ženou všechny požadavky, a zkoumají veškeré HTTP kódy a hlavičky tam. Výhoda je jasná, mohou jednoduše monitorovat veškerý provoz, i třeba JavaScriptové volání.

Pokud není potřeba takovýto kanón, přidal jsem do wrapperu metodu download_url. Buď předáte URL, kterou stáhnout, nebo se stáhne aktuální stránka. Vlastně je to samé, jako například volání request.get(url), jen s tím rozdílem, že to za vás pořeší sušenky. Podobně jsem přidal metodu download_file, která je dostupná nad odkazem či formulářem. Například si představte formulář a při jeho odeslání se stáhne požadovaný soubor. Jak ověřit jeho obsah, status kód a hlavičky? Tak, že ze stránky vytáhnete, zda je formulář POST či GET, všechny jeho formulářové políčka, cookie ve stránce a složíte patřičný request. Nebo…

form = driver.get_elm('form_id')
res = form.download_file()
assert res.status_code == 200

Co vše se v odpovědi nachází, viz dokumentace.

Formuláře

Tím se dostáváme k dalšímu důležitému aspektu a to je vyplňování formulářů. S čistým Seleniem je taková akce utrpením. Jelikož naše aplikace je, dalo by se říct, jedním obrovským formulářem, byla to první věc, kterou jsme si zjednodušili. Jak se vyplňuje například formulář v Djangu či WTForms? Předáním slovníku z requestu. Proč to nezkusit tedy i pro testování webovek…

form = driver.get_elm('form_id')
form.fill_out_and_submit({
    'string': u'Nějaký text',
    'number': 42,
    'checkbox': True,
    'radio': 'value',
    'select': 'value',
    'multiselect': ['value1', 'value5'],
    'file': '/tmp/file.txt',
})

Vyhnuli jsme se tedy různým způsobům vyplňováním na pozadí. Klíč je vždy name formulářového políčka a hodnota jak byste ji reprezentovali v Pythonu. O zbytek se postará wrapper za vás.

Například se v poslední době rozmáhá Bootstrap a jiné knihovny sloužící nejen programátorům vytvářet rychle pohledný web. Problém je, že všude je moderní klasický checkbox skryt a přestylován k obrazu svému. Tím poté po volání metody click vyskočí výjimka o skrytém elementu a tudíž na něj „uživatel“ nemůže kliknout. Musí se tedy klikat místo toho na label kolem checkboxu. Jelikož je to rozšířená praktika, wrapper si s tímto popere. Stejně tak u radio tlačítek.

Dále bývá problém při onchange událostech. Takové události se spustí po opuštění políčka. Takže pokud budeme mít navázaný JavaScript na změnu políčka a v testu na tuto změnu budeme čekat… nedočkáme se, pokud explicitně políčko neopustíme. Například stiskem klávesy tabulátor.

from selenium.webdriver.common.keys import Keys

input = driver.get_elm('input_id')
elm.send_keys('text' + Keys.TAB)

Což pro jistotu wrapper dělá automaticky.

Běžně, díky metodě fill_out, není potřeba obsluhovat selecty ručně, ale stejně takové případy občas nastanou. Normálně byste buď obsluhu museli řešit komplikovaně klikáním, nebo si vytvořit instanci Select:

select = driver.get_elm('select_id')
select = selenium.webdriver.support.select.Select(select)
select.deselect_all()

S wrapperem není potřeba. Select je automaticky Select a můžete volat jeho metody.

select = driver.get_elm('select_id')
select.deselect_all()

Práce s elementy

Možná se divíte, co to stále používám za metodu get_elm. Je to zkratka pro metody find_element_by_*. Tedy původně jsem ji napsal kvůli výjimkám. Selenium totiž umí povědět, zda je něco špatně, ale detailů se nedočkáte. Když nám padaly testy na NoSuchElementException, neměli jsme ponětí, co je špatně. A když jsme měli ponětí (z tracebacku bylo jasné, žádná dynamika), nevěděli jsme na jaké stránce.

Jde o to, že chyba NoSuchElement nemusí nutně znamenat chybějící element. Požadovaný element může na své stránce být, ale problém nastal při přechodu na požadovanou stránku. Například odkaz byl špatný. Takové věci se bez patřičných informací blbě ladí a proto jsem si vytvořil vlastní metodu, která přidává potřebné informace.

>>> driver.get_elm('resultCount')
NoSuchElementException: Message: u'No element <* id=resultCount> found at http://www.seznam.cz'

Hned je vidět, že chybí element s ID resultCount na homepage Seznamu. Tam opravdu daný element není a tudíž problém bude někde jinde.

Jakmile jsem vyřešil, aby takové výjimky házely i metody find_element_by_*, přemýšlel jsem o zrušení. Ale lenost vyhrála. Lenost psaní dlouhých testů. Za prvé první parametr je ID, což se používá nejčastěji a tudíž kratší zápis; co je ale důležitější je kombinace s parent_* atributy.

elm = driver.find_element_by_id('id')
elms = elm.find_elements_by_tag_name('a')
# =>
elms = driver.get_elms(parent_id='id', tag_name='a')

Asi to nezní moc jako důvod, proč by to tu mělo zůstávat. Zajímavé to začne být v kombinaci s dalšími metodami. Například možnost nemuset získat element a poté na něj kliknout, ale rovnou kliknout.

elm = driver.get_elm('id')
elm.click()
# =>
driver.click('id')

Nebo při řešení slavné chyby StaleElementException. Přihlašte se, kdo se s ní setkal! Je velice jednoduché na ni narazit, především, pokud máte tu čest s JavaScriptem. Jakmile vám JavaScript pod rukama začně mazat a znovu vytvářet elementy, staré reference přestanou existovat. (Více jsem o tom už psal.)

Při kliknutí na klasický odkaz Selenium počká na načtení stránky. Při kliknutí na cokoliv jiného, co má na starost JavaScript, Selenium nečeká. Neví totiž, zda má čekat. A na co vlastně. Stejně jako uživatel by nevěděl na co má čekat. Takové akce si musíte zařídit explicitně. time.sleep rozhodně nechcete, a všude dávat ručně cyklus taky ne. Selenium nabízí WebDriverWait, já nabízím jednoduší možnost:

elm = driver.wait_for_element('id')

Občas je potřeba opak – počkat, až se něco skryje. Například nějaké nastavení v modálním okně apod.

elm = driver.wait_for_element_hide('id')

Testování

Pravá sranda nastává až se samotným testováním. Jak si vytvořit driver? Jak se případně přihlásit do aplikace? Co testovat? Jak neustále hlídat nečekané chybové hlášky? Či dokonce chybové stránky? …

Samotné Selenium s ničím takovým nepomáhá a proto jsem do wrapperu přidal i podportu pro snadné testování. Pro pytest stačí vytvořit fixturu s názvem _driver, která vytváří samotný driver a specifikuje jeho scope.

Wrapper obsahuje fixturu driver, který se využívá v testech a definuje nějaké chování, o které se nemusíte starat. Například test může kdykoliv otevřít novou záložku, třeba i omylem, a v každém testu byste museli řešit jejich zavření a v dalším testu pokračovat v hlavním okně.

@pytest.yield_fixture(scope='session')
def _driver():
    driver = Chrome()
    yield driver
    driver.quit()

V případě, že potřebujete před/po každé testovací funkcí driver nějak resetovat, můžete postupovat standardně.

@pytest.yield_fixture(scope='session')
def session_driver():
    driver = Chrome()
    yield driver
    driver.quit()


@pytest.fixture(scope='function')
def _driver(session_driver):
    reset_driver(session_driver)  
    return session_driver

Fixtura driver se nestará pouze o aktivní tab, ale také o detekci chybových hlášek a stránek. Automaticky se hledají CSS třídy error a pokud se nalezne, test skončí chybou. Dále se hledá CSS třída error-page pro chybové stránky jako 500, 404, 403, …

Tedy jakákoliv chybová hláška či stránka je špatně. Nemusíte ručně psát testování chybových hlášek a test sám selže.

def test_save(driver):
    driver.fill_out_and_submit({'username': 'michael', 'password': 'abcd'})

# ErrorMessagesException: Unexpected error messages "Password is not correct".

Samozřejmě ale chceme testovat aplikaci i při selhání. Ověřit, že se uživatel dozví potřebné informace. Na to tu existují dekorátory…

@expected_error_messages('Password is not correct')
def test_save(driver):
    driver.fill_out_and_submit({'username': 'michael', 'password': 'abcd'})

# OK.

Test naopak selže, pokud na stránce nalezne jiné (znamená i žádné) chybové hlášky.

Někdy může nastat situace, kdy se může zobrazit chybová hláška, nebo taky nemusí. Ale to nám nevadí. Třeba že některý server není dostupný, ale pro daný test nás tato informace nezajímá. Pak lze použít allowed_error_messages. Či dokonce allowed_any_error_message pokud je nám text chyby úplně jedno.

Podobně jsou dekorátory pro stránky: expected_error_page a allowed_error_pages. Či dokonce pro informační hlášky typu „úspěšně uloženo“: expected_info_messages a allowed_info_messages. Více v dokumentaci.

Aby toho nebylo málo, wrapper kontroluje taky chyby v JS na stránce. Aby tato kontrola mohla fungovat, je potřeba přidat do stránky následující JS kód:

<script>
    window.jsErrors = [];
    window.onerror = function(errorMessage) {
        window.jsErrors[window.jsErrors.length] = errorMessage;
    }
<script>

Teď ale zní otázka – co když mám chybové hlášky definováno jinak?

driver = Chrome()

# Lepsi nastavit na instanci, takze pri zmene prohlizece neni potreba menit tento kod.
driver.__class__.get_error_page = your_method
driver.__class__.get_error_traceback = your_method
driver.__class__.get_error_messages = your_method
driver.__class__.get_info_messages = your_method

Ještě se zmíním o metodě break_point, díky které se kód zastaví a čeká na signál k pokračování (enter v konzoli). Tím můžete snadno test pozastavit a podívat se do Chrome konzole a podobně.

Pro UnitTesty fungují dekorátory stejně. Pouze použijte připravený WebdriverTestCase.

Další drobnosti

To není vše. Ještě je zde několik detailů. Například metoda contains_text, vylepšená switch_to_window a další pro práci s taby, alerty a dalšími detaily. Projděte dokumentaci.


A to je vše. Máte nějaké dotazy či nápady na vylepšení? Sem s nimi!

Co se mi líbí na pytestu

Stručně jsem tu už o pytestu psal v poznámkách z konference PyCON UK. Od té doby jsme v našem týmu pytest nasadili na několik komponent a testuje se nám parádně. Proč?

Asserty

Není potřeba psát více, než je potřeba. Python už assert v sobě má.

def test_eq():
    assert 1 == 2

Spustíme…

$ py.test test.py
=========================== test session starts ===========================
platform linux2 -- Python 2.7.3 -- py-1.4.25 -- pytest-2.6.3
collected 1 items

test.py F

================================= FAILURES ================================
--------------------------------- test_eq ---------------------------------

    def test_eq():
>   	assert 1 == 2
E    assert 1 == 2

test.py:6: AssertionError
========================= 1 failed in 0.01 seconds =======================

Super! Tímto samozřejmě pytest nekončí. Například balík unittest dodává při assertu různé pomocné hlášky. Tady začíná trochu magie. Python sám o sobě nic víc při použití assertu neřekne, proto pytest udělá menší analýzu kódu a vyplivne podrobný rozbor, třeba i s diffem.

def test_eq():
    a = 'aa'
    assert a == 'aab'
    def test_eq():
    	a = 'aa'
>   	assert a == 'aab'
E    assert 'aa' == 'aab'
E      - aa
E      + aab
E      ?   +

Poradí si i s instancemi a vnořenými instrukcemi. Co ale další typy, třeba assertIn, assertIsInstance, …?

    def test_in():
>   	assert 42 in range(10)
E    assert 42 in [0, 1, 2, 3, 4, 5, ...]
E     +  where [0, 1, 2, 3, 4, 5, ...] = range(10)
    def test_isinstance():
>   	assert isinstance(datetime.date.today(), datetime.datetime)
E    assert isinstance(datetime.date(2015, 2, 14), )
E     +  where datetime.date(2015, 2, 14) = ()
E     +    where  = .today
E     +      where  = datetime.date
E     +  and    = datetime.datetime

Tady je vidět, že občas pytest trochu spamuje. V tomto případě je nám jedno, jakou konkrétní hodnotu datum obsahuje a pokud by se hodilo, stačí vědět datum, nikoliv kde všude v paměti se vše nachází. Ono je to ale takhle lepší, jak zjistíme později. Hlavně pokud se to děje samo s kódem, který musíme tak jako tak napsat, proč ne?

Jak jsou na tom očekávané výjimky?

def test_zerodivision():
    with pytest.raises(ZeroDivisionError) as excinfo:
        1/0
    assert excinfo.value.args[0] == 'integer division or modulo by zero'

Co třeba almostEqual?

Tato metoda a jí podobné chybí. Ale nikdo nezakazuje použití jiných balíků, třeba nose.

from nose.tools import assert_almost_equals

def test():
    assert_almost_equals(1.234, 1.2345, places=4)

Pytest s obyčejným assertem dokáže vše, co je potřeba, aniž byste přišli o jakoukoliv informaci. Dokonce vám dovolí rozšířit assert o vaše informace. Takže při volání assert s vašimi instancemi si můžete přidat potřebná data. Více v dokumentaci.

class Foo:
    def __init__(self, val):
        self.val = val

def pytest_assertrepr_compare(op, left, right):
    if isinstance(left, Foo) and isinstance(right, Foo) and op == "==":
        return [
            'Comparing Foo instances:',
            '   vals: %s != %s' % (left.val, right.val)
        ]

Výstup pak vypadá například takto:

    def test_compare():
        f1 = Foo(1)
        f2 = Foo(2)
>       assert f1 == f2
E       assert Comparing Foo instances:
E            vals: 1 != 2

Fixtury

Test se však neskládá jen z testu. Pro test je potřeba, především pro integrační test, předpřipravit data, konexe a další závislosti. Pytest tuto nutnost řeší od začátku bravurním způsobem. Pravděpodobně největší páka, proč jsem vynaložil energii unittest u nás vyměnit právě za pytest.

Klasické unittesty mají metody setUp a tearDown, kterými lze předpřipravit prostředí pro testy, či po sobě uklidit. Problém začne, jakmile taková příprava nění jednoduchá. Například pokud chcete otestovat založení objednávky v e-shopu. K tomu potřebujete minimálně testovací produkt a testovacího uživatele. Takový produkt může být nějakého typu, který potřebuje nakonfigurovat. U uživatele zase potřebujete vyplněné fakturační údaje; ověřit, že se vygeneruje dobře faktura. Poměrně hodně věcí pro jeden setUp.

Pytest na to jde jinak. „Magickým“ dependency injection.

def test_order(user, product):
    # test ordering of product by user

Kde pytest získá uživatele a produkt?

Vy mu je nadefinujete.

@pytest.fixture
def user():
    return create_some_user_here()

@pytest.fixture
def product():
    return create_some_product_here()

Dobře. Ale pro tyto fixtury je potřeba databáze…

@pytest.fixture(scope='function')
def db():
    return connect_to_database()

@pytest.fixture
def user(db):
    return create_some_user_here(db)

OK. Chtěl bych, aby test končil rollbackem a nic po sobě nenechal.

@pytest.yield_fixture(scope='function')
def db():
    db = connect_to_database()
    db.begin()
    yield db
    db.rollback()
    db.close()

Za pozornost stojí parametr scope. Tímto parametrem se určuje, kde všude se má fixture cachovat. Pokud řeknu „function“, říkám tím, aby ve všech fixturách, které potřebuje jeden test, byla stejná instance databáze. Tím mohu pracovat v jedné transakci a rollbacknout. Pokud bych neurčil scope, každá fixture by měla jiné spojení s databází a nešlo by využít transakcí.

Tohle je na fixturách moc hezké. Mohou záviset sami na sobě a příprava i čistka jsou na jednom místě vedle sebe. Plus fixtury mohou být sdílené přes všechny testy a vytvářejí se pouze, pokud jsou opravdu potřeba daným testem. Osobně mi to přijde skvělé a mnohem přehlednější.

Zní dobře. Je možné se připojit k databázi jednou a pro každý test jen začít transakci?

@pytest.yield_fixture(scope='session')
def db_connection():
    db = connect_to_database()
    yield db
    db.close()

@pytest.yield_fixture(scope='function')
def db(db_connection):
    db_connection.begin()
    yield db_connection
    db_connection.rollback()

Fixtury lze skládat jak je libo. Jediné pravidlo je, že nelze, logicky, z většího scope záviset na menším. Tedy například ve fixture db_connection nelze záviset například na fixture se scope „function“. Protože by se pak tato fixture musela v db_connection nacachovat pro všechny testy, což by mohlo mít nežádoucí následky.

Mimochodem v reportu testu uvidíte, jaká instance fixtury do testu vstupovala. To je proč se hodí vidět i ty adresy instancí v paměti. Lze pak jednoduše zkontroloval, zda chybu nezpůsobuje špatná konfigurace fixtur.

Parametry

Super. Chtěl bych vyzkoušet otestování s dvěma typy uživatelů – zaregistrovaný a jednorázový nákup.

@pytest.fixture(params=['registred', 'unregistred'])
def user(request, db):
    return create_some_user_here(db, request.param)

Takhle málo stačí a všechny testy, kde se využívá fixture user, se provedou dvakrát. V reportu testu je to opět hezky vidět.

================================= FAILURES ================================
-------------------------- test_order[registred] --------------------------

user = 'registred'

    def test_order(user):
>       assert 0
E       assert 0

t.py:11: AssertionError
------------------------- test_order[unregistred] -------------------------

user = 'unregistred'

    def test_order(user):
>       assert 0
E       assert 0

t.py:11: AssertionError
========================= 2 failed in 0.01 seconds =======================

Skvělé! Bude to fungovat i když bych chtěl testovat více produktů?

No jasně! Pak se test provede pro každou kombinaci. Samozřejmě lze parametrizovat i fixtury využité jinými fixtury. V našem případě například db_connection může vracet pokaždé jinou konexi – jednou na MySQL, podruhé třeba na PostgreSQL.

Dříve testovat všechny kombinace bylo peklo. S pytestem napíšete jeden test, jednu fixture a nastavíte parametry.

V některém testu bych chtěl mít ale nějaká specifická data. Třeba v jednom testu bych chtěl, aby se mi vytvořil produkt s určitou cenou, kdežto pro ostatní případy stačí cokoliv. V unittestu lze tohle snadno, ale tady se předává jen název fixtury.

I tohle lze zařídit a běžně využíváme. Dělá se to pomocí speciálního parametru request, už jsme ho viděli výše při předávání parametrů. Je to vlastně podobná technika.

@pytest.fixture
def user(request):
    return create_some_user_here(request.function.user_name)


def test(user):
    # ...
test.user_name = 'Some name'

V requestu naleznete také atribut module či cls. Ve skutečnosti je tam toho ještě víc, mrkněte do dokumentace.

Abychom si tohle usnadnili, udělali jsme si hezký dekorátor a pomocnou funkci na mergování těchto dat. Pak vytvoření záznamů do databáze máme jednou pro všechny testy, i když každý test potřebuje trochu jiná data.

def use_data(**data):
    def wrapper(func):
        for key, value in data.items():
            setattr(func, key, value)
        return func
    return wrapper

def get_data(request, attribute_name, default_data={}):
    data = default_data.copy()
    data.update(getattr(request.module, attribute_name, {}))
    data.update(getattr(request.cls, attribute_name, {}))
    data.update(getattr(request.function, attribute_name, {}))
    return data

Poté je použití velice jednoduché a můžete definovat na modulu i třídě najednou. Data se zmergují do sebe logicky, jak by se očekávalo (modul má nejnižší prioritu a funkce přebíjí vše).

@pytest.fixture
def user(request):
    data = get_data(request, 'user_data')
    return create_some_user_here(data)

user_data = {'username': 'test', 'name': 'name'}

class UserTest:
    user_data = {'email': 'test@example.com'}

    @use_data(user_data={'name': 'Some name'})
    def test(user):
	# user.username = 'test'
	# user.email = 'test@example.com'
	# user.name = 'Some name'

Naše pomocná metoda je trochu chytřejší a dokáže pracovat i se seznamem, takže snadno můžeme říct, že uživatelů chceme pro test víc. Ještě ale ladíme detaily. Jakmile vyladíme, velká šance, že na PyPI přibude balíček. Nebo pull request do samotného pytestu. :-)

EDIT: Balíček už existuje a je k nalezení pod názvem pytest-data.

Označování testů

Půjdeme dál. Je velice vhodné mít tzv. smoke testy. Pár testů, které musí vždy projít a je jistota, že základní věci budou určitě fungovat. S pytestem velice snadno.

@pytest.mark.smoketest
def test():
    pass

Hotovo. Nyní stačí spouštět buď všechny jako doposud, nebo s parametrem -m: py.test -m smoketest. Toto označení v pytestu není, takto jste si vlastně vytvořili vlastní označení testů a můžete si je označit jak chcete.

Co překlepy?

Aby nedocházelo k chybám, lze si marky zaregistrovat, případně nikdo nebrání si vytvořit mark a importovat si ho. Lze označit také pouze jeden parametr v parametrizovaném testu.

U nás jsme si to obohatili o vlastní command line option. Udělali jsme pro jistotu opak – místo označování důležitých testů označujeme nedůležité. Tak nezapomene na nějaký důležitý test. Díky tomuto spuštění testů je raz dva a integrační tool se pak postará o kompletní funkčnost. Mimochodem dříve jsme měli jednotkové a integrační testy oddělené – nyní je máme vedle sebe a integrační je označen jako slow.

slow = pytest.mark.slow

def pytest_addoption(parser):
    parser.addoption('--skip-slow-tests', action='store_true', help='skip slow tests')

def pytest_runtest_setup(item):
    if 'slow' in item.keywords and item.config.getoption('--skip-slow-tests'):
        pytest.skip('Skipping slow test (used param --skip-slow-tests)')

Jaké testy jsou ale pomalé? Easy peasy. Stačí přidat argument --duration. Pokud zavolám například py.test --duration 10, na konci testu pytest vyreportuje 10 nejpomalejších míst. Záměrně neříkám testů, ale míst, protože report je rozdělen na část setup, call a teardown. Což se hodí; například při testování se Seleniem je inicializace prohlížeče náročné a vidím aspoň tak, kolik reálně test dlouho trval. První test v řadě tak není hendikepován.

Zašli jsme ještě dál a začali jsme označovat integrační testy, které využívají cizí službu. Pracujeme na obchodní aplikaci, která komunikuje téměř se všemi službami Seznamu. To už je velká šance, že některé rozhraní v testu na chvíli spadne. Pokud taková situace nastane, popadá nám několik integračních testů, což akorát v reportu překáží.

Dnes si testy označíme a pokud potřebné rozhraní není dostupné, test se přeskočí. Stále vidíme, pokud se tak stalo, ale nyní se nemusíme prokousávat například sto spadlými testy. A stačí tak málo…

def pytest_runtest_setup(item):
    marker = item.get_market('uses')
    if marker:
        if not any(is_not_available(arg) for arg in marker.args):
            pytest.skip('Skipping test requiring {}'.format(marker.args))

@pytest.mark.uses('penezenka', 'sklik')
def test():
    # ...

Pluginy

Ještě nekončíme. Unittesty či nose mají argument fast fail indikující, aby běh testů spadl po první chybě. Je tu super věc na ladění. Pytest má ještě lepší: last fail.

Last fail je ve skutečnosti plugin. Instalace je však velice snadná a přidá do command line další argument --lf. A cože to dělá? Spustí jen ty testy, které selhaly v posledním běhu.

Modelová situace: spustíte všechny testy. Trvají 10 minut, spadne jich 20. Provedete opravu, ale nechcete čekat celých deset minut. Nevadí, přidáte argument --lf a spustí se pouze těch 20 rozbitých. Po dokončení většinu opravíte, ale ještě 6 rozbitých testů zbývá. Provedete opravu a zase spustíte s argumentem --lf. Spustí se pouze těch posledních šest. Jakmile se dostanete na nulu, spustíte opět všechny a ověříte, zda jste nerozbili něco dalšího. Mezitím ale můžete na kafe, protože velice pravděpodobně máte hotovo.

Šikovných pluginů je celá řada. Například si můžete udělat testy na PEP8. Existují pluginy pro Flask, Django, … Taky instafail na okamžitý report. Podívejte se do dokumentace a internetu.

Propojení s unittesty

Celé to je děsně super. Ale už mám v mém projektu unittesty…

Nevadí. Pouze zahoďte unittestový či nosový runner, nakonfigurujte si pytest dle potřeb a můžete začít psát testy v novém stylu. U nás máme nakombinované Seleniové testy a žádná velká dřina to nebyla. Pytest si s unittesty poradí, dokonce lze do unittestů dostat fixture. Takto jsem tam dostal Seleniový driver:

class BaseTestCase(unittest.TestCase):
    @pytest.fixutre(autouse=True)
    def _set_driver(self, driver):
        self.driver = driver

Parametr autouse u fixture je taky dobrá věc. Využili jsme například taky při vytváření virtuálního displeje pro Selenium. Není pak potřeba někde explicitně psát, aby se taková fixture všude použila.

OK. Ale pročti si dokumentaci. Tímhle to nekončí!

WebDriver: Best practices

Není to poprvé, co píšu o Selenium aka WebDriver. Dnes bych však chtěl mluvit více o detailech technologie. Popravdě chci spíše mluvit o tipech místo popisování WebDriveru krok za krokem.

Dostala se mi totiž do ruky kniha Selenium WebDriver Practical Guide, což je průvodce přes celé API Selenia a mnohem víc. Začne od Selenia první verze a rozdíly mezi právě první a druhou verzí. Tím také vysvětlí, proč je lepší sáhnout po druhé verzi a není zapomenuto pár poznámek o přechodu. Což je podle mne důležité. Je to takový základ k pochopení celé technologie.

Dále kniha pokračuje popisováním API od základů k pokročilým tématům. Sice není popsané vše a občas mi přijde, že je bráno více ohledu na méně důležité části, ale i tak je popis dobrý. Místy to může vypadat jako více ukecaná dokumentace, věřím však, že to začátečníkovi pomůže se rychleji zorientovat.

Někoho může vyděsit, že je důležité znát Javu. Všechny ukázky jsou v knize ukazovány v Javě. Sice mi absence jiných jazyků a odlišností v implementaci chybí, naštěstí jsou ukázky minimalistické bez JUnit či jiných testovacích nástrojů (jednoduše jsou ukázky psané jako jednorázové scripty) a tak se lze v ukázkách lehce zorientovat. I když si s Javou nerozumíte třeba jako já.

Jelikož testování pomocí Selenia se nemusí dělat ve stejném jazyce, jako samotná webová aplikace, je zvolení jazyka k výuce docela škoda. Java je totiž docela psavá a testy, kór Seleniové, moc programátory psát nebaví a ukecanost tomu rozhodně nepomůže. Školil jsem Selenium JavaScriptáře a například s Pythonem neměli žádné problémy (jaké by také mohli mít, když se to tolik podobá CoffeeScriptu). Nedovedu si však představit, kdybych je školil v Javě. Byli by chudáci vyděšení…

Po seznámení s API následuje poslední kapitola seznamující s patternem Page Objects. Pattern, díky kterému lze psát lepší a lépe udržovatelné testy. Z knihy mám však takový pocit, jakoby se tak Seleniové testy měli psát vždy. Nejsem toho názoru. Page Objects je skvělý způsob, jak si udělat mezivrstvu mezi testem a stránkou a tak snížit problémy při změně šablony na minimum. Určitě vhodné pro SPA, nemohu doporučit pro přesně opačný typ webových aplikací (maximálně na nějaké části).

Kniha celkově není vysloveně špatná, ale chybí mi v ní několik dalších praktických rad. Popíšu podle mne ty nejdůležitější.

Začnu u konfigurace – Selenium Grid umožňující pouštět testy přes několik serverů a prohlížečů pravděpodobně nepotřebujete. Začněte si stavět grid pouze, pokud potřebujete otestovat funkčnost na více prohlížečích, nebo až v době, kdy server přestane stíhat. Ušetříte si čas na začátku a zbytečně se do toho nezamotáte. Přece jen rozběhat Windows, Linux, Mac a mobilní platformy a pro každý systém různé prohlížeče není sranda.

Pokud se rozhodnete jen pro jeden prohlížeč, protože vám stačí ověřit funkčnost a nikoliv kompatibilita napříč prohlížeči, doporučuji použít Chrome. Měl jsem s ním nejméně problému a je nejrychlejší (i když instalace je o něco složitější). Tedy minimálně pod Linuxem, nevím jaká je situace jinde.

V knize se vůbec nemluví o běhu na serverech bez grafického prostředí. Samozřejmě něco takového je možné a už jsem o tom psal.

Nyní se přesunu k samotnému psaní testu, konkrétně k výjimce StaleElementException. Tato výjimka, co vím, strašila spoustu lidí při seznamování se Seleniem a překvapilo mě, že se o ni nepíše. Dokonce je v knize ukázka, kde se něco takového může stát a nijak na možný problém neupozorňuje.

Tedy o co jde – představte si hledací políčko a našeptávač. Implementaci našeptávače mějme takovou, že po zadání znaků se v určitou chvíli odešlou tyto znaky na server, server odpoví se seznamem k našeptání a ty JS zobrazí tak, že starý seznam smaže a nahradí novým. Teď se podívejme na následující test:

def test_autocomplete(self):
    search = self.driver.find_element_by_id('search')
    search.send_keys('sele')
    first_autocomplete = self.driver.find_element_by_tag('li')
    self.assertEqual(first_autocomplete.text, 'select')
    search.send_keys('nium')
    self.assertEqual(first_autocomplete.text, 'selenium')

U posledního řádku dostaneme právě výše zmíněnou výjimku. Proč? Protože při zaslání dalších znaků JavaScript původní element smazal a vytvořil nový. Sice takový element na stránce stále existuje, ale už to není ten samý, který jsme získali na čtvrtém řádku. Na každou proměnnou s elementem lze nahlížet jako na referenci do DOM stromu. Pokud vaše testovaná webová aplikace má k statické stránce hodně daleko, je dobrým zvykem si před prací s elementem vždy vytáhnout čerstvou referenci.

Na to bych navázal s událostí onchange. Pokud bychom výše uvedený našeptávač řešili (i když to je trochu blbost) pomocí této události, test nebude vůbec fungovat. Proč? Protože metoda send_keys po odeslání posledního znaku nezařídí ztrátu focusu a tedy se událost onchange nevykoná. Aby test fungoval s touto změnou, muselo by se ještě posílat explicitně například znak zabulátor:

search.send_keys('sele' + Keys.TAB)

Což tedy osobně řeším automaticky pomocí mého wrapperu WebDriverWrapper, díky kterému se vůbec formuláře vyplňují mnohem snadněji. Jedná se však pouze o Python.

WebDriverWrapper neřeší pouze formuláře, řeší toho mnohem víc. V naší aplikaci máme například prokliky do cizích systémů či nějaké popup okna. Stalo se nám, že se jeden test neočekávaně proklikl pryč a pak kvůli tomu selhali ostatní testy. Vyřešeno tak, že na začátku testu se zaručí běh v hlavním okně, aniž by se to muselo v každém testu dělat ručně.

Ale to už se dostávám k detailům, které zde nechci řešit. Cílem bylo zhodnotit knihu Selenium WebDriver Practical Guide pro Packt Publishing a říct důležité věci, které mi v knize chyběly. Což jsem udělal a tak jste snad zase o něco chytřejší. :-)

Tipy na testování webových aplikací

Testováním webových aplikací se zabývám už poměrně dlouho a přijde mi, že integrační testy v podobě Selenia jsou mnohem důležitější, než jednotkové testy JavaScriptu nebo backendu. V různých knihách a článcích se píše o tom, že pokud není praxe, ať se testuje dle toho, co danému programátorovi přijde vhodné. Tím se získá praxe a najde se ten cit pro rozhodování jaké napsat testy přednostně a čím se nemá smysl zabývat.

Také jsem tak začínal a cit si vybudoval. I když, stále se mám co učit. Dnes už ale vím, že to není zrovna nejlepší poučka. Minimálně co se týče webových aplikací. Tam už vím, že nejlepší je sáhnout prvně po Seleniu. Důved je velmi prostý – jednotkové testy, jak JavaScriptu, tak backendu (ať už je to jakýkoliv jazyk), otestují jen nějakou tu jednotku. Řekne to programátorovi „hele, tenhle kus kódu je správný. Jestli ale náhodou šablona očekává něco jiného a nezobrazí tlačítko, na kterém to je závislé… to už nevím. Ověř si sám.“ nebo tak nějak.

Tohle však testy bohužel neřeknou. Odpoví jen stručné „OK“. Jakmile tuto odpověď uvidí programátor, jde radostně nasadit na betu, oznámí uživatelům že mohou testovat a ejhle. Ona tam je chyba v šabloně (nebo v čemkoliv jiném, co už jednotkové testy nepodchytí) a programátor musí po víkendu zjišťovat, cože to v pátek dělal a cože to musí opravit.

Neříkám však, že ostatní testy nejsou dobré. Taky mi odhalili spoustu chyb, Selenium však jednoznačně vede.

To je tedy tip číslo jedna – u webovek se zaměřit více na Selenium.

V naší aplikaci v práci mi hodně vadilo, že tam máme spoustu chyb v JavaScriptu. Webmasteři se u nás ještě do nedávna střídali jeden za druhým, každý si to dělal po svém a nikoho nezajímalo, co tam je a co tam bude. Dnešní situace je už dobrá a neustále se zlepšuje, bylo mi to ale málo a napadlo mě do našich Selenium testů přidat kontrolu JavaScriptových chyb. Do stránky jsem přidal kus JS:

<script type="text/javascript">
window.jsErrors = [];
window. function(errorMessage) {
  window.jsErrors[window.jsErrors.length] = errorMessage;
}
</script>

a v Selenium (po každém testu, v tearDown) přečetl obsah proměnné jsErrors. Spustil to a… nalézalo to jednu chybu za druhou. Tady className neexistuje, tajhle null nemá atribut getElementsByTagName apod. Vlastně ještě dnes to nalézá takové chyby díky tipu číslo tři, o tom ale až za chvíli.

Úprava mě stála asi hodinku se vším všudy a dostal jsem mocnou zbraň proti bugům v JavaScriptu. Stačí aby se na něco opomnělo a testy zařvou. Druhý tip je myslím jasný – přidat do Selenia podporu na hledání JavaScriptových chyb.

Je tedy pravda, že takhle selžou testy, které ve skutečnosti prošly. „Jen“ nastala nějaká drobnost v JavaScriptu. Já na to odpovím: nezájem. Je tam chyba? Je. Opravit.

Říkal jsem, že nám tu testy stále oznamují různé chyby v JavaScriptu díky třetímu tipu. Tento tip zní: fuzzy testování.

Zmíněná aplikace je velmi obrovská a testy se začaly psát teprve někdy před rokem a půl. Stále máme spoustu nepokrytého místa, které se zpětně těžce vyplňuje (nelze se vybodnout na všechny známé bugy a feature requesty, na měsíc se zavřít ve sklepě a dopsat všechny testy). To mě pochopitelně trápilo a vyřešil jsem tento problém alespoň fuzzy testováním. Během chvilky jsem napsal primitivní algoritmus na náhodné proklíkání webové aplikace.

Neočekával jsem, že to něco pořádně najde, ale světe div se… ono to našlo hned chybu na dvacátý náhodný klik. Klik, který by neudělal žádný programátor (protože ví, kam klikat nemá), ale ani tester či uživatel. Tedy, věřím, že to někdo už udělal, jen se nenamáhal s reportem. Fuzzy testování jsem zatím nechal v primitivní podobě jak jsem to poprvé napsal a stále to nachází různé fatální chyby, které měly skončit jen chybovou hláškou. Někdo by mohl konstatovat, že to je drobnost, nic kritického. Uživatel má však opačný názor.

Minimálně v našem případě to hodně pomáhá s najitím chyb v JavaScriptu různě v aplikaci, kam se Selenium testy sami od sebe jen tak nepodívají.

Zbývá poslední tip. Selenium je takové neohrabané. Hodně mi vadil styl použití. Sice používám webdriver (Selenium 2), což je lepší, než předchozí verze, ale stále to není ono. Takže jsem napsal wrapper, který práci se Seleniem ulehčí. Kód jsem vystavil volně na GitHubu a ještě mám nějaké plány na zlepšení než to převedu do stable verze. Každopádně už nyní to používají dvě aplikace. Více zde: https://github.com/horejsek/python-webdriverwrapper. (Jedná se o Python.)

Pokud bych dnes začínal nový projekt, s těmito testy bych začal.

Chyby jsou povinnost

Zjistil jsem, že každý správný programátor by měl dělat chyby. Možná to může vyznít bláznivě, ale mám pro to své důvody, kterých je hned několik:

Asi jako nejdůležitější důvod je ten, abychom si udrželi práci. Představte si situaci, kde po vás zákazník chce nějakou aplikaci, kterou mu dodáte před termínem ve vynikajícím stavu (tzn. kde nejsou žádné chyby a odpovídá představám zákazníka). Myslíte, že vás bude zákazník po předání ještě potřebovat nebo že dostanete speciální odměnu? Ale kdepak… jen když v aplikaci budou chyby a nebude dle očekávání, máte možnost ze zákazníka dostat další prácí za feature, které nebyly v zadání.

S tím úzce souvisí další důvod – musíme si naši práci udržet, poněvadž přeci nic jiného dělat neumíme. Nebo dokážete si představit (pokud jste pravý noční tvor nazývající se developer) jak někde něco děláte rukama? Třeba zametáte chodníky? Přece takový člověk by si o koště přerail obě ruce.

Další neméně důležitý důvod je vzdělání. Přeci chybami se člověk učí. Nikdy se nenaučíte nic tak dobře jako tím, že něco zkusíte a ono vám to dá ránu, která vás naučí, že tohle se dělat nesmí. Třeba strkat vidličku do zásuvky. A čím víc chyb uděláme, tím víc špatných možností vyloučíme. Pak jednoho dne, až toho hodně vyzkoušíme, budeme vědět, která řešení jsou ta správná.

No a takový poslední důvod je abychom si připadali jako lidé a nikoliv jako stroje. Protože chybovat je lidské.

Tak, a teď vážně…

Zdůrazňuji, že potuď to byl žert!

Tento článek je o testování, konkrétně o tom, co vlastně testovat. Postupů pro to je několik a záleží, v jaké situaci právě aplikace je. Začnu od ideálního případu, kdy se teprve aplikace začíná vyvíjet.

SLIME

Zatím jsem neprozkoumal mnoho postupů, jak pokrývat aplikaci testy, ale osobně se mi prozatím nejvíce zamlouvá SLIME (by Adam Goucher), což je zkratka ze security, languages, requirements, measurement a existing. 

Tedy nejprve otestovat bezpečnostní prvky. Například otestovat, že aplikace se ubrání proti různým útokům jako je napříkald XSS, CSRF, SQL injection, Clickjacking, zda se posílají hlavičky zvyšující bezpečnost a spoustu dalšího. Protože pokud aplikace bude mít problém s bezpečností, je většinou už jedno, že jinými chybami netrpí.

Další v pořadí je nutné otestovat, zda si aplikace poradí na všech místech s kódováním. Bylo by velmi nepříjemné narazit na skvělou aplikaci, kterou nemohu používat, protože místo písmenek vidím čtverečky kvůli špatnému kódování.

Dobrou technikou může být otestovat některé vlastnosti knihoven, na které je aplikace velmi závislá a není jisté, že autoři té knihovny nedostanou amok, kdy celé API změní bez předchozího upozornění. Poté může být velmi těžké zjišťovat, co se vlastně změnilo a co se musí upravit. Právě takovéhle testy by s tím měly pomoci.

Dále se hodí měřit výkon aplikace. Je to velmi podobné jako s jazyky – může se jednat o skvělou aplikaci, ale když budu čekat na odezvu příliš dlouho, velmi rychle mě to přestane bavit. Proto je dobré si napsat testy a nechat se upozornit, jakmile se výkon aplikace zhoršuje.

A nakonec testovat samotný kód, který vzniká. Jednotkové testy, integrační testy a tak dále.

80 % ve 20 %

Většinou se však snažíme testovat již existující aplikaci a tam už testovat předchozí body není úplně nutné, protože aplikace si prošla nějakým vývojem, kde se všechny popisované neduhy už (doufejme) opravily a testy by nepřinesly mnoho ovoce. Což ale neznamená, že to není užitečné – pokud se plánuje měnit jádro aplikace, určitě je užitečné si nejprve výše zmíněné testy napsat.

Popojďme ale dál k samotnému kódu aplikace. Určitě nejlepší strategie je začít těmi nejdůležitějšími a nejvíce používanými částmi aplikace. Existuje takové pravidlo 80/20, což znamená že 80 % všech chyb je 20 % kódu. Většinou toho nejvíce užívaného. Takže relativně během chvilky jsme schopni pokrýt spoustu věcí.

Mindmaps

Když už si myslíme, že jsme pokryli to nejdůležitější, chceme pokračovat dál a aplikace je tak rozsáhlá, že není lehké zjistit, co už pokryto je a co není, mohou pomoci mindmap grafy. Jedná se o diagram, ve kterém si postupně vyznačíme všechny funkčnosti aplikace.

Řekněme, že naše aplikace je e-shop. Začneme prvotní tečkou někde uprostřed a z té uděláme větve „uživatel“, „produkty“, „košík“, … Z košíku udvětvíme dál na „přidat do košíku“ a to můžeme zase odvětvit na „dynamické přidání pomocí JS“ a „klasické přidání“ atd. Pak zbývá jen fajfkovat, co už otestováno je a co nikoliv.

Nutno podotknout, že existují i utility, které prozkoumají pokrytí testů. Nemám s nimi ale dobrou zkušenost – čísla nejsou přesná a lze to využít jen pro jednotkové testy.

Dělat chyby

No a když už aplikace obsahuje spoustu testů a jejich psaní nás stále neomrzelo, zbývá poslední trik o kterém je tento článek – dělat chyby.

A dělat je záměrně. Nikoliv však kvůli důvodům ze začátku, ale kvůli otestování testů. Při úspěšném rozbití (jinak skvěle fungující, samozřejmě) aplikace lze jednoduše zjistit, kde mají testy mezery a je potřeba některé upravit či další připsat.

Tato poslední metoda neplatí jen pro případ, kdy už se neví, jaké testy napsat. Dobré je při psaní testu ověřit, že test funguje. Pokud se praktikuje TDD, tak se to vlastně dělá; ale pokud se testy dopisují k existujícímu kódu, je potřeba funkčnost testu ověřit dodatečně. Test, který je neustále zelený, je k ničemu…

Tak dělejme chyby!

Selenium + Python + Debian server

Už mi nestačí hrabat se pouze v jednotkových či integračních testech a tak si nějak dobu hraju se Selenium. S nástrojem, který dokáže z velké části nahradit i „testera klikače“. Před pár dny jsem měl potřebu si Selenium rozběhat na serveru s tím, že absolutně netuším, jestli to lze a jestli to lze udělat nějak snadno. (Mimochodem serverem mám na mysli mašinu někde bůh ví kde bez jakéhokoliv vstupně/výstupního zařízení.)

Pokud nebudu počítat občas trochu zmatené lidi v diskuzích kolem tohoto problému (na otázku jak nainstalovat Firefox na Debian serveru bez Xek jsem našel nespočet odpovědí „ty blázne, to nejde. Proč to vůbec potřebuješ, vždyť to je blbost!“), tak to udělat lze a velmi jednoduše. 

Jako první si musíme uvědomit, cože to vlastně požadujeme. Chceme použít Selenium 2.0 (Webdriver) v Pythonu na otestování webové aplikace, která nám běží na Debian serveru bez X serveru. Začněme tedy postupně řešit.

Nejprve je potřeba Python, nainstalujeme.

# apt-get install python

Super, máme. Dále potřebujeme Selenium pro Python. Selenium lze nejjednodušejí nainstalovat přes PyPI (Python Package Index), na to potřebujeme nainstalovat balík python-pip.

# apt-get install python-pip
# pip install selenium

Máme nutný základ pro jakékoliv testování pomocí Pythonu a Selenia. Dál je potřeba nějak vyřešit problém „žádný X server“. Řešeních je víc, já si však vybral XVFB. Celým jménem virtual framebuffer X server. Česky se jedná o virtuální X server. Tedy možnost spouštět grafické programy bez nutnosti fyzické podpory pro grafický výstup. Jen nic není vidět. :)

Budeme potřebovat nejen balík xvfb, ale také knihovnu pyvirtualdisplay pro Python, abychom mohli ovládat xvfb přímo z našeho testovacího kódu.

# apt-get install xvfb
# pip install pyvirtualdisplay

Nyní nám zbývá předposlední část a to nainstalovat Firefox. Ve standardních Debian repozitářích k nalezení není a tak se musí nejprve přidat další zdroj na linuxmint.

# vim /etc/apt/sources.list
// pridat radek "deb http://packages.linuxmint.com debian import"
# apt-get update
# apt-get install firefox

Tím je připraveno prostředí a můžeme konečně spustit první test funkčnosti.

from selenium import webdriver
from pyvirtualdisplay import Display

display = Display(visible=0, size=(800, 600))
display.start()

browser = webdriver.Firefox()
browser.get('http://www.google.com')
print browser.title
browser.quit()

display.stop()

Pokud vše proběhlo hladce, zbývá si už jen ukázku přepsat do Pythoních unittestů a s radostí začít testovat. :)

class SomeTest(unittest.TestCase):
    @classmethod
    def setUpClass(cls):
        cls.display = Display(visible=0, size=(800, 600))
        cls.display.start()
    
    def setUp(self):
        self.browser = webdriver.Firefox()
        self.browser.get('http://www.google.com')
    
    def testGoogleTitle(self):
        self.assertEqual(self.browser.title, 'Google')
    
    def tearDown(self):
        self.browser.quit()
    
    @classmethod
    def tearDownClass(cls):
        cls.display.stop()

if __name__ == '__main__':
    unittest.main(verbosity=2)

Výstup pak už je klasický, který známe:

testGoogleTitle (__main__.SomeTest) ... ok

----------------------------------------------------------------------
Ran 1 test in 4.226s

OK

Dále je dobré si udělat nějaký vlastní wrapper pro TestCase, který se postará o rutiny. (Stále pamatujeme na DRY.) Případně ještě nevytvářet instanci Firefoxu pro každý jednotlivý test, ale pro celou třídu nebo pro všechny testy. Záleží na potřebách a preferencích.

Tento způsob se mi velmi líbí i pro desktop, protože (díky použití Webdriveru) nemusím pouštět žádný Selenium server a ani mi na monitoru nevyskakuje Firefox. Vše hezky v pozadí. A když se něco nedaří a potřebuju vidět, co se děje, stačí pouze dočasně odstranit vytváření virtuálního X serveru.

Proč jsem zvolil Webdriver?

Protože to je součást Selenia 2.0 (největší novinkou druhé verze je právě integrace Webdriveru). Tedy je to směr, kterým se tento nástroj ubírá.

Protože má objektovější API. Sice to v mých velmi jednoduchých ukázkách není dobře vidět, ale náznak tam je. Pak lze kusy kódu lépe zapouzdřit. Například vytvořit celou třídu reprezentující nějaký základní stavební kámen na webu a není třeba jeho funkčnost mít všude možně (změna v HTML se poté nerovná změně ve všech testech, ale jen někde na jednom místě).

Protože Selenium ovládá prohlížeč skrze JavaScript, kdežto Webdriver ovládá přímo prohlížeč. Na Webdriveru se podílejí všichni hlavní tvůrci prohlížečů. Díky tomu také není potřeba spouštět Selenium server.