Martin Dybal

Vývojářský blog Martina Dybala

Podle kategorie

Canopy – přehledné UI testy

Martin Dybal       6. 5. 2015       Testování, F#       5716 zobrazení

V poslední době jsem se hodně věnoval různým formám automatického testování, postupně jsem se prokousal od unit testů, přes integrační testy až po UI testy. Zkoušel jsem CodedUI, Selenium a díky kolegovy jsem narazil na Canopy.

O UI testování se často říká že je složité, zbytečné, drahé a nevyplatí se. V tomhle článku bych vás chtěl přesvědčit o opaku. Ukážeme si že se dají psát UI testy i jednoduše a srozumitelně. Napíšeme si jednoduché smoke testy pro již běžící web.

Canopy je UI test Framework postavený nad Seleniem, díky tomu má dobrou podporu a široké možnosti. Je napsaný v F# a jeho hlavním cílem je srozumitelná syntaxe testů.

"click polling" &&& fun _ -> 
    url "http://lefthandedgoat.github.io/canopy/testpages/autocomplete"
    click "#search"
    click "table tr td"
    "#console" == "worked"

Pojďme se na to podívat. Pokud neumíte F#, tak nevadí, elementární znalosti vám budou stačit, alespoň máte možnost naučit se něco nového. Syntaxi F# budu přirovnávat k C#.

První test

Založíme nový projekt F# Console app. Canopy potřebuje .Net 4 nebo vyšší.

.image

Přidáme nuget package Canopy jako dependecy se nám nainstalujte i Selenium.01_NugetPackageInstall

Stávající kód nahradíme naším prvním testem

open canopy
open runner
open System
 
//První UI test
"test method" &&& fun _ ->
    url "http://google.com"  //Přejdi na URL
 
start firefox //Nastartuje browser
run() //Spustí všechny UI testy

open System je obdoba C# using System. "test method" &&& fun _ –> je deklarace testu kde “Test method” je název našeho testu a na řádku pod je implementace testu. I tak málo nám stačí ke zprovoznění UI testů.

image

Pozor! F# podobně jako Python používá bílé znaky k ukončení bloku.

"test method" &&& fun _ ->
    url "http://google.com"
    //Je to až v metodě, tudíže se to nikdy nepustí
    start firefox 
    run()

Ovšem tenhle test zatím nic netestuje, pouze nám otevře Firefox a načte stránku. Pojďme si něco vyhledat a otestovat. Testování provádíme klasicky pomocí assertů, assert je ověření zda platí daná podmínka, Canopy má celou řadu assertů, jejich kompletní seznam najdete v dokumentaci.

"test google search for Canopy home page" &&& fun _ ->
    url "http://google.com"
    "#lst-ib" << "Canopy UI test framework" //do inputu s id lst-ib vložíme string "Canopy UI test framework"
    press enter //simulujeme stisk enter
    click ".srg .g a" //kliknutí na první výsledek hledání. Selektor je poněkud kostrbatý 
    on "http://lefthandedgoat.github.io/canopy/" //Assert je aktuální stránka 
start firefox
run()

Nyní test testuje, jestli po kliknutí na první odkaz vyhledávání je jako první výsledek domovská stránka Canopy. Poslední věc, kterou od testu očekáváme je, že po sobě uklidí. Na to máme metodu quit(). Výsledný test tedy vypadá takto.

open canopy
open runner
open System
 
"test google search for Canopy home page" &&& fun _ ->
    url "http://google.com"
    "#lst-ib" << "Canopy UI test framework"
    press enter
    click ".srg .g a"
    on "http://lefthandedgoat.github.io/canopy/"
 
start firefox
run()
quit()

Když jeden prohlížeč nestačí

Testovat vše jen ve Firefoxu by bylo trochu málo, Selenium podporuje celou řadu prohlížečů, aktuální seznam najdete zde. Pojďme si pustit náš test i v Chrome.

K tomu potřebujeme ChromeDriver. Canopy defaultně očekává chrome driver na cestě C:\\ChromeDriver.exe, to ale není moc vhodné. My si ho nainstalujeme přes nuget, najdeme ho jako Selenium.WebDriver.ChromeDriver

.02_NugetPackageInstallChrome

Do solution se nám přidá soubor chromedriver.exe. Zkontrolujte si v properties souboru, že je nastaveno Copy if newer v Copy to output directory.

image

Cestu k driveru přenastavíme pomocí následujících dvou řádků, jak asi tušíte operátor <- je v F# operátorem přiřazení. "." označuje aktuální složku.

open configuration

chromeDir <- "."

Teď již můžeme testy pustit i pro chrome

open canopy
open runner
open System
open configuration

chromeDir <- "."

"test google search for Canopy home page" &&& fun _ ->
    url "http://google.com"
    "#lst-ib" << "Canopy UI test framework"
    press enter
    click ".srg .g a"
    on "http://lefthandedgoat.github.io/canopy/"

start chrome
run()

start firefox
run()

quit()

Na gitu je již přidaná metoda runFor, díky které půjde spouštět testy paralelně v několika prohlížečích najednou, ale v aktuálním buildu nugetu ještě není.

runFor [chrome; firefox; ie]

Smoke test reálného webu

Na začátku jsem sliboval že si napíšeme testy skutečný web, jako pokusný web použijeme Lepší místo. Uděláme si jednoduchou sadu smoke testů, které můžou vývojáři pustit po nasazení, aby ověřil základní funkčnost aplikace.

Jako první si napíšeme test ověřující funkčnost vyhledávání, test je velmi podobný testu vyhledávání na google.

"search Oprava laveček v nádražní budově Kr.Pole" &&& fun _ ->
    url "http://www.lepsimisto.cz/"

    "#searching" << "Oprava laveček v nádražní budově Kr.Pole"
    press enter

    click "section.search-results article.bottom div.content div.item h3 a"

    on "http://www.lepsimisto.cz/tip/oprava-lavecek-v-nadrazni-budove-krpole"

Na tomhle pro nás ale není nic nového. Pojďme zkontrolovat jestli se na mapě zobrazuje výpis tipů, problémem je že se tipy do načítají až po načtení stránky. Nejjednodušším způsobem jak otestovat, takovou to situaci je sleep.

"maps show Announcements" &&& fun _ ->
    url "http://www.lepsimisto.cz/mapa"
    sleep 5
    displayed (element "#announcement-list .item")

Tohle není ale moc pěkné řešení a hlavně není deterministické. Mnohem lepším řešením je počkat si až se data načtou a pak je otestovat.

"maps show Announcements" &&& fun _ ->
    url "http://www.lepsimisto.cz/mapa"
    let itemsIsDisplayed() =  (elements "#announcement-list .item").Length > 3
    waitFor itemsIsDisplayed

Vím že testy nejsou shodné, ale jde mi o ukázku možností Canopy. Metoda waitFor čeká dokud itemsIsDisplayed nevrátí true, pokud to nestihne do timeout (defaultně 10s) test failne.

Dalším testem otestujeme přepínání jazyků

"check translation works" &&& fun _ ->
    url "http://www.lepsimisto.cz/o-projektu"
    click (first "#language-selector a")
    contains "Our independence"  (read ".about-content")

    click (last "#language-selector a")
    contains "Naše nezávislost" (read ".about-content")

Ale co když chceme zjistit jetli je daný text na stránce nezávisle na jazyku? Potřebovali by jsme test, kterému předhodíme tři stringy a on nám řekl jestli třetí string obsahuje jeden z předchozích dvou. Jenže takový assert v Canopy není. Naštěstí dopsat si vlastní assert není těžké. Assert je funkce, která nic nevrátí a pokud assert padne vyhodí výjimku. Takže námi požadovaný assert by vypadal nějak takhle

let containsOneOf (value1 : string) (value2 : string) (value3 : string) =
    if (value3.Contains(value1) <> true) &&
       (value3.Contains(value2) <> true)  then
        raise (InvalidOperationException(sprintf "contains check failed.  %s does not contain %s or %s" value3 value1 value2))

Samotný test

"about project" &&& fun _ ->
    url "http://www.lepsimisto.cz/o-projektu"
    containsOneOf "Naše nezávislost" "Our independence"  (read ".about-content")

Výsledný soubor se smoke testama

open canopy
open runner
open System
open OpenQA.Selenium.Chrome
open configuration

chromeDir <- @"."

let containsOneOf (value1 : string) (value2 : string) (value3 : string) =
    if (value3.Contains(value1) <> true) &&
       (value3.Contains(value2) <> true)  then
        raise (InvalidOperationException(sprintf "contains check failed.  %s does not contain %s or %s" value3 value1 value2))

"about project" &&& fun _ ->
    url "http://www.lepsimisto.cz/o-projektu"
    containsOneOf "Naše nezávislost" "Our independence"  (read ".about-content")

"check translation works" &&& fun _ ->
    url "http://www.lepsimisto.cz/o-projektu"
    click (first "#language-selector a")
    contains "Our independence"  (read ".about-content")

    click (last "#language-selector a")
    contains "Naše nezávislost" (read ".about-content")

"maps show Announcements" &&& fun _ ->
    url "http://www.lepsimisto.cz/mapa"
    let itemsIsDisplayed() =  (elements "#announcement-list .item").Length > 3
    waitFor itemsIsDisplayed

"search Oprava laveček v nádražní budově Kr.Pole" &&& fun _ ->
    url "http://www.lepsimisto.cz/"
    "#searching" << "Oprava laveček v nádražní budově Kr.Pole"
    press enter

    click "section.search-results article.bottom div.content div.item h3 a"
    on "http://www.lepsimisto.cz/tip/oprava-lavecek-v-nadrazni-budove-krpole"

start firefox
run()

start chrome
run()

quit()

ZÁZNAM TESTŮ

Hodně praktickou funkcí je možnost pořídit screenshot. Napíšeme si na to jednoduchou pomocnou funkci

let testPath = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) + @"\canopy\" + DateTime.Now.ToString("yyyy-MM-dd_HH-mm-ss")

let takeScreenshot(screenshotName : string) =
    let path = testPath + "\\" + browser.ToString()
    screenshot path screenshotName

Stačí ji zavolat kdekoli s testu

takeScreenshot "search"

Další žádanou vlastností je export výsledků testů do html. Stačí pár snadných úprav

open canopy
open runner
open System
open OpenQA.Selenium.Chrome
open configuration
open reporters

chromeDir <- @"."

let startTime = DateTime.Now.ToString("yyyy-MM-dd_HH-mm-ss")
let path = fun _ -> Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) + @"\canopy\" + startTime + "\\" + browser.ToString()

let takeScreenshot (screenshotName : string) =
    let pathBrowserFolder = path()
    screenshot pathBrowserFolder screenshotName

let containsOneOf (value1 : string) (value2 : string) (value3 : string) =
    if (value3.Contains(value1) <> true) &&
       (value3.Contains(value2) <> true)  then
        raise (InvalidOperationException(sprintf "contains check failed.  %s does not contain %s or %s" value3 value1 value2))

context "tests"
"about project" &&& fun _ ->
    url "http://www.lepsimisto.cz/o-projektu"
    containsOneOf "Naše nezávislost" "Our independence"  (read ".about-content")

"check translation works" &&& fun _ ->
    url "http://www.lepsimisto.cz/o-projektu"
    click (first "#language-selector a")
    contains "Our independence"  (read ".about-content")

    click (last "#language-selector a")
    contains "Naše nezávislost" (read ".about-content")

"maps show Announcements" &&& fun _ ->
    url "http://www.lepsimisto.cz/mapa"
    let itemsIsDisplayed() =  (elements "#announcement-list .item").Length > 3
    waitFor itemsIsDisplayed

"search Oprava laveček v nádražní budově Kr.Pole" &&& fun _ ->
    url "http://www.lepsimisto.cz/"
    "#searching" << "Oprava laveček v nádražní budově Kr.Pole"
    press enter

    takeScreenshot "search"

    click "section.search-results article.bottom div.content div.item h3 a"
    on "http://www.lepsimisto.cz/tip/oprava-lavecek-v-nadrazni-budove-krpole"

reporter <- new LiveHtmlReporter() :> IReporter
let chromeReporter = reporter :?> LiveHtmlReporter
start chrome
run()
let chromePath = path()
chromeReporter.saveReportHtml chromePath "report"

reporter <- new LiveHtmlReporter() :> IReporter
let firefoxReporter = reporter :?> LiveHtmlReporter
start firefox
run()
let firefoxPath = path()
firefoxReporter.saveReportHtml firefoxPath "report"

quit()

Výsledek testů najdete %appdata%\canopy

image

Další možností je TeamCity report, o tom ale až příště.

A pokud se večer nemáte na co dívat přidejte k &&& ještě jeden &&&&, testy se pustí v pomalém módu a zvýrazňují co zrovna dělají.

 

hodnocení článku

0       Hodnotit mohou jen registrované uživatelé.

Mohlo by vás také zajímat

 

Nový příspěvek

 

Příspěvky zaslané pod tento článek se neobjeví hned, ale až po schválení administrátorem.

Canopy ve VS 2013

Je to pěkný, ještě by mě zajímalo, jak udělat, aby to v případě chyby předalo fail do Unit Testu ve VS aby to mohlo být součástí Buildu. Do UnitTestu jsem to dostal, ale vždy to Passne, i když je tam chyba. Neřešili jste to náhodou?

nahlásit spamnahlásit spam 0 odpovědětodpovědět

My používáme TeamCity, takže s tím osobní zkušenost nemám. Zkuste ale http://stackoverflow.com/questions/24618..., vypadá to nadějně.

nahlásit spamnahlásit spam 0 odpovědětodpovědět

Hezké

Framework hezký, ale ten F# je hroznej. Zkusím to o víkendu rozchodit v C# a zabalit do unit test projektu, aby se to dalo používat nějak rozumně.

nahlásit spamnahlásit spam 0 odpovědětodpovědět

To nedává moc smysl, radši si to rovnou postav nad Seleniem. Tady je hlavní výhoda ta čitelnost F#, ten test ti pak napíše i projekťák. Trochu to připomíná akceptační testy ve SpecFlow. Testy jako takové jsou úplně v pohodě, spíš to potřebuje dopsat ještě větší sadu assertů a helprů pro práci s HTML a CSS. I těžší testy které něco odesílají, vyplňují a podobně jsou pěkně čitelné. A ještě by se hodilo porovnávání obrazu, které by mi dokázalo říct jestli screen odpovídá nějakému před definovanému.

nahlásit spamnahlásit spam 0 odpovědětodpovědět

No mě to v tom F# právě nepřijde čitelné ani trochu, je to spíš zmatené. Ale to je asi o zvyku.

nahlásit spamnahlásit spam 0 odpovědětodpovědět

Kdyby, všechny testy vypadali jako tento:

"search Oprava laveček v nádražní budově Kr.Pole" &&& fun _ ->
    url "http://www.lepsimisto.cz/"
    "#searching" << "Oprava laveček v nádražní budově Kr.Pole"
    press enter

    takeScreenshot "search"

    click "section.search-results article.bottom div.content div.item h3 a"
    on "http://www.lepsimisto.cz/tip/oprava-lavecek-v-nadrazni-budove-krpole"


Tak by to byla paráda, zkusím pár assertu dopsat a poslat jim pull request

nahlásit spamnahlásit spam 0 odpovědětodpovědět
                       
Nadpis:
Antispam: Komu se občas házejí perly?
Příspěvek bude publikován pod identitou   anonym.

Nyní zakládáte pod článkem nové diskusní vlákno.
Pokud chcete reagovat na jiný příspěvek, klikněte na tlačítko "Odpovědět" u některého diskusního příspěvku.

Nyní odpovídáte na příspěvek pod článkem. Nebo chcete raději založit nové vlákno?

 

  • Administrátoři si vyhrazují právo komentáře upravovat či mazat bez udání důvodu.
    Mazány budou zejména komentáře obsahující vulgarity nebo porušující pravidla publikování.
  • Pokud nejste zaregistrováni, Vaše IP adresa bude zveřejněna. Pokud s tímto nesouhlasíte, příspěvek neodesílejte.

Příspěvky zaslané pod tento článek se neobjeví hned, ale až po schválení administrátorem.

přihlásit pomocí externího účtu

přihlásit pomocí jména a hesla

Uživatel:  
Heslo:  

zapomenuté heslo

 

založit nový uživatelský účet

zaregistrujte se

 
zavřít

Nahlásit spam

Opravdu chcete tento příspěvek nahlásit pro porušování pravidel fóra?

Nahlásit Zrušit

Chyba

zavřít

feedback