Přejít k navigační liště

Zdroják » Různé » Zombie, fantóm, bezhlavý rytíř… aneb Automatické zpracování webu

Zombie, fantóm, bezhlavý rytíř… aneb Automatické zpracování webu

Články Různé

Znají to všichni weboví vývojáři, co se rozhodli testovat: prohlížeče nejsou moc „test-friendly“, možnosti skriptem simulovat chování uživatele jsou celkem mizivé. A nejen při testování – je spousta dalších úloh, kdy by se hodilo „naskriptovat chování prohlížeče“. Řešením může být „bezhlavý prohlížeč“.

Při testování webu (nikoli designu) je potřeba někoho, kdo zkusí kliknout na položku menu a podívá se, jestli se zavolala správná stránka. Jestli se na daném URL objeví to, co se tam objevit má. Jestli (třeba) funguje správně počítadlo návštěv. A mnoho dalších situací a testů, které jsou poměrně mechanické, ale s běžným prohlížečem těžko automatizovatelné. Co v takové situaci?

Jak automatizovat procházení webu?

Jednou z možností, jak kontrolovat, jestli server posílá to, co posílat má, jsou skripty, které využívají wget či cURL. Takový skript požádá server o stránku, server odpoví, skript může v odpovědi zkontrolovat, zda obsahuje ty prvky, které obsahovat má, a skončit s hlášením OK – CHYBA.

Pro řadu úloh, spočívajících v jednoduché kontrole („je na stránce about.php vždy H1 a má element BODY určené ID?“) si s napsáním skriptu nad wget/cURL v *doplňte váš oblíbený jazyk* vystačíme. Ale co když není stránka about.php takhle prostá? Co když se tenhle obsah donačítá AJAXem do prázdné HTML stránky, když se změní #fragment v URL? (Trošku přeháním – ale jen trošku.) Co když je na stránce skript, který má po načtení odkrýt obsah a naopak skrýt hlášení „Povolte si JavaScript“? Co když je na stránce SVG, a v něm skript, který… Nebo když je celé menu dělané JavaScriptem?

Dost, dost, pointa je jasná: Takové úkoly wget ani cURL nezařídí. Je potřeba zapojit opravdový prohlížeč. Ten umí JavaScript, umí renderovat HTML, vytvoří DOM strom, umí pracovat se SVG i s canvasem, zkrátka umí všechno to, co umí prohlížeč, který používají návštěvníci (ještě aby ne…) Má jen drobnou nevýhodu: musí u něj sedět živý člověk a po každém buildu do zblbnutí testovat mechanické věci, protože napsat skript, co bude za něj šmejdit myší, není tak prosté.

Selenium

Nebo že by to tak prosté bylo? Pro testování webových stránek můžete použít nástroj Selenium. Všichni čtenáři Selenium znají, nebo alespoň o něm slyšeli, takže jen stručně: Jde o plugin + další nástroje, které umožňují „nahrát“ posloupnost testovacích úkonů a rozhodnout, jestli výsledek odpovídá očekáváním. Tyto posloupnosti lze exportovat, sdružovat do testovacích sad, spouštět na různých prohlížečích a zpracovávat výsledky.

Někdy je ale „velký“ prohlížeč se Seleniem přece jen příliš silné řešení. Mezi wgetem a Seleniem je velký prostor pro další nástroje. A právě tento prostor zaplňují tzv. headless nástroje.

Bezhlavé nástroje

Termínem „bezhlavý“ (headless) je míněn nástroj, který nemá uživatelské rozhraní, jen API. Například dále zmíněný headless browser nabízí plnohodnotný prohlížeč – postavený na běžném engine, s plnou výbavou, takže funguje i JavaScript, i SVG, i Canvas… Jen uživatelské akce (otvírání stránek, čtení obsahu a další…) jsou vyvolávány skriptem přes API.

Před časem jsme ve zprávičkách upozornili na jeden takový nástroj: Zombie vám otestuje JavaScript. Připomeňme si ho:

Zombie

Nástroj Zombie simuluje prohlížeč (nemá tedy skutečný engine) a uživatelské akce, aniž by k tomu nějaký prohlížeč či uživatele potřeboval. Je napsaný pro Node.js, lze jej tedy spouštět přímo na serveru a automatizovaně. Testy, resp. uživatelské akce, jsou popsány v JavaScriptu (konzistentně se zbytkem Node.js). Například takto:

var zombie = require("zombie");
var assert = require("assert");
// Načteme webovou stránku
zombie.visit("http://localhost:3000/", function (err, browser) {
  // Vyplníme formulář a "stiskneme tlačítko"
    fill("email", "zombie@underworld.dead").
    fill("password", "eat-the-living").
    pressButton("Sign Me Up!", function(err, browser) {
      // Formulář byl odeslán, měla by se načíst nová stránka
      assert.equal(browser.text("title"), "Welcome To Brains Depot");
    })
});

Jednoduchý test požádá o webovou stránku, v ní najde pole email a password, vyplní je a nasimuluje stisk tlačítka „Sign Me Up!“ Výsledná stránka, kterou server vrátí, by měla nést titulek "Welcome To Brains Depot". Pokud ano, je vše v pořádku, pokud ne, je někde chyba. (Příklad je převzat z dokumentace Zombie, což vysvětluje použitý titulek stránky, e-mail i heslo.)

Je možné přistupovat nejen k políčkům formulářů a mačkat tlačítka – Zombie má plnohodnotné API, které nabízí přístup k prvkům pomocí metod DOMu, umožňuje číst a nastavovat atributy, umožňuje například i výběr prvků ve stylu jQuery, dovoluje klikat na odkazy i na další ovládací prvky.

Zombie sice nemá plnohodnotné možnosti prohlížečů, ale důležité věci (DOM, CSS, HTML5 parser atd.) fungují, stejně jako např. historie prohlížeče nebo lokální úložiště. Můžeme tak použít Zombie na testovacím serveru a připravenou sadu testů spouštět automaticky v rámci testování vytvářené aplikace.

Fantóm

Nástroj PhantomJS šel o kus dál. Je postaven na engine QtWebKit, ale nebojte – naštěstí jsou k dispozici přeložené verze i pro Windows a Mac, takže nemusíte vše překládat sami, stačí jen stáhnout. Phantom nabízí všechny možné funkce WebKit prohlížeče. Ostatně – pojďme se podívat na výsledky našeho Testeru moderních technologií.

Výsledky Testeru moderních technologií pro prohlížeč, použitý v PhantomJS

Pojďme si ukázat, jak tento screenshot vlastně vznikl. Je jasné, že to nebylo z obrazovky – Phantom nemá uživatelské rozhraní. Přesto si stránku interně vykresluje a dokáže ji exportovat v různých formátech. Skript, použitý k pořízení screenshotu, je velmi prostý:

if (phantom.state.length === 0) {
    phantom.viewportSize = { width: 600, height: 600 };
    phantom.state = 'rasterize';
    console.log('Otviram stranku...');
    phantom.open('https://www.zdrojak.cz/wp-content/uploads/ukazka/html5');
} else {
    phantom.sleep(200);
    phantom.render('./ukazka.png');
    console.log('Screenshot porizen');
    phantom.exit();
}

Uložíme si ho jako „test.js“ a spustíme pomocí phantomjs test.js. (Na tomto místě si můžeme říct, že Phantom pracuje i s testovacími skripty, napsanými v CoffeeScriptu.)

Z kódu je vidět, jak PhantomJS pracuje. Důležitý prvek je „phantom.state“, neboli stav skriptu. Pokud není žádný (tj. skript probíhá poprvé), provede se první větev – nastaví se viewport, nastaví se nový stav („rasterize“) a je otevřena požadovaná stránka. Skript v tomto okamžiku končí. Znovu je proveden při změně stránky – ovšem tentokrát už je phantom.state nastaven, takže se provádí část za else: chvíli se čeká na „ustálení“, a pak je screenshot uložen jako obrázek ve formátu png. Nakonec je explicitně ukončen běh Phantomu funkcí exit(). Bez ní by Phantom běžel dál a neměl by jak skončit.

Skript probíhá v kontextu načtené stránky,  může tedy přistupovat přímo k elementům a jiným skriptům. Samotný PhantomJS nabízí několik vlastností a metod prostřednictvím objektu phantom. Pojďme si je stručně představit:

Vlastnosti

args (pole)
Seznam argumentů, předaných v příkazové řádce
content (řetězec)
Vlastní obsah HTML stránky (<html>…</html>). Pokud tuto hodnotu změníte, odpovídá to načtení nové stránky
loadStatus (řetězec)
Informace o stavu posledního načítání. Obsahuje buď „success“, nebo „fail“
state (řetězec)
Obsahuje perzistentní řetězec, jehož obsah je zachováván i při změnách stránky. Výchozí hodnota je prázdný řetězec
userAgent (řetězec)
Tento řetězec je posílán při požadavcích coby UA. Výchozí hodnota závisí na konkrétní verzi a platformě, odpovídá zhruba UA prohlížeče Safari.
version (objekt)
Obsahuje informace o verzi PhantomJS
viewportSize (objekt)
Objekt s vlastnostmi width a height. Jeho nastavení před načtením stránky může být užitečné v případě, že chcete např. vybrat, jestli je stránka na výšku nebo na šířku

Objekt phntom nabízí pouhé čtyři metody:

exit(hodnota)
Ukončí běh Phantomu a vrátí danou hodnotu
open(URL)
Otevře dané URL a načte stránku. Pokud stránka obsahuje nějaké skripty, jsou provedené.
render(filename)
Uloží vykreslenou stránku do souboru, Jeho typ je dán příponou; v současnosti lze použít png, jpg nebo pdf.
sleep(ms)
Čeká zadaný počet milisekund (na doběhnutí animací, akcí, na AJAX atd.)

Samotný Phantom tedy nenabízí žádná velká kouzla. Pokud chcete simulovat kliknutí, budete muset poslat danému elementu zprávu click  apod.

Phantom se hodí nejen k testování webů. Lze jím jednoduše „těžit data“ – např. z Twitteru či z jiných webů, které nenabízejí rozumné API. Pokud chcete např. strojově využít slovníku na Seznamu, není nic jednoduššího – hledané slovo předáte jako parametr, přidáte si ho do požadovaného URL, počkáte na načtení stránky a z ní vyextrahujete DIV s výsledky. Příklady, dodávané s PhantomJS, jistě napoví víc. Je mezi nimi např. získávání cesty z Google, měření rychlosti načtení stránky, získání seznamu pizzerií (pomocí parsování výsledků Googlu), spouštění testů v Jasmine či qUnit (testy probíhají v phantomu, po jejich doběhnutí se jen hledá div s výsledkem testů, a ten je vrácen) nebo (poněkud složitější) načtení stránky Twitteru, „kliknutí“ na tlačítko „Přihlásit“ a čekání na animaci přihlašovacího formuláře.

Mezi příklady je i tento jednoduchý skript:

if (phantom.state.length === 0) {
    phantom.state = 'news';
    phantom.viewportSize = { width: 320, height: 480 };
    phantom.open('http://news.google.com/news/i/section?&topic=t');
} else {
    var body = document.body;
    body.style.backgroundColor = '#fff';
    body.querySelector('div#title-block').style.display = 'none';
    body.querySelector('form#edition-picker-form').parentElement.parentElement.style.display = 'none';
    phantom.sleep(500);
    phantom.render('technews.png');
    phantom.exit();
}

Jedná se o lehce rozšířenou variantu předchozího skriptu, která ještě před zobrazením udělá některé úpravy – část webu skryje a ponechá jen požadované informace.

Antispam buster

Na místě je upozornit na možný problém – více či méně důmyslné metody pro blokování komentářového spamu, založené na tom, že „roboti neumí JavaScript“, tváří v tvář možnostem takového nástroje selže. Phantom dokáže načíst stránku tak, jak by ji načetl prohlížeč. Bude mu fungovat vše, co běžnému uživateli – skriptem se políčka odstraní / vytvoří, ve formuláři je správný CSRF token, Phantom vyplní co je potřeba, případně počká vhodnou dobu, a formulář odešle.

A dál?

V tomto článku jsme se věnovali především nástroji Phantom a serverovému nástroji Zombie. Existují ale i jiné alternativy. Uživatele Ruby může zajímat Watir. Zajímat vás může článek o kombinaci Watiru, Selenia a WebDriveru. Obdobou Watiru pro prostředí .NET je Watin. Pokud je vám bližší Python, můžete zkusit twill. No a konečně alternativa pro Javu nese název HtmlUnit.

A nezapomeňte: bezhlavé nástroje nejsou kompatibilní s bezhlavými programátory.

Komentáře

Subscribe
Upozornit na
guest
8 Komentářů
Nejstarší
Nejnovější Most Voted
Inline Feedbacks
View all comments
Franta

Spousta webovych aplikaci dnes obsahuje nejaky flash prvek (video, mp3 …) Celkem bych uvital nejaky plugin do selenia ktery dokaze simulovat click apod. ve flashi. Neznate nekdo neco takoveho ?

Franta

Diky moc… take se podivam.

Franta

Nakonec jsem nasel, ze existuje primo ten plugin do selenia http://code.google.com/p/flash-selenium/

Necroman

Zajimave nastroje, ty si primo rikaji o vyzkouseni na seznamu 62 000 hesel, co zas vydala hackerska skupina Lulzsec :)
http://www.theinquirer.net/inquirer/news/2079606/lulzsec-releases-62-emails-passwords

Franta.

ROFL, script kiddies se seleniem…

v6ak

K Watiru existují určité obdoby pro jiné jazyky/pplatformy, například Javu (Watij) a .NET (Watin).

Holmistr

Velmi podobné jsou funkcionální testy v symfony. Společně s unit testy je to k nezaplacení, člověk ušetří spoustu času…

Enum a statická analýza kódu

Mám jednu univerzální radu pro začínající programátorty. V učení sice neexistují rychlé zkratky, ovšem tuhle radu můžete snadno začít používat a zrychlit tak tempo učení. Tou tajemnou ingrediencí je statická analýza kódu. Ukážeme si to na příkladu enum.