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šší.
.
Přidáme nuget package Canopy jako dependecy se nám nainstalujte i Selenium.
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ů.
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
.
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.
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
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í.