Apua tehtävien tekoon kurssin Discord-kanavalla sekä zoom-pajassa:

  • Torstaisin klo 14-16 tällä linkillä

Tehtävissä 1-2 tutustutaan siihen, miten Poetry-sovelluksiin lisätään ulkoisia kirjastoja riippuvuudeksi. Loput tehtävät liittyvät storyjen hyväksymistestauksen automatisointiin tarkoitetun Robot Frameworkin, 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ä.

1. Pelaajalista

Hae kurssirepositorion hakemistossa koodi/viikko3/nhl-reader lähes tyhjä Poetry-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. Tarvitset ohjelmassa yhtä kirjastoa, eli riippuvuutta. Kyseinen kirjasto on requests-kirjasto, jonka avulla voi tehdä HTTP-pyyntöjä. Huomaa, että Pythonilla on myös valmiita moduuleja tähän tarkoitukseen, mutta requests-kirjaston käyttö on huomattavasti näitä moduuleja helpompaa.

Kertaa nopeasti Ohjelmistotekniikka-kurssin Poetry-ohjeesta, miten Poetrylla asennetaan riippuvuuksia. Asenna sen jälkeen requests-kirjasto projektin riippuvuuksiksi. Käytä kirjastosta uusinta versiota (jonka Poetry asentaa automaattisesti).

Voit ottaa projektisi pohjaksi seuraavan tiedoston:

import requests
from player import Player

def main():
    url = "https://nhlstatisticsforohtu.herokuapp.com/players"
    response = requests.get(url).json()

    print("JSON-muotoinen vastaus:")
    print(response)

    players = []

    for player_dict in response:
        player = Player(
            player_dict['name']
        )

        players.append(player)

    print("Oliot:")

    for player in players:
        print(player)

Tehtäväpohjassa on valmiina luokan Player koodin runko. Edellä esitetyssä koodissa requests.get(url) tekee HTTP-pyynnön, jonka jälkeen json-metodin kutsu muuttaa JSON-muotoisen vastauksen Python-tietorakenteiksi. Tässä tilanteessa response sisältää listan dictionaryja. Tästä listasta muodostetaan lista Player-olioita for-silmukan avulla.

Tee Player-luokkaan attribuutit kaikille JSON-datassa oleville kentille, joita ohjelmasi tarvitsee. Ohjelmasi voi toimia esimerkiksi niin, että se tulostaisi pelaajat seuraavalla tavalla:

Players from FIN 2021-01-04 19:15:32.858661

Sami Vatanen team CAR  goals 5 assists 18
Janne Kuokkanen team NJD  goals 0 assists 0
Leo Komarov team NYI  goals 4 assists 10
Otto Koivula team NYI  goals 0 assists 0
Kaapo Kakko team NYR  goals 10 assists 13
Juuso Riikola team PIT  goals 1 assists 6
Urho Vaakanainen team BOS  goals 0 assists 0
Tuukka Rask team BOS  goals 0 assists 0
Rasmus Ristolainen team BUF  goals 6 assists 27
...

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 2021-01-04 19:19:40.026464

Sebastian Aho        CAR 38 + 28 = 66
Patrik Laine         WPG 28 + 35 = 63
Teuvo Teravainen     CAR 15 + 48 = 63
Aleksander Barkov    FLA 20 + 42 = 62
Mikko Rantanen       COL 19 + 22 = 41
Kasperi Kapanen      TOR 13 + 23 = 36
Miro Heiskanen       DAL  8 + 27 = 35
Roope Hintz          DAL 19 + 14 = 33
Joonas Donskoi       COL 16 + 17 = 33
Rasmus Ristolainen   BUF  6 + 27 = 33
Mikael Granlund      NSH 17 + 13 = 30
Joel Armia           MTL 16 + 14 = 30
...
  • Vinkki 1: voit halutessasi hyödyntää filter-funktiota.
  • Vinkki 2: kokeile, mitä f"{self.name:20}" tekee merkkijonoesitykselle Player-luokan __str__-metodissa. Mitä :20 koodissa tekee? Numeroarvot tulee muuttaa merkkijonomuotoisiksi, jotta lopputulos on oikea. Esimerkiksi f"{str(self.goals):2}".

3. Pelaajalistan refaktorointi

Tällä hetkellä suurin osa pelaajatietoihin liittyvästä koodista on luultavasti main-funktiossa. Funktion koheesion aste on melko matala, koska se keskittyy usean toiminnallisuuden toteuttamiseen. Koodi kaipaisi siis pientä refaktorointia.

Jaa toiminnallisuuden vastuut kahdelle luokalle: PlayerReader ja PlayerStats. PlayerReader-luokan vastuulla on hakea JSON-muotoiset pelaajat konstruktorin parametrin kautta annetusta osoitteesta ja muodostaa niistä Player-olioita. Tämä voi tapahtua esimerkiksi luokan get_players-metodissa. PlayerStats-luokan vastuulla on muodostaa PlayerReader-luokan tarjoamien pelaajien perusteella erilaisia tilastoja. Tässä tehtävässä riittää, että luokalla on metodi top_scorers_by_nationality, joka palauttaa parametrina annetun kansalaisuuden pelaajat pisteiden mukaan laskevassa järjestyksessä (suurin pistemäärä ensin).

Refaktoroinnin jälkeen main-funktion tulee näyttää suurin piirtein seuraavalta:

def main():
    url = "https://nhlstatisticsforohtu.herokuapp.com/players"
    reader = PlayerReader(url)
    stats = PlayerStats(reader)
    players = stats.top_scorers_by_nationality("FIN")

    for player in players:
        print(player)

Funktion pitäisi tulostaa samat pelaajat samassa järjestyksessä kuin edellisessä tehtävässä.

4. Tutustuminen Robot Frameworkkiin

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

5. Kirjautumisen testit

Hae kurssirepositorion hakemistossa koodi/viikko3/login-robot oleva projekti.

Tutustu ohjelman rakenteeseen. Huomaa, että ohjelman UserService-olio ei tallenna suoraan User-oliota vaan epäsuorasti UserRepository-luokan olion kautta. Mistä on kysymys?

Sovelluksen käyttämään tietoon kohdistuvien operaatioiden abstrahointiin sovelluslogiikasta löytyy useita suunnittelumalleja, kuten Data Access Object, Active Record ja Repository. Kaikkien näiden suunnittelumallien perimmäinen idea on siinä, että sovelluslogiikalta tulee piilottaa tietoon kohdistuvien operaatioiden yksityiskohdat.

Esimerkiksi repositorio-suunnittelumallissa tämä tarkoittaa sitä, että tietokohteeseen kohdistetaan operaatioita erilaisten funktioiden tai metodien kautta, kuten find_all, create ja delete. Tämän abstraktion avulla sovelluslogiikka ei ole tietoinen operaatioiden yksityiskohdista, jolloin esimerkiksi tallennustapaa voidaan helposti muuttaa.

Sovellukseen on määritelty repositorio-suunnittelumallin mukainen luokka UserRepository. Luokka tallentaa sovelluksen käyttäjiä muistiin. Jos päättäisimme tallentaa käyttäjät esimerkiksi SQLite-tietokantaan, ei tämä vaatisi muutoksia luokan ulkopuolelle.

Asenna projektin riippuvuudet ja kokeile suorittaa index.py-tiedosto. Ohjelman tuntemat komennot ovat login ja new. Suorita myös projektiin siihen liittyvät Robot Framework -testit virtuaaliympäristössä komennolla robot src/tests.

Tutki miten Robot Framework -testit on toteutettu hakemistossa src/tests. Tutki myös, miten avainsanat on määritelty src-hakemiston AppLibrary.py-tiedoston AppLibrary-luokassa. Huomioi erityisesti, miten testit käyttävät testaamisen mahdollistavaa StubIO-oliota 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ä.

Saatat löytää .robot-tiedostoista ennestään tuntemattomia ominaisuuksia. resource.robot-tiedossa on määritelty avainsana Input Credentials, jolla on argumentit username ja password:

Input Credentials
    [Arguments]  ${username}  ${password}
    Input  ${username}
    Input  ${password}
    Run Application

Kyseinen avainsana on käytössä login.robot-tiedostossa seuraavasti:

Input Credentials  kalle  kalle123

Lisäksi login.robot-tiedoston *** Settings ***-osiossa on uusi asetus, Test Setup. Kyseisen asetuksen avulla voimme määritellä avainsanan, joka suoritetaan ennen jokaista testitapausta. Tässä tapauksessa ennen jokaista testiä halutaan suorittaa avainsana Create User And Input Login Command, joka luo uuden käyttäjän ja antaa sovellukselle login-komennon.

Toteuta user storylle User can log in with valid username/password-combination seuraavat testitapaukset login.robot-tiedostoon:

*** Test Cases ***
Login With Incorrect Password
# ...

Login With Nonexistent Username
# ...

Suorita testitapauksissa sopivat avainsanat, jotta haluttu tapaus tulee testattua.

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

Lisää testihakemistoon uusi testitiedosto register.robot. Toteuta tiedostoon user storylle A new user account can be created if a proper unused username and a proper password are given seuraavat testitapaukset:

*** Test Cases ***
Register With Valid Username And Password
# ...

Register With Already Taken Username And Valid Password
# ...

Register With Too Short Username And Valid Password
# ...

Register With Valid Username And Too Short Password
# ...

Register With Valid Username And Long Enough Password Containing Only Letters
# ...

Säännöllisissä lausekkeissa voi hyödyntää Pythonin re-moduulia seuraavasti:

import re

if re.match("^[a-z]+$", "kalle"):
  print("Ok")
else:
  print("Virheellinen")

Tee testitapauksista suoritettavia ja täydennä ohjelmaa siten että testit menevät läpi. Oikea paikka koodiin tuleville muutoksille on src/services/user_service.py-tiedoston UserService-luokan metodi validate.

HUOM 1: Testitapaukset kannattaa toteuttaa yksi kerrallaan, laittaen samalla vastaava ominaisuus ohjelmasta kuntoon. Eli ÄLÄ copypastea ylläolevaa kerrallaan tiedostoon, vaan etene pienin askelin. Jos yksi testitapaus ei mene läpi, älä aloita uuden tekemistä ennen kuin kaikki ongelmat on selvitetty. Seuraava luku antaa muutaman vihjeen testien debuggaamiseen.

HUOM 2: Saattaa olla hyödyllistä toteuttaa resource.robot-tiedostoon avainsana Input New Command ja register.robot-tiedostoon avainsana Input New Command And Create User, joka antaa sovellukselle new-komennon ja luo käyttäjän testejä varten. Avainsana kannattaa suorittaa ennen jokaista testitapausta hyödyntämällä Test Setup-asetusta.

Robot Framework -testien debuggaaminen

On todennäköistä että testien tekemisen aikana tulee ongelmia, joiden selvittäminen ei ole triviaalia. Epäonnistuneen testitapauksen kohdalla kannattaa miettiä mahdollisia syitä:

  • Onko vika testissä, eli toimiiko sovellus kuten pitääkin? Voit esimerkiksi testata sovelluksen toimivuuden manuaalisesti. Jos näin on, keskity testin korjaamiseen
  • Onko vika sovelluksessa, eli eikö manuaalisesti testattu sovellus toimi kuten pitäisi? Jos näin on, keskity tarkastelemaan ohjelman suoritusta epäonnistuneessa testitapauksessa

Tutustutaan seuraavaksi tekniikoihin, jotka helpottavat ja nopeuttavat virheiden metsästystä.

Suoritettavien testien lukumäärän rajoittaminen

Kun kohtaat epäonnistuvan testitapauksen, kannattaa testien suorittamista nopeuttaa suorittamalla vain epäonnistunut testitapaus. Jos testitapaus Login With Correct Credentials, voimme suorittaa ainoastaan sen seuraavalla komennolla:

robot -t "Login With Correct Credentials" src/tests/login.robot

Komennolle robot annetaan siis -t-optionin kautta suoritettavan testitapauksen nimi ja tiedosto, jossa testitapaus sijaitsee.

Ohjelman suorituksen seuraaminen

Jos virheen löytäminen pelkän manuaalisen testauksen avulla ei tuota tulosta, kannattaa alkaa tutkimaan miten ohjelman suoritus etenee. Ensin on jollain tavalla rajattava, missä ongelma saattaisi olla. Jos esimerkiksi Login With Correct Credentials-testitapaus epäonnistuu, on ongelma luultavasti UserService-luokan metodissa check_credentials. Voimme pysäyttää ohjelman suorituksen halutulle riville hyödyntämällä pdb-moduulia:

# ...
# debugattavaan tiedostoon tulee tuoda tarvittavat moduulit
import sys, pdb

class UserService:
    def __init__(self, user_repository):
        self._user_repository = user_repository

    def check_credentials(self, username, password):
        # pysäytetään ohjelman suoritus tälle riville
        pdb.Pdb(stdout=sys.__stdout__).set_trace()

        if not username or not password:
            raise UserInputError("Username and password are required")

        user = self._user_repository.find_by_username(username)

        if not user or user.password != password:
            raise AuthenticationError("Invalid username or password")

        return user

    # ...

Ohjelman suorituksen pysäyttäminen onnistuu siis kutsumalla Pdb-luokan metodia set_trace. Jotta tulosteet tulisivat näkyviin testien suorituksen aikana, tulee luokan konstruktorin stdout argumentin arvoksi asettaa sys.__stdout__. Tätä varten debugattavaan tiedostoon tulee tuoda pdb-moduulin lisäksi sys-moduuli, joka tapahtuu esimerkissä import sys, pdb-rivillä.

Käynnistä nyt ohjelma uudelleen, jotta muutokset koodiin astuvat voimaan. Suorita sen jälkeen pelkästään Login With Correct Credentials-testitapaus edellä mainitun ohjeen mukaisesti. Kun testitapauksen suoritus saavuttaa check_credentials-metodin kutsun, koodin suoritus pysähtyy ja palvelinta suorittavalle komentoriville ilmestyy seuraavanlainen komentorivi:

-> if not username or not password:
(Pdb)

Kyseessä on interaktiivinen komentorivi, jossa voimme suorittaa koodia. Nuoli (->) viittaa seuraavaksi suoritettavaan koodiriivin. Katsotaan komentorivin avulla, mitkä ovat muuttujien username ja password arvot:

(Pdb) username
'kalle'
(Pdb) password
'kalle123'
(Pdb)

Annamme siis komentoriville syötteen ja painamme Enter-painiketta. Jatketaan koodin suorittamista antamalla syöte next(). Koodi on ohittanut if-lauseen (koska muuttujilla oli arvot) ja on seuraavaksi suorittamassa riviä user = self._user_repository.find_by_username(username):

-> user = self._user_repository.find_by_username(username)
(Pdb)

Suoritetaan rivi syöttämällä uudestaan next() ja tulostetaan user-muuttujan arvo:

-> if not user or user.password != password:
(Pdb) user
<entities.user.User object at 0x10f7a55e0>

Kun olet lopettanut debuggaamiseen, syötä exit() ja poista koodista set_trace-metodin kutsu.

7. WebLogin

Tarkastellaan edellisestä tehtävästä tutun toiminnallisuuden tarjoamaa esimerkkiprojektia, joka löytyy kurssirepositorion hakemistossa koodi/viikko3/web-login-robot oleva projekti. Sovellus on toteutettu Flask-nimisellä minimalistisella web-sovelluskehyksellä.

Hae projekti, asenna sen riippuvuudet komennollla poetry install ja käynnistä se virtuaaliympäristössä komennolla python3 src/index.py. Sovelluksen käynnistymisen jälkeen pääset käyttämään sitä avaamalla selaimella osoitteen http://localhost:5000

Sovellus siis toimii localhostilla eli paikallisella koneellasi portissa 5000.

Sovelluksen rakenne on suunnilleen sama kuin tehtävien 4-5 ohjelmassa. Poikkeuksen muodostaa pääohjelma, joka käsittelee 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:5000 tulevat pyynnöt käsittelee mainista seuraava koodinpätkä:

@app.route("/")
def render_home():
    return render_template("index.html")

Koodi muodostaa Jinja-kirjaston avulla src/templates/index.html-tiedostosta löytyvästä sivupohjasta HTML-muotoisen sivun ja palauttaa sen käyttäjän selaimelle.

Sivupohja näyttää seuraavalta:

{% extends "layout.html" %}
{% block title %} Ohtu Application {% endblock %}
{% block body %}
<h1>Ohtu Application</h1>

<ul>
  <li><a href="/login">Login</a></li>
  <li><a href="/register">Register new user</a></li>
</ul>
{% endblock %}

Kaikki GET-alkuiset määrittelyt ovat samanlaisia, ne ainoastaan muodostavat HTML-sivun (joiden sisällön määrittelevät sivupohjat sijaitsevat hakemistossa src/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:

@app.route("/login", methods=["POST"])
def handle_login():
    username = request.form.get("username")
    password = request.form.get("password")

    try:
        user_service.check_credentials(username, password)
        return redirect_to_ohtu()
    except Exception as error:
        flash(str(error))
        return redirect_to_login()

Koodi pääsee käsiksi käyttäjän lomakkeen avulla lähettämiin tietoihin request-olion kautta:

username = request.form.get("username")
password = request.form.get("password")

Koodi tarkistaa käyttäjätunnuksen ja salasan oikeellisuuden kutsumalla UserService-luokan metodia check_credentials. Jos kirjautuminen onnistuu, ohjataan käyttäjä “/ohtu”-polun sivulle. Jos se epäonnistuu, check_credentials-metodi nostaa virheen, jonka käsittelemme except-lohkossa ohjaamalla käyttäjän “/login”-polun sivulle ja näyttämällä siellä virheilmoituksena virheen sisältämän viestin.

Tutustu nyt sovelluksen rakenteeseen ja toiminnallisuuteen. Saat sammutettua sovelluksen painamalla komentoriviltä ctrl+c tai ctrl+d.

8. Web-sovelluksen testaaminen osa 1

Jatketaan saman sovelluksen parissa.

Käynnistä web-sovellus 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. Seleniumin käyttö Robot Framework -testeissä onnistuu valmiin SeleniumLibrary-kirjaston avulla.

Jotta selainta käyttävien testien suorittaminen on mahdollista, täytyy lisäksi asentaa halutun selaimen ajuri. Projektin testit käyttävät Chrome-selainta, jolla testejä voi suorittaa käyttämällä ChromeDriver-ajuria. Ennen kuin siirrymme testien pariin, asenna ChromeDriver seuraamalla tätä ohjetta.

ChromeDriverin kanssa voi käyttää myös Chromium-selainta, mutta se vaatii yhden lisäaskeleen testitiedostossa. Vaadittava lisäys mainitaan alempana.

Kun Chrome-ajuri on asennettu onnistuneesti, avaa uusi terminaali-ikkuna ja suorita projektin testit virtuaaliympäristössä komennolla robot src/tests. Huomaa, että web-sovelluksen tulee olla käynnissä toisessa terminaali-ikkunassa. Komennon pitäisi suorittaa onnistuneesti kaksi testitapausta, Login With Correct Credentials ja Login With Incorrect Password. Testitapausten suoritusta voi seurata aukeavasta Chrome-selaimen ikkunasta.

HUOM: Windows 10 / WSL2 -käyttäjänä saatat törmätä seuraavaan virheilmoitukseen:

Suite setup failed:
WebDriverException: Message: unknown error: Chrome failed to start: crashed.
  (unknown error: DevToolsActivePort file doesn't exist)
  (The process started from chrome location /usr/bin/google-chrome is no longer running, so ChromeDriver is assuming that Chrome has crashed.)

Tämä ohje saattaa tuoda ratkaisun.

HUOM2: seuraava virheilmoitus kertoo siitä, että suoritat testejä ilman että sovellus on päällä:

[ ERROR ] Error in file '/.../viikko3/web-login-robot/src/tests/resource.robot' 
on line 3: Initializing library 'AppLibrary' with no arguments failed: 
ConnectionError: HTTPConnectionPool(host='localhost', port=5000): 
Max retries exceeded with url: /tests/reset (Caused by NewConnectionError('<urllib3.connection.HTTPConnection object at 0x7f459e7c4280>: 
Failed to establish a new connection: [Errno 111] Connection refused'))

Testit siis olettavat, että sovellus on käynnissä. Käynnistä siis sovellus yhteen terminaaliin, avaa uusi ja suorita testit siellä.

Tutustutaan aluksi testitapauksien yhteisiin asetuksiin ja avainsanoihin, jotka löytyvät src/tests/resource.robot-tiedostosta. Tiedoston sisältö on seuraava:

*** Settings ***
Library  SeleniumLibrary
Library  ../AppLibrary.py

*** Variables ***
${SERVER}  localhost:5000
${BROWSER}  chrome
${DELAY}  0.5 seconds
${HOME URL}  http://${SERVER}
${LOGIN URL}  http://${SERVER}/login
${REGISTER URL}  http://${SERVER}/register

*** Keywords ***
Open And Configure Browser
    Open Browser  browser=${BROWSER}
    Maximize Browser Window
    Set Selenium Speed  ${DELAY}

Login Page Should Be Open
    Title Should Be  Login

Main Page Should Be Open
    Title Should Be  Ohtu Application main page

Go To Login Page
    Go To  ${LOGIN URL}

*** Settings *** osiossa on käytössä projektin oma AppLibrary.py-kirjasto sekä edellä mainittu SeleniumLibrary-kirjasto. SeleniumLibrary-kirjasto tuo mukaan lukuisia uusia avainsanoja, joista kaikki on dokumentoitu täällä.

Tiedostossa on myös ennestään tuntematon *** Variables ***-osio. Kuten osion nimi kertoo, voimme määritellä osion sisällä muuttujia, jotka ovat kaikkien avainsanojen käytössä. Huomaa, että osion alla määritellyt muuttujat kirjoitetaan isoilla kirjaimilla, toisin kuin argumentit. Muuttujia kannattaa suosia aina kovakoodattujen arvojen sijaan.

*** Keywords ***-osiossa on määritelty yleiskäyttöisiä avainsanoja:

  • Open And Configure Browser -avainsana käynnistää selaimen käyttämällä SeleniumLibrary-kirjaston Open Browser -avainsanaa antaen browser-argumentin arvoksi BROWSER-muuttujan arvon, joka on chrome. Lisäksi avainsana asettaa viiveeksi Selenium-komentojen välille DELAY-muuttujan arvon käyttämällä Set Selenium Speed -avainsanaa. Pidempi viive helpottaa testien suorituksen seuraamista
    • Jos haluat käyttää Chromiumia Chromen sijaan, tähän kohtaan pitää lisätä sopiva options-osio: Open Browser browser=${BROWSER} options=binary_location="/polku/chromiumin/sijaintiin"
  • Login Page Should Be Open - ja Main Page Should Be Open -avainsanojen tarkoitus on tarkistaa, että käyttäjä on oikealla sivulla. Ne käyttävät Title Should Be -avainsanaa, joka tarkistaa HTML-sivun title-elementin arvon. Title-elementin arvon sijaan voisimme esimerkiksi tarkistaa, että sivulta löytyy tietty teksti käyttämällä Page Should Contain -avainsanaa
  • Go To Login Page -avainsana käyttää Go To -avainsanaa avatakseen selaimessa kirjautumis-sivun, jonka URL on tallennettu LOGIN URL-muuttujaan

Tutustutaan seuraavaksi itse testitapauksiin avaamalla tiedosto src/tests/login.robot. Tiedoston *** Settings ***-osio on seuraava:

*** Settings ***
Resource  resource.robot
Suite Setup  Open And Configure Browser
Suite Teardown  Close Browser
Test Setup  Create User And Go To Login Page

Osiossa on käytössä ennestään tuntemattomat Suite Setup-, Suite Teardown- ja Test Setup-asetukset. Niiden merkitykset ovat seuraavat:

  • Suite Setup-asetuksen avulla voimme suorittaa avainsanan ennen tiedoston ensimmäistä testitapausta (Test Setup sen sijaan suoritetaan ennen jokaista testitapausta)
  • Suite Teardown-asetuksen avulla voimme suorittaa avainsanan tiedoston viimeisen testitapauksen jälkeen (Test Teardown sen sijaan suoritetaan jokaisen testitapauksen jälkeen)

Tiedoston *** Keywords *** osiossa on testitapausten käyttämiä avainsanoja:

  • Login Should Succeed -avainsana tarkastaa, että käyttäjä on siirtynyt oikealle sivulle onnistuneen kirjautumisen jälkeen
  • Login Should Fail With Message -avainsana tarkastaa, että käyttäjä on kirjautumissivulla ja että sivulta löytyy tietty virheviesti. Tarkastuksessa käytetään Page Should Contain-avainsanaa, joka tarkistaa, että sivulta löytyy haluttu teksti
  • Submit Credentials -avainsana painaa “Login”-painiketta käyttämällä Click Button-avainsanaa
  • Set Username- ja Set Password -avainsanat syöttävät annetut arvot tiettyihin kenttiin käyttämällä Input Text- ja Input Password-avainsanoja (huomaa, että salasanan kenttä ei ole tavallinen tekstikenttä, vaan salasanakenttä)
  • Create User And Go To Login Page -avainsana luo sovellukseen käyttäjän ja avaa kirjautumis-sivun

Testitapauksissa ollaan interaktiossa erilaisten HTML-elementtien, kuten tekstikenttien ja painikkeiden kanssa. Selenium yrittää löytää elementin annettujen argumenttien perusteella käyttäen tiettyä strategiaa. Esimerkiksi Click Button  foo löytää seuraavat button-elementit:

<button id="foo">Click</button>
<button name="foo">Click</button>
<button>foo</button>

Selenium siis etsii button-elementin, jonka id-attribuutin arvo, name-attribuutin arvo, tai sisältö vastaa annettua argumenttia. Kutsu Click Button  Login löytää siis seuraavan src/templates/login.html-tiedostossa määritellyn painikkeen:

<button>Login</button>

Samalla tavoin kutsu Input Text  username  kalle löytää id-attribuutin avulla seuraavan input-elementin:

<input type="text" name="username" id="username" />

Tee nyt uusi tiedosto home.robot ja lisää sinne seuraavat testitapaukset:

*** Settings ***
Resource  resource.robot
Suite Setup  Open And Configure Browser
Suite Teardown  Close Browser
Test Setup  Go To Home Page

*** Test Cases ***
Click Login Link
    Click Link  Login
    Login Page Should Be Open

Click Register Link
    Click Link  Register new user
    Register Page Should Be Open

Testitapausten tulee siis testata, että “Login”- ja “Register new user”-linkkien painaminen avaa oikean sivun. Linkkien klikkaus tapahtuu käyttämällä valmiiksi määriteltyä Click Link-avainsanaa.

Toteuta testin käyttämät avainsanat tiedostoon resource.robot. Kun suoritat testit, virheilmoitus kertoo mitä avainsanoja on määrittelemättä:

Click Register Link                                                   | FAIL |
Setup failed:
No keyword with name 'Go To Home Page' found.

9. Web-sovelluksen testaaminen osa 2

Jatketaan kirjautumiseen liittyvien hyväksymistestien toteuttamista. Katsotaan sitä ennen pikaisesti, miltä AppLibrary-kirjaston toteutus näyttää. Kirjaston määrittelevä luokka AppLibrary löytyy tiedostosta src/AppLibrary.py, jonka sisältö on seuraava:

import requests


class AppLibrary:
    def __init__(self):
        self._base_url = "http://localhost:5000"

        self.reset_application()

    def reset_application(self):
        requests.post(f"{self._base_url}/tests/reset")

    def create_user(self, username, password):
        data = {
            "username": username,
            "password": password,
            "password_confirmation": password
        }

        requests.post(f"{self._base_url}/register", data=data)

Kirjaston toteutus eroaa jonkin verran edellisestä, komentoriviä hyödyntävän projektin kirjaston toteutuksesta. Erona on, että tässä projektissa testit ja itse sovellus suoritetaan eri prosesseissa, joten testit eivät voi suoraan muuttaa sovelluksen tilaa. Voimme kuitenkin muuttaa sovelluksen tilaa HTTP-kutsujen avulla jo tutuksi tulleen requests-kirjaston avulla.

Metodi reset_application lähettää POST-tyyppisen pyynnön sovelluksen polkuun “/tests/reset”. Pyynnön käsittelee seuraava funktio:

@app.route("/tests/reset", methods=["POST"])
def reset_tests():
    user_repository.delete_all()
    return "Reset"

Funktio poistaa kaikki sovelluksen käyttäjät ja näin nollaa sovelluksen tilan.

Metodi create_user lähettää samankaltaisesti POST-tyyppisen pyynnön sovelluksen polkuun “/register”. Pyynnön käsittelevä funktio luo uuden käyttäjän, jos se on validi:

@app.route("/register", methods=["POST"])
def handle_register():
    username = request.form.get("username")
    password = request.form.get("password")
    password_confirmation = request.form.get("password_confirmation")

    try:
        user_service.create_user(username, password, password_confirmation)
        return redirect_to_welcome()
    except Exception as error:
        flash(str(error))
        return redirect_to_register()

Lisää User storylle User can log in with valid username/password-combination seuraava testitapaus login.robot-tiedostoon:

Login With Nonexistent Username
# ...

10. Web-sovelluksen testaaminen osa 3

Tehdään seuraavaksi pari muutosta testien suorituksen nopeuttamiseksi. Ensiksi, aseta resource.robot-tiedostossa olevan DELAY-muuttujan arvoksi 0. Sen jälkeen, otetaan käyttöön Chrome-selaimen Headless Chrome -variaatio. “Headless”-selainten käyttö on kätevää esimerkiksi automatisoiduissa testeissä, joissa selaimen käyttöliittymä ei ole tarpeellinen. Suorita testit Headless Chromen avulla asettamalla BROWSER-muuttujan arvoksi headlesschrome.

HUOM: Headless Chrome vaikeuttaa testien debuggaamista, koska selaimen käyttöliittymä ei ole näkyvissä. Jos testitapauksen suorittaminen epäonnistuu, projektin juurihakemistoon ilmestyy tiedosto selenium-screenshot-*.png, josta on nähtävissä selainikkunan sisältö virhetilanteen hetkellä. Jos tämä tieto ei riitä, voit muutta debuggaamista varten DELAY- ja BROWSER-muuttujien arvoja.

Tee User storylle A new user account can be created if a proper unused username and a proper password are given seuraavat testitapaukset register.robot-tiedostoon:

Register With Valid Username And Password
# ...

Register With Too Short Username And Valid Password
# ...

Register With Valid Username And Too Short Password
# ...

Register With Nonmatching Password And Password Confirmation
# ...

HUOM tee yksi testitapaus kerrallaan. Testitapausta koodatessa kannattaa suorittaa ainoastaan työn alla olevaa testitapausta täällä olevan ohjeen mukaan, ja kannattanee asettaa headlesschrome:n sijaan chrome muuttujan BROWSER arvoksi jotta näet miten testitapaus etenee.

Käyttäjätunnus ja salasana noudattavat samoja sääntöjä kuin tehtävässä 5, 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. Oikea paikka koodiin tuleville muutoksille on src/services/user_service.py-tiedoston UserService-luokan metodi validate.

Muista käynnistää web-palvelin uudestaan, kun teet muutoksia koodiin! Sammuta palvelin näppäilemällä Ctrl+C terminaali-ikkunaan, jossa web-pavelinta suoritetaan. Käynnistä tämän jälkeen palvelin uudelleen komennolla python3 src/index.py.

11. Web-sovelluksen testaaminen osa 4

Tee User storylle A new user account can be created if a proper unused username and a proper password are given vielä seuraavat testitapaukset register.robot-tiedostoon:

Login After Successful Registration
# ...

Login After Failed Registration
# ...

Ensimmäisessä testitapauksessa tulee testata, että käyttäjä voi kirjautua sisään onnistuneen rekisteröitymisen jälkeen. Toisessa testitapauksessa taas tulee testata, että käyttäjä ei voi kirjautua sisään epäonnistuneen rekisteröitymisen jälkeen.

Vinkki: voit halutessasi toteuttaa login_resource.robot-tiedoston, joka määrittelee kirjautumiseen käytettäviä avainsanoja. Voit hyödyntää tämän tiedoston avainsanoja sekä login.robot-, että register.robot-tiedostossa lisäämällä *** Settings ***-osioon uuden resurssin:

*** Settings ***
Resource  resource.robot
Resource  login_resource.robot

Web-sovelluksen testien suorittaminen CI-palvelimella

HUOM: Seuraava osio ei kuulu tehtäviin, eli siinä esitettyjä esimerkkejä ei tarvitse tehdä mihinkään. Ohjeista saattaa kuitenkin olla hyötyä esimerkiksi kurssin miniprojektissa.

Edellisissä tehtävissä luultavasti käynnistit ensin Flask-palvelimen yhdessä terminaali-ikkunassa, jonka jälkeen suoritit testit toisessa terminaali-ikkunassa. Lopuksi, kun testit oli suoritettu, saatoit sammuttaa palvelimen.

Jotta sovelluksen testit pystyisi suorittamaan CI-palvelimella, tulee nämä vaiheet ilmaista komentorivikomennoilla. Tähän tarkoitukseen, voimme käyttää esimerkiksi seuraavaa bash-skriptiä:

#!/bin/bash

# käynnistetään Flask-palvelin taustalle
poetry run python3 src/index.py &

# odotetaan, että palvelin on valmiina ottamaan vastaan pyyntöjä
while [[ "$(curl -s -o /dev/null -w ''%{http_code}'' localhost:5000/ping)" != "200" ]]; 
  do sleep 1; 
done

# suoritetaan testit
poetry run robot src/e2e

status=$?

# pysäytetään Flask-palvelin portissa 5000
kill $(lsof -t -i:5000)

exit $status

Skriptin voi lisätä esimerkiksi projektin juurihakemiston run_robot_tests.sh-tiedostoon. Tämän jälkeen sen voi suorittaa projektin juurihakemistossa komennolla bash run_robot_tests.sh. Huomaa, että komento käyttää Unix-komentoja, joten sen suorittaminen ei onnistu esimerkiksi Windows-käyttäjärjestelmän tietokoneella ilman asiaan kuuluvaa komentoriviä. CI-palvelimella tämä ei kuitenkaan koidu ongelmaksi, jos valitsemme virtuaalikoneen käyttöjärjestelmäksi esimerkiksi Ubuntun.

Skriptiä voi hyödyntää CI-palvelimella GitHub Actionsin avulla määrittelemällä sen suorittaminen omana askeleena konfiguraatiossa:

name: CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v2
      - name: Set up Python 3.8
        uses: actions/setup-python@v2
        with:
          python-version: 3.8
      - name: Install Poetry
        run: pip install poetry
      - name: Setup chromedriver
        uses: nanasess/setup-chromedriver@master
      - run: |
            export DISPLAY=:99
            chromedriver --url-base=/wd/hub &
            sudo Xvfb -ac :99 -screen 0 1280x1024x24 > /dev/null 2>&1 &
      - name: Install dependencies
        run: poetry install
      - name: Run robot tests
        run: bash run_robot_tests.sh

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