layout | title | inheader | permalink |
---|---|---|---|
page |
Viikko 3 |
false |
/tehtavat3/ |
Viikon 3 tehtävien Python-versiot löytyvät täältä
{% include guidance_info.md %}
Muista myös tämän viikon monivalintatehtävät.
Tehtävissä 1-3 tutustutaan siihen miten gradle-sovelluksiin lisätään ulkoisia kirjastoja riippuvuudeksi, sekä miten riippuvuuksia sisältävästä koodista saadaan generoitua jar-paketti. Loput tehtävät liittyvät storyjen hyväksymistestauksen automatisointiin tarkoitetun Cucumberin, sekä selainsovellusten testaamiseen käytettävän Selenium-kirjaston soveltamiseen.
{% include typo_instructions.md %}
Tehtävät palautetaan GitHubiin, sekä merkitsemällä tehdyt tehtävät palautussovellukseen <{{site.stats_url}}>
Katso tarkempi ohje palautusrepositorioita koskien täältä.
Käytämme tälläkin viikolla gradle-muotoisia projekteja. Jos gradle-koodi lukee syötteitä komentoriviltä, tulee määrittelytiedostojen loppuun liittää seuraava
run {
standardInput = System.in
}
Ilman tätä määrittelyä ohjelmaa gradlella suorittaessa, eli komennolla gradle run
, ohjelma ei pääse käsiksi syötevirtaan ja scannerin luominen epäonnistuu.
Tämän viikon tehtäviin liittyviin projekteihin määrittely on jo lisätty.
Jos ohjelma lukee syötteitä käyttäjältä, kannattaa se suorittaa komennolla gradle -q --console plain run
, jolloin gradlen tekemät tulostukset eivät tule konsoliin.
HUOM! näyttää siltä, että NetBeans 11.1:llä Scanner ei toimi ollenkaan gradle-projekteissa, eli jos törmäät samaan ongelmaan, suorita ohjelmat komentoriviltä.
Hae kurssirepositorion https://github.com/ohjelmistotuotanto-hy/syksy2020 hakemistossa viikko3/nhlreader lähes tyhjä gradle-projektin runko.
- mukana on kohta tarvitsemasi luokka Player
Tehdään ohjelma, jonka avulla voi hakea https://nhl.com-sivulta kuluvan kauden NHL-liigan tilastotietoja. Jos tarkkoja ollaan, niin tilastot haetaan tämän kurssin tarpeisiin rakennetulta palvelimelta, joka hakee todelliset tilastot NHL:n sivulta kerran vuorokaudessa. Pandemian takia kautta ei ole aloitettu ja tilastot ovat viime vuodelta.
Näet tilastojen json-muotoisen raakadatan web-selaimella osoitteesta https://nhlstatisticsforohtu.herokuapp.com/players
Tee ohjelma, joka listaa suomalaisten pelaajien tilastot.
Ohjelmassa tarvitaan kahta kirjastoa eli riippuvuutta:
- HTTP-pyynnön tekemiseen Apache HttpClient Fluent API
- json-muotoisen merkkijonon muuttaminen olioksi http://code.google.com/p/google-gson/
Kertaa nopeasti viime viikolta, miten gradle-projektin riippuvuudet määritellään. Tarvittaessa lisää tietoa löytyy Gradlen manuaalista.
Liitä projektisi käännösaikaisiksi (compile) riippuvuuksiksi
- Apache HttpClient Fluent API ja gson
- löydät riippuvuuksien tiedot osoitteesta http://mvnrepository.com/
- Ota molemmista uusin versio
- Klikkaamalla versionmeroa, avautuu näkymä, mistä voit kopioida suoraan riippuvuuden lisäävän rivin
Voit ottaa projektisi pohjaksi seuraavan tiedoston:
package ohtu;
import com.google.gson.Gson;
import java.io.IOException;
import org.apache.http.client.fluent.Request;
public class Main {
public static void main(String[] args) throws IOException {
String url = "https://nhlstatisticsforohtu.herokuapp.com/players";
String bodyText = Request.Get(url).execute().returnContent().asString();
System.out.println("json-muotoinen data:");
System.out.println( bodyText );
Gson mapper = new Gson();
Player[] players = mapper.fromJson(bodyText, Player[].class);
System.out.println("Oliot:");
for (Player player : players) {
System.out.println(player);
}
}
}
Tehtäväpohjassa on valmiina luokan Player
koodin runko. Gson-kirjaston avulla json-muotoisesta datasta saadaan taulukollinen Player
-olioita, joissa jokainen olio vastaa yhden pelaajan tietoja. Tee luokkaan oliomuuttujat (sekä tarvittaessa getterit ja setterit) kaikille json-datassa oleville kentille, joita ohjelmasi tarvitsee.
Ohjelmasi voi toimia esimerkiksi seuraavalla tavalla:
Players from FIN Wed Nov 06 23:31:32 EET 2019 Henrik Borgstrom team FLA goals 0 assists 0 Sami Niku team WPG goals 0 assists 0 Mikael Granlund team NSH goals 2 assists 2 Miikka Salomaki team NSH goals 1 assists 0 Roope Hintz team DAL goals 9 assists 2 Sebastian Aho team CAR goals 5 assists 5 Erik Haula team CAR goals 8 assists 3 Miro Heiskanen team DAL goals 4 assists 5 Markus Granlund team EDM goals 0 assists 1 Henri Jokiharju team BUF goals 1 assists 4 Joel Armia team MTL goals 6 assists 4 Artturi Lehkonen team MTL goals 2 assists 4 ...
Tulostusasu ei tässä tehtävässä ole oleellista, eikä edes se mitä pelaajien tiedoista tulostetaan.
Tulosta suomalaiset pelaajat pisteiden (goals + assists) mukaan järjestettynä. Tarkka tulostusasu ei ole taaskaan oleellinen, mutta se voi esimerkiksi näyttää seuraavalta:
Players from FIN Wed Nov 06 23:47:11 EET 2019 Aleksander Barkov FLA 2 + 15 = 17 Patrik Laine WPG 3 + 11 = 14 Mikko Rantanen COL 5 + 7 = 12 Teuvo Teravainen CAR 4 + 8 = 12 Roope Hintz DAL 9 + 2 = 11 Erik Haula CAR 8 + 3 = 11 Joel Armia MTL 6 + 4 = 10 Sebastian Aho CAR 5 + 5 = 10 Kasperi Kapanen TOR 4 + 6 = 10 Miro Heiskanen DAL 4 + 5 = 9 Joonas Donskoi COL 5 + 3 = 8 Sami Vatanen NJD 4 + 4 = 8 Artturi Lehkonen MTL 2 + 4 = 6 Valtteri Filppula DET 1 + 5 = 6 Mikko Koivu MIN 1 + 5 = 6 Kaapo Kakko NYR 3 + 2 = 5 Henri Jokiharju BUF 1 + 4 = 5 Ville Heinola WPG 1 + 4 = 5 Rasmus Ristolainen BUF 0 + 5 = 5 ...
- tehdään äskeisen tehtävän projektista jar-tiedosto komennolla
gradle jar
- suoritetaan ohjelma komennolla
java -jar build/libs/nhlreader.jar
- mutta ohjelma ei toimikaan, tulostuu:
$ java -jar build/libs/nhlreader.jar
Exception in thread "main" java.lang.NoClassDefFoundError: org/apache/http/client/fluent/Request
at ohtu.Main.main(Main.java:16)
Caused by: java.lang.ClassNotFoundException: org.apache.http.client.fluent.Request
at java.net.URLClassLoader.findClass(URLClassLoader.java:381)
at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:331)
at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
... 1 more
Mistä on kyse? Ohjelman riippuvuuksia eli projekteja Apache HttpClientin ja gson vastaavat jar-tiedostot eivät ole käytettävissä, joten ohjelma ei toimi.
Saamme generoitua ohjelmasta jar-tiedoston, joka sisältää myös kaikki riippuvuudet gradlen shadow-pluginin avulla.
Ota plugin käyttöön laajentamalla tiedoston build.gradle pluginien määrittelyä seuraavasti:
plugins {
id 'java'
id 'application'
id "com.github.johnrengelman.shadow" version "6.1.0"
}
Tutki komennon gradle tasks avulla, miten saat muodostettua riippuvuudet sisältävän jarrin.
Generoi jar ja varmista, että ohjelma toimii komennolla java -jar shadowilla_tehty_jar.jar
HUOM: Tässä tehtävässä on pakko käyttää seuraavaa vanhaa tapaa mainClassin määrittelyyn tiedostosssa build.gradle
mainClassName = 'ohtu.Main'
sillä shadowJar-plugin ei osaa uudempaa syntaksia. Jos latasit koodin ennen keskiviikkoa 12.11. saattaa tehtäväpohjassasi olla vanha määrittelytapa käytössä.
Lue täällä oleva Cucumber-johdanto ja tee siihen liittyvät tehtävät.
Hae kurssirepositorion hakemistossa viikko3/LoginCucumber oleva projekti.
Tutustu ohjelman rakenteeseen. Piirrä ohjelman rakenteesta UML-kaavio.
Huomaa, että ohjelman AuthenticationService-olio ei talleta suoraan User-oliota vaan epäsuorasti UserDAO-rajapinnan kautta. Mistä on kysymys?
DAO eli Data Access Object on yleisesti käytetty suunnittelumalli jonka avulla abstrahoidaan sovellukselta se, miten oliot on talletettu, ks. esim. https://www.oracle.com/technetwork/java/dataaccessobject-138824.html
Ideana on, että sovellus "hakee" ja "tallettaa" User-oliot aina UserDAO-rajapinnan metodeja käyttäen. Sovellukselle on injektoitu konkreettinen toteutus, joka tallettaa oliot esim. tietokantaan tai tiedostoon. Se minne ja miten talletus tapahtuu on kuitenkin läpinäkyvää sovelluksen muiden osien kannalta.
Ohjelmaamme on määritelty testauskäyttöön sopiva InMemoryUserDao, joka tallettaa User-oliot ainoastaan muistiin. Muu ohjelma säilyisi täysin muuttumattomana jos määriteltäisiin esim. SqliteUserDao, joka hoitaa talletuksen tietokantaan ja injektoitaisiin tämä sovellukselle.
Kokeile ohjelman suorittamista (ohjelman tuntemat komennot ovat login ja new) ja suorita siihen liittyvät testit.
Muistutus: saat suoritettua ohjelman ilman gradlen välitulostuksia komennolla gradle run --console=plain
Tutki miten testien stepit on määritelty suoritettavaksi tiedostossa src/test/java/ohtu/StepDefs.java Huomioi erityisesti, miten testit käyttävät testaamisen mahdollistavaa stub-olioa käyttäjän syötteen ja ohjelman tulosteen käsittelyyn. Periaate on täsmälleen sama kuin viikon 1 tehtävien riippuvuuksien injektointiin liittyvässä esimerkissä.
Lisää user storylle User can log in with valid username/password-combination seuraavat skenaariot ja määrittele niihin sopivat When ja Then -stepit:
Scenario: user can not login with incorrect password Given command login is selected When ... Then ... Scenario: nonexistent user can not login to Given command login is selected When ... Then ...
Tee stepeistä suoritettavat ja varmista että testit menevät läpi.
Tee user storylle A new user account can be created if a proper unused username and a proper password are given seuraavat skenaariot ja niille sopivat stepit:
Feature: A new user account can be created if a proper unused username and password are given Scenario: creation is successful with valid username and password Given command new is selected When ... Then ... Scenario: creation fails with already taken username and valid password Given command new is selected When ... Then ... Scenario: creation fails with too short username and valid password Given command new is selected When ... Then ... Scenario: creation fails with valid username and too short password Given command new is selected When ... Then ... Scenario: creation fails with valid username and password long enough but consisting of only letters Given command new is selected When ... Then ... Scenario: can login with successfully generated account Given user "eero" with password "salainen1" is created And command login is selected When ... Then ...
- käyttäjätunnuksen on oltava merkeistä a-z koostuva vähintään 3 merkin pituinen merkkijono, joka ei ole vielä käytössä
- salasanan on oltava pituudeltaan vähintään 8 merkkiä ja se ei saa koostua pelkästään kirjaimista (vihje)
Tee stepeistä suoritettavia ja täydennä ohjelmaa siten että testit menevät läpi. Oikea paikka koodiin tuleville muutoksille on luokan AuthenticationService metodi invalid
HUOM skenaarioita kannattaa toteuttaa yksi kerrallaan, laittaen samalla vastaava ominaisuus ohjelmasta kuntoon. Eli ÄLÄ copypastea ylläolevaa kerrallaan feature-tiedostoon, vaan etene pienin askelin. Jos yksi skenaario ei mene läpi, älä aloita uuden tekemistä ennen kuin kaikki ongelmat on selvitetty. Seuraava luku antaa muutaman vihjeen testien debuggaamiseen.
On todennäköistä että testien tekemisen aikana tulee ongelmia, joiden selvittäminen ei ole triviaalia.
Jos näin käy, kannattaa ongelmaa selvitellessä suorittaa ainoastaan yhtä testiä kerrallaan. Tämä onnistuu merkkaamalla ongelmallinen testi tagilla, eli @-merkillä alkavalla merkkijonolla. Seuraavassa on merkattu eräs testiskenaario tagilla @problem:
Feature: User can log in with valid username/password-combination // ... @problem Scenario: user can not login with incorrect password Given command login is selected When username "pekka" and password "wrong" are entered Then system will respond with "wrong username or password" // ...
Määrittelemällä luokkaan RunCucumberTest annotaatiolle @CucumberOptions parametri tags, on mahdollista säädellä mitä testejä Cucumber suorittaa:
@RunWith(Cucumber.class)
@CucumberOptions(
plugin = "pretty",
features = "src/test/resources/ohtu",
snippets = SnippetType.CAMELCASE,
tags = "@problem"
)
public class RunCucumberTest {}
Näin määriteltynä tulee suoritetuksi ainoastaan tagilla @problem merkitty testi.
Sama tagi on mahdollista liittää myös useampaan skenaarioon, tai suoraan featureen, jolloin jokainen featureen liittyvä story tulee tagatyksi.
Myös vanha kunnon println-debuggaus toimii Cucumberin yhteydessä. Voit lisäillä println-komentoja testattavassa tai testikoodissa koodissa:
@Then("system will respond with {string}")
public void systemWillRespondWith(String expectedOutput) {
System.out.println("ohjelma tulosti seuraavat rivit "+io.getPrints());
assertTrue(io.getPrints().contains(expectedOutput));
}
Tarkastellaan edellisestä tehtävästä tutun toiminnallisuuden tarjoamaa esimerkkiprojektia, joka löytyy kurssirepositorion hakemistossa viikko3/WebLogin oleva projekti.
Sovellus on toteutettu Spark-nimisellä minimalistisella Web-sovelluskehyksellä. Spark on osalle kenties tuttu kurssilta Tietokantojen perusteet.
Hae projekti ja käynnistä se komennolla gradle run
Pääset käyttämään sovellusta avaamalla selaimella osoitteen http://localhost:4567
![]({{ "/images/lh3-2.png" | absolute_url }}){:height="200px" }
Sovellus siis toimii localhostilla eli paikallisella koneellasi portissa 4567.
Sovelluksen rakenne on suunnilleen sama kuin tehtävien 4-6 ohjelmassa. Poikkeuksen muodostaa pääohjelma, joka sisältää selaimen tekemät HTTP-pyynnöt. Tässä vaiheessa ei ole tarpeen tuntea HTTP-pyyntöjä käsittelevää koodia kovin tarkasti. Katsotaan kuitenkin pintapuolisesti mistä on kysymys.
Polulle "/" eli sovelluksen juureen, osoitteeseen http://localhost:4567 tulevat pyynnöt käsittelee mainista seuraava koodinpätkä:
get("/", (request, response) -> {
HashMap<String, String> model = new HashMap<>();
model.put("template", "templates/index.html");
return new ModelAndView(model, LAYOUT);
}, new VelocityTemplateEngine());
Koodi muodostaa luokan VelocityTemplateEngine avulla hakemistossa templates/index.html olevan "templateen" perustuvan HTML-sivun, ja palauttaa sen käyttäjän selaimelle.
Sivun HTML-koodi on seuraava:
<h1>Ohtu App</h1>
<ul>
<li><a href="login">login</a></li>
<li><a href="user">register new user</a></li>
</ul>
Kaikki get-alkuiset määrittelyt ovat samanlaisia, ne ainoastaan muodostavat HTML-sivun (joiden sisällön määrittelevät templatet sijaitsevat hakemistossa templates) ja palauttavat sivun selaimelle.
post-alkuiset määrittelyt ovat monimutkaisempia, ne käsittelevät lomakkeiden avulla lähetettyä tietoa. Esimerkiksi käyttäjän kirjautumisyrityksen käsittelee seuraava koodi:
post("/login", (request, response) -> {
HashMap<String, String> model = new HashMap<>();
String username = request.queryParams("username");
String password = request.queryParams("password");
if ( !authenticationService().logIn(username, password) ) {
model.put("error", "invalid username or password");
model.put("template", "templates/login.html");
return new ModelAndView(model, LAYOUT);
}
response.redirect("/ohtu");
return new ModelAndView(model, LAYOUT);
}, new VelocityTemplateEngine());
Koodi pääsee käsiksi käyttäjän lomakkeen avulla lähettämiin tietoihin request-olion kautta:
String username = request.queryParams("username");
String password = request.queryParams("password");
Koodi käyttää metodikutsulla authenticationService()
saamaansa AuthenticationService
-oliota kirjautumisen onnistumisen varmistamiseen. Jos kirjautuminen ei onnistu, eli mennään if-haaraan, palataan kirjautumislomakkeelle. Lomakkeelle näytettäväksi liitetään virheilmoitus invalid username or password.
Tutustu nyt sovelluksen rakenteeseen ja toiminnallisuuteen. Saat sammutettua sovelluksen painamalla konsolissa ctrl+c tai ctrl+d.
Jatketaan saman sovelluksen parissa.
Käynnistä websovellus edellisen tehtävän tapaan komentoriviltä. Varmista selaimella, että sovellus on päällä.
Selenium WebDriver -kirjaston avulla on mahdollista simuloida selaimen käyttöä koodista käsin. Sovelluksen luokassa ohtu.Tester.java on "toinen pääohjelma", jonka koodi on seuraava:
public static void main(String[] args) {
WebDriver driver = new ChromeDriver();
driver.get("http://localhost:4567");
WebElement element = driver.findElement(By.linkText("login"));
element.click();
element = driver.findElement(By.name("username"));
element.sendKeys("pekka");
element = driver.findElement(By.name("password"));
element.sendKeys("akkep");
element = driver.findElement(By.name("login"));
element.submit();
driver.quit();
}
Avaa toinen terminaali ja suorita siellä komento gradle browse, joka on konfiguroitu suorittamaan luokan Tester metodin main koodi.
HUOM: osalla on ollut ongelmia Seleniumin kanssa. Tänne on koottu joitain tapoja, miten ongelmia on saatu ratkaistua. Jos törmäät ongelmaan ja saat sen ratkaistua jollain em. dokumentissa mainitsemattomalla tavalla, lisää ohje dokumenttiin.
Seuraa avautuvasta selaimesta mitä tapahtuu.
Tester-ohjelmassa luodaan alussa selainta koodista käsin käyttävä olio WebDriver driver. Tämän jälkeen mennään selaimella osoitteeseen localhost:4567.
Kahden sekunnin odottelun jälkeen haetaan sivulta elementti, jossa on linkkiteksti login ja linkkiä klikataan:
WebElement element = driver.findElement(By.linkText("login"));
element.click();
Seuraavaksi etsitään sivulta elementti, jonka nimi on username, kyseessä on lomakkeen input-kenttä, ja ohjelma "kirjoittaa" kenttään metodia sendKeys()
käyttäen nimen "pekka"
element = driver.findElement(By.name("username"));
element.sendKeys("pekka");
Mistä tiedetään, miten lomakkeen elementti tulee etsiä, eli miksi sen nimi oli nyt username? Elementin nimi on määritelty tiedostossa src/main/resources/templates/login.html:
![]({{ "/images/lh3-3.png" | absolute_url }}){:height="250px" }
Tämän jälkeen täytetään vielä salasanakenttä ja painetaan lomakkeessa olevaa nappia.
Ohjelma siis simuloi selaimen käyttöskenaarion, jossa kirjaudutaan sovellukseen.
Koodin seassa on kutsuttu sopivissa paikoin metodia sleep, joka hidastaa selainsimulaation etenemistä siten, että ihminenkin pystyy seuraamaan tapahtumia.
Muuta nyt koodia siten, että läpikäyt seuraavat skenaariot
- epäonnistunut kirjautuminen: oikea käyttäjätunnus, väärä salasana
- uuden käyttäjätunnuksen luominen
- uuden käyttäjätunnuksen luomisen jälkeen tapahtuva ulkoskirjautuminen sovelluksesta
HUOM1: voit tehdä skenaariot yksi kerrallaan, kaiken main-metodiin, siten että laitat esim. kommentteihin muiden skenaarioiden koodin kun suoritat yhtä skernaariota
HUOM2: salasanan varmistuskentän (confirm password) nimi on passwordConfirmation
HUOM3:
Uuden käyttäjän luomisen kokeilua hankaloittaa se, että käyttäjänimen on oltava uniikki. Kannattanee generoida koodissa satunnaisia käyttäjänimiä esim. seuraavasti:
Random r = new Random();
element = driver.findElement(By.name("username"));
element.sendKeys("arto"+r.nextInt(100000));
HUOM3:
Joskus linkin klikkaaminen Seleniumissa aiheuttaa poikkeuksen StaleElementReferenceException
Käytännössä syynä on se, että Selenium yrittää klikata linkkiä "liian aikaisin". Ongelma on mahdollista kiertää klikkaamalla poikkeuksen tapahtuessa linkkiä uudelleen. Jos törmäät ongelmaan, voit ottaa koodiisi seuraavassa olevan apumetodin clickLinkWithText, joka suorittaa sopivan määrän uudelleenklikkauksia:
public class Tester {
public static void main(String[] args) {
WebDriver driver = new ChromeDriver();
driver.get("http://localhost:4567");
clickLinkWithText("register new user", driver);
// ...
}
private static void clickLinkWithText(String text, WebDriver driver) {
int trials = 0;
while( trials++<5 ) {
try{
WebElement element = driver.findElement(By.linkText(text));
element.click();
break;
} catch(Exception e) {
System.out.println(e.getStackTrace());
}
}
}
Lisää asiasta esimerkiksi täällä.
Tehdään nyt sovellukselle hyväksymätestejä Cucumberilla.
Projektissa on valmiina User storystä As a registered user can log in with valid username/password-combination kaksi eri feature-määrittelyä:
- logging_in.feature ja
- logging_in_antipattern.feature
Näistä ensimmäinen, eli logging_in.feature on tehty "hyvien käytäntöjen" mukaan ja jälkimmäinen eli logging_in_antipattern.feature on taas huonompi.
Huonommassa versiossa skenaarioiden stepeistä on tehty monikäyttöisemmät. Sekä onnistuneet että epäonnistuneen skenaariot käyttävät samoja steppejä ja eroavat ainoastaan parametreiltaan:
Feature: As a registered user can log in with valid username/password-combination
Scenario: user can login with correct password
Given login is selected
When username "jukka" and password "akkuj" are given
Then system will respond "Ohtu Application main page"
Scenario: user can not login with incorrect password
Given login is selected
When username "jukka" and password "wrong" are given
Then system will respond "invalid username or password"
Paremmassa versiossa taas stepit ovat erilaiset, paremmin tilannetta kuvaavat:
Feature: As a registered user can log in with valid username/password-combination
Scenario: user can login with correct password
Given login is selected
When correct username "jukka" and password "akkuj" are given
Then user is logged in
Scenario: user can not login with incorrect password
Given login is selected
When correct username "jukka" and incorrect password "wrong" are given
Then user is not logged in and error message is given
Tästä seurauksena on se, että stepit mappaavia metodeja tulee suurempi määrä. Metodit kannattaakin määritellä siten, että ne kutsuvat testejä varten määriteltyjä apumetodeita, jotta koodiin ei tule turhaa toistoa:
@When("correct username {string} and password {string} are given")
public void correctUsernameAndPasswordAreGiven(String username, String password) {
logInWith(username, password);
}
@When("correct username {string} and incorrect password {string} are given")
public void correctUsernameAndIncorrectPasswordAreGiven(String username, String password) {
logInWith(username, password);
}
private void logInWith(String username, String password) {
assertTrue(driver.getPageSource().contains("Give your credentials to login"));
WebElement element = driver.findElement(By.name("username"));
element.sendKeys(username);
element = driver.findElement(By.name("password"));
element.sendKeys(password);
element = driver.findElement(By.name("login"));
element.submit();
}
Vaikka siis kuvaavammin kirjoitetut stepit johtavatkin hieman suurempaan määrään mappayksestä huolehtivaa koodia, on stepit syytä kirjata mahdollisimman kuvaavasti ja huolehtia detaljeista mappaavan koodin puolella. Stepit mappaavien eri metodien samankaltainen koodi kannattaa ehdottomasti eriyttää omiin apumetodeihin, kuten esimerkissäkin tapahtuu (metodit logInWith ja pageHasContent).
Testien konfiguraatioon liittyy vielä muutama detalji. Testit alustava luokka RunCucumberTest
on nyt seuraava:
@RunWith(Cucumber.class)
@CucumberOptions(
plugin = "pretty",
features = "src/test/resources/ohtu",
snippets = SnippetType.CAMELCASE
)
public class RunCucumberTest {
@ClassRule
public static ServerRule server = new ServerRule(4567);
}
Luokka määrittelee testeille ClassRule:n, eli joukon toimenpiteitä, jotka suoritetaan ennen kuin testien suoritus aloitetaan ja kun testien suoritus on ohi. Toimenpiteet määrittelevä luokka ServerRule
näyttää seuraavalta:
public class ServerRule extends ExternalResource {
private final int port;
public ServerRule(int port) {
this.port = port;
}
@Override
protected void before() throws Throwable {
Spark.port(port);
UserDao dao = new UserDaoForTests();
dao.add(new User("jukka", "akkuj"));
Main.setDao(dao);
Main.main(null);
}
@Override
protected void after() {
Spark.stop();
}
}
Metodi before
käynnistää web-sovelluksen ennen testien suorittamista (komento Main.main(null)). Web-sovellukselle myös injektoidaan testejä varten tehty UserDao-olio, jolle on lisätty yksi käyttäjä- ja salasana.
Metodi after
sulkee web-sovelluksen testien päätteeksi.
Suorita nyt testit komennolla gradle test
Huomaa, että testit käynnistävät sovelluksen samaan porttiin kuin sovellus käynnistyy komennolla gradle run
. Jos saat virheilmoituksen:
FAILURE: Build failed with an exception. * What went wrong: Execution failed for task ':test'. > Process 'Gradle Test Executor 6' finished with non-zero exit value 100 This problem might be caused by incorrect test process configuration. Please refer to the test execution section in the User Manual at https://docs.gradle.org/5.6.3/userguide/java_testing.html#sec:test_execution * Try: Run with --stacktrace option to get the stack trace. Run with --debug option to get more log output. Run with --scan to get full insights.
syynä on todennäköisesti se, että sovellus on päällä. Joudutkin sulkemaan sovelluksen testien suorittamisen ajaksi.
Jos Seleniumin kanssa oli ongelmia ja käytit HtmlUnitDriveria niin määrittele luokassa Stepdefs driver kentäksi new HtmlUnitDriver();
Jos haluat pitää sovelluksen päällä testatessasi, käynnistä se johonkin muuhun portiin, esim. komento
PORT=4569 gradle run
käynnistää sovelluksen porttiin 4569.
Voit nyt halutessasi poistaa testien huonon version eli tiedoston logging_in_antipattern.feature ja siihen liittyvät Java-stepit.
Lisää User storylle User can log in with valid username/password-combination seuraava skenaario ja määrittele sille sopivat When ja Then -stepit:
Scenario: nonexistent user can not login to Given login is selected When ... Then ...
Tee steppien nimistä kuvaavasti nimettyjä.
Protip jos et saa jotain testiä menemään läpi, kannattaa "pysäyttää" testin suoritus ongelmalliseen paikkaan lisäämällä stepin koodiin esim. rivi
try{ Thread.sleep(120000); } catch(Exception e){} // suoritus pysähtyy 120 sekunniksi
ja tarkastella sitten ohjelman tilaa testin käyttämästä selaimesta.
HUOM: konsoliin tulostuu seuraava STANDARD_ERROR jota ei tarvitse välittää:
ohtu.RunCucumberTest STANDARD_ERROR SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder". SLF4J: Defaulting to no-operation (NOP) logger implementation SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.
HUOM: saat testien suorituksen huomattavasti nopeammaksi käyttämällä ChromeDriverin sijaan HtmlUnitDriver:iä joka ns. headless- eli käyttöliittymätön selain.
HtmlUnitDriver vaikeuttaa testien debuggaamista, joten jos jotain ongelmia ilmenee, kannattanee debuggaamiseen käyttää ChromeDriveriä.
Tee User storylle A new user account can be created if a proper unused username and a proper password are given seuraavat skenaariot ja niille sopivat stepit:
Feature: A new user account can be created if a proper unused username and password are given Scenario: creation is successful with valid username and password Given command new user is selected When a valid username "liisa" and password "salainen1" and matching password confirmation are entered Then a new user is created Scenario: creation fails with too short username and valid password Given command new user is selected When ... Then user is not created and error "username should have at least 3 characters" is reported Scenario: creation fails with correct username and too short password Given command new user is selected When ... Then user is not created and error "password should have at least 8 characters" is reported Scenario: creation fails when password and password confirmation do not match Given command new user is selected When ... Then user is not created and error "password and password confirmation do not match" is reported
Käyttäjätunnus ja salasana noudattavat samoja sääntöjä kuin tehtävässä 7 eli
- käyttäjätunnuksen on oltava merkeistä a-z koostuva vähintään 3 merkin pituinen merkkijono, joka ei ole vielä käytössä
- salasanan on oltava pituudeltaan vähintään 8 merkkiä ja se ei saa koostua pelkästään kirjaimista
Laajenna koodiasi siten, että testit menevät läpi.
Tee User storylle A new user account can be created if a proper unused username and a proper password are given vielä seuraavat skenaariot ja niille sopivat stepit:
Scenario: user can login with successfully generated account Given user with username "lea" with password "salainen1" is successfully created And login is selected When ... Then ... Scenario: user can not login with account that is not successfully created Given user with username "aa" and password "bad" is tried to be created And login is selected When ... Then ...
{% include submission_instructions.md %}