*Allaolevien tehtävien deadline on *

Apua tehtävien tekoon kurssin Discord-kanavalla, kampuksella ja Zoomissa:

  • Maanantaisin (poislukien 6.12.) klo 14-16 kampuksella BK107-luokassa
  • Torstaisin (4.11. alkaen) klo 14-16 Zoomissa

Ohjauskalenteri:

Muista myös tämän viikon monivalintatehtävät, joiden deadline on .

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.

Typoja tai epäselvyyksiä tehtävissä?

Tee korjausehdotus editoimalla tätä tiedostoa GitHubissa.

Tehtävien palauttaminen

Tehtävät palautetaan GitHubiin, sekä merkitsemällä tehdyt tehtävät palautussovellukseen https://study.cs.helsinki.fi/stats/courses/ohtu-avoin-2022

Katso tarkempi ohje palautusrepositorioita koskien täältä.

Huomio gradleen liittyen

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ä.

1. lisää gradlea: riippuvuuksien lisääminen

Hae kurssirepositorion https://github.com/ohjelmistotuotanto-hy-avoin/java-kevat-2022 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 edellisen kauden NHL-liigan tilastotietoja.

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:

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.

2. siistimpi pelaajalista

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
...

3. lisää gradlea: jar joka sisältää kaikki riippuvuudet

  • 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ä.

4. Tutustuminen cucumberiin

Lue täällä oleva Cucumber-johdanto ja tee siihen liittyvät tehtävät.

5. Kirjautumisen testit

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.

6. Uuden käyttäjän rekisteröitymisen testit

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.

Cucumber-testien debuggaaminen

On todennäköistä että testien tekemisen aikana tulee ongelmia, joiden selvittäminen ei ole triviaalia.

Suoritettavien testien lukumäärän rajoittaminen

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.

println-debuggaus

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));
}    

7. WebLogin

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

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.

8. Selenium, eli web-selaimen simulointi ohjelmakoodista

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:

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ä.

9. Web-sovelluksen testaaminen: Cucumber+Selenium

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.

10. Web-sovelluksen testaaminen osa 2

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.

11. Web-sovelluksen testaaminen osa 3

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  ...  

Tehtävien palautus

Pushaa kaikki tekemäsi tehtävät (paitsi ne, joissa mainitaan, että tehtävää ei palauteta mihinkään) GitHubiin ja merkkaa tekemäsi tehtävät palautussovellukseen https://study.cs.helsinki.fi/stats/courses/ohtu-avoin-2022