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 UnitTest
y 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!