Viikko 3
Tämän viikon tehtävät
Allaolevien tehtävien deadline on maanantai 16.11. klo 23:59
Apua tehtävien tekoon kurssin Discord-kanavalla sekä kampuksella pajassa BK107:
- ma 16-19
- ti 13-16
- to 16-19
- pe 13-16
Liittyminen kurssin Discord-kanavalle tapahtuu komennolla /join course TKT20006 - Ohjelmistotuotanto - ohtu
Muista myös tämän viikon monivalintatehtävät, joiden deadline on sunnuntai 15.11. klo 23:59:00 .
Tehtävät liittyvät storyjen hyväksymistestauksen automatisointiin tarkoitetun Robot Frameworkin. Tehtävässä 8 jatketaan kurssin [tekoäly]-tehtävien sarjaa.
Typoja tai epäselvyyksiä tehtävissä?
Tee korjausehdotus editoimalla tätä tiedostoa GitHubissa.
Kurssipalaute
Kurssilla on käytössä normaalin lopussa kerättävän palautteen lisäksi ns. jatkuva palaute: voit antaa milloin vain kurssihenkilökunnalle anonyymiä palautetta osoitteessa https://norppa.helsinki.fi/targets/112400522/feedback
Ongelmia uv:n kanssa?
Muutamia ohjeita täällä
Tehtävien palauttaminen
Tehtävät palautetaan GitHubiin, sekä merkitsemällä tehdyt tehtävät palautussovellukseen https://study.cs.helsinki.fi/stats/courses/ohtu-2026 välilehdelle “my submission”.
Tehtävät 1 ja 5-9 palautetaan jo edellisillä viikoilla käyttämääsi palautusrepositorioon, tehtävän hakemiston viikko3 sisälle. Tehtävät 2-4 palautetaan omaan, uuteen repositorioon.
Katso tarkempi ohje palautusrepositoriota koskien täältä.
GitHub Education
Viikoilla 2–7 on [tekoäly]-tehtäviä, joissa käytetään GitHubin Copilotia. Suurimman osan tehtävistä voi tehdä ilmaisella Copilot Free -versiolla, mutta sen kuukausikiintiö on rajallinen. Viikon 5 tehtävät edellyttävät opiskelijoille ilmaista Copilot Student -versiota, jonka saat käyttöösi aktivoimalla GitHub Education -jäsenyyden. Jos et ole vielä jäsen, hae jäsenyyttä nyt. Hakemuksen hyväksyminen kestää internetin mukaan jopa viikon.
Checkboxit tehtävien teon seurannan apuna
Kurssin tehtävänannot ovat välillä pitkiä. Tekstin seasta saattaa olla haastavaa bongata niitä askelia, jotka edellyttävät toimenpiteitä. Tehtävien selkeyttämiseksi ja seurannan helpottamiseksi tehtävien toimenpiteitä edellyttävät askeleet on merkitty ranskalaisten viivojen sijaan checkboxeilla.
Voit käyttää checkboxeja oman edistymisesi seurannan helpottamiseen. Kun askel on tehty, rastita boksi. Näin et mene sekaisin sen suhteen, missä kohtaa olet menossa. Rastit tallentuvat selaimesi paikalliseen muistiin (localStorageen), eli ne säilyvät vaikka lataat sivun uudelleen. Rastit eivät siirry toiselle koneelle tai selaimelle, eikä niitä lähetetä minnekään.
1. Tutustuminen Robot Frameworkkiin
Lue täällä oleva Robot Framework -johdanto ja tee siihen liittyvät tehtävät.
2. Web-laskuri
HUOM: jos käytät Dockeria, lue tämä!
Siirrytään seuraavaksi Web-sovellusten maailmaan. Oletuksena on, että hallitset ainakin jossain määrin kurssilta Tietokannat ja Web-ohjelmointi (vanhalta nimeltään Aineopintojen harjoitustyö: Tietokantasovellus) tutun Flask-kirjaston käytön.
Tarkastellaan edellisestä tehtävästä tutun toiminnallisuuden tarjoamaa esimerkkiprojektia, joka löytyy kurssirepositorion alihakemistosta viikko3/webcounter.
Tee tätä ja kahta seuraavaa tehtävää varten kokonaan uusi repositorio, nimeltään esimerkiksi webcounter, ja laita muiden tehtävien palautukseen käyttämäsi palautusrepositorion tiedostoon README.md linkki tätä tehtävää varten tehtyyn repositorioon.
Asenna projektin riippuvuudet komennolla uv sync 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:5001:

Sovellus siis toimii localhostilla eli paikallisella koneellasi portissa 5001.
Saat sammutettua sovelluksen painamalla komentoriviltä ctrl+c tai ctrl+d.
Sovelluksen rakenne on pääosin sama mitä kurssin Tietokannat ja Web-ohjelmointi esimerkkisovelluksissa.
Tiedostossa app.py määritellään sivupyyntöjen käsittelijäfunktiot:
from flask import Flask, redirect, render_template
from counter import Counter
app = Flask(__name__)
cnt = Counter()
@app.route("/")
def index():
return render_template("index.html", value=cnt.value)
@app.route("/increment", methods=["POST"])
def increment():
cnt.increase()
return redirect("/")
Sovelluksen HTML-sivupohjat on määritelty hakemistossa templates. Sovelluksen ainoa näkymä näyttää muuttujaan cnt talletetun laskuriolion arvon. Näkymä sisältää myös kaksi nappia, joista “Paina” aiheuttaa POST-pyynnön reitille increment. Reitin käsittelijä kasvattaa laskurin arvoa, ja uudelleenohjaa sovelluksen takaisin juuriosoitteeseen. Nappi “Nollaa” ei toimi tällä hetkellä.
Sovellukselle on tehty pari testiä Robot Frameworkilla. Testit suoritetaan normaaliin tapaan komennolla robot src/tests mutta ennen testien suorittamista joudumme tekemään muutaman ekstratempun.
Testeissä on käytössä Robot Frameworkin Browser-kirjasto, jonka avulla on mahdollista simuloida selaimen käyttöä koodista käsin. Browser-kirjasto perustuu Microsoftin Playwright-työkaluun, joka ohjaa selainta suoraan.
Projektin riippuvuuksina on kirjaston lisäksi paketti robotframework-browser-batteries, joka sisältää kirjaston tarvitseman Node.js-ajoympäristön valmiina, eli Node.js:ää ei tarvitse asentaa erikseen.
Ennen kuin siirryt testien pariin, asenna testien käyttämä Chromium-selain komennolla uv run rfbrowser install chromium.
Selain asentuu projektin virtuaaliympäristön sisälle. Asennus on siis tehtävä jokaiselle projektille erikseen, ja uudelleen, jos poistat hakemiston .venv.
Käynnistä web-sovellus edellisen tehtävän tapaan komentoriviltä.
Varmista selaimella, että sovellus on päällä. Varmista, että sovelluksen laskurin arvo on 0. Jos se on jotain muuta, uudelleenkäynnistä sovellus.
Avaa uusi terminaali-ikkuna ja suorita projektin testit virtuaaliympäristössä komennolla robot src/tests.
Komennon pitäisi suorittaa onnistuneesti kaksi testitapausta, At start the counter is zero ja When button pressed twice the counter is two. Testitapausten suoritusta voi seurata aukeavasta selaimen ikkunasta.
Ongelmia?
Tämä ohje saattaa auttaa.
Tutustuminen testeihin
Tiedostossa increment.robot olevat testit näyttävät seuraavalta:
*** Settings ***
Resource resource.robot
Suite Setup Open And Configure Browser
Suite Teardown Close Browser
*** Test Cases ***
At start the counter is zero
Go To ${HOME_URL}
Get Title == Laskuri
Get Text body *= nappia painettu 0 kertaa
When button pressed twice the counter is two
Go To ${HOME_URL}
Click button >> text=Paina
Click button >> text=Paina
Get Text body *= nappia painettu 2 kertaa
Jos unohdetaan alun osio Settings, on testien toiminnallisuus aika ilmeinen. Käytössä olevat avainsanat Go To, Click, Get Title ja Get Text ovat Browser-kirjaston tarjoamia valmiita avainsanoja.
Avainsanat Get Title ja Get Text hakevat sivulta arvon, ja niille voi antaa lisäksi vertailuoperaattorin ja odotetun arvon. Esimerkiksi Get Title == Laskuri tarkistaa, että sivun otsikko on täsmälleen Laskuri, ja Get Text body *= nappia painettu 0 kertaa tarkistaa, että sivun body-elementin teksti sisältää annetun merkkijonon. Jos ehto ei toteudu, testi epäonnistuu. Browser-kirjasto odottaa tarvittaessa hetken (oletusarvoisesti korkeintaan 10 sekuntia), että ehto toteutuu, ennen kuin se toteaa testin epäonnistuneen.
Miten Browser-kirjasto löytää sivun elementit?
Testitapauksissa ollaan interaktiossa erilaisten HTML-elementtien, kuten tekstikenttien ja painikkeiden kanssa. Avainsanoille kerrotaan selektorin avulla, mihin elementtiin ne kohdistuvat. Browser-kirjasto tukee useita selektorityyppejä, joista tärkeimmät ovat:
| selektori | löytää |
|---|---|
id=foo |
elementin, jonka id-attribuutin arvo on foo |
text=foo |
elementin, jonka tekstisisältö on tai sisältää foo |
button |
CSS-selektori, tässä tapauksessa button-elementti |
input[name="foo"] |
CSS-selektori: input-elementti, jonka name-attribuutin arvo on foo |
Selektoreita voi myös ketjuttaa merkinnällä >>, jolloin jälkimmäistä selektoria etsitään edellisen löytämien elementtien joukosta. Kutsu Click button >> text=Paina etsii siis button-elementin, jonka tekstinä on Paina, eli seuraavan src/templates/index.html-tiedostossa määritellyn painikkeen:
<button type="submit">
Paina
</button>
Pelkkä Click text=Paina toimisi tässä tapauksessa myös, sillä sivulla ei ole muita elementtejä, joiden tekstinä on Paina.
Tutustuminen testeihin jatkuu
Osassa *** Settings *** on useita huomionarvoisia seikkoja, rivi
Resource resource.robot
kertoo, että testin tulee ottaa käyttöön tiedostossa resource.robot tehdyt määritelmät (eli resurssit).
Tiedoston resource.robot sisältö on seuraava:
*** Settings ***
Library Browser
*** Variables ***
${DELAY} 500ms
${HOME_URL} http://localhost:5001
${BROWSER} chromium
*** Keywords ***
Open And Configure Browser
New Browser browser=${BROWSER} headless=False slowMo=${DELAY}
New Context
New Page about:blank
*** Settings *** osiossa otetaan käyttöön edellä mainittu Browser-kirjasto, joka siis tuo mukaan lukuisia uusia avainsanoja, joista kaikki on dokumentoitu täällä.
Tiedostossa on myös osio *** Variables *** missä on mahdollista määritellä muuttujia, jotka ovat kaikkien osion avainsanojen käytössä. Huomaa, että määritellyt muuttujat kirjoitetaan isoilla kirjaimilla, toisin kuin argumentit. Muuttujia kannattaa suosia aina kovakoodattujen arvojen sijaan.
*** Keywords ***-osiossa on määritellään avainsana Open And Configure Browser joka alustaa selaimen testejä varten:
- Avainsana käynnistää selaimen käyttämällä Browser-kirjaston New Browser -avainsanaa antaen
browser-argumentin arvoksi käytetyn selaimen, joka on oletusarvoisesti chromium. Muita vaihtoehtoja ovat firefox ja webkit, jotka tosin on asennettava erikseen komennollauv run rfbrowser install firefoxtaiuv run rfbrowser install webkit. - Argumentti
headless=Falsemäärittelee, että selaimen ikkuna on näkyvissä testien suorituksen ajan. - Argumentti
slowMoasettaa jokaisen selainoperaation väliinDELAY-muuttujan arvon mittaisen viiveen. Pidempi viive helpottaa testien suorituksen seuraamista. - New Context luo selaimeen uuden, muista erillisen kontekstin, joka vastaa käytännössä selaimen incognito-ikkunaa: kontekstilla on omat evästeensä ja muu tilansa. Selaimen ikkunan koon voi tarvittaessa asettaa kontekstin luomisen yhteydessä argumentilla
viewport, nyt käytössä on oletusarvoinen koko. - New Page avaa kontekstiin uuden välilehden, tässä tapauksessa tyhjän sivun.
Palataan vielä tiedostoon increment.robot, jonka alun osio *** Settings *** on seuraava
*** Settings ***
Resource resource.robot
Suite Setup Open And Configure Browser
Suite Teardown Close Browser
*** Test Cases ***
...
Osiossa on käytössä ennestään tuntemattomat Suite Setup-, Suite Teardown-asetukset. Niiden merkitykset ovat seuraavat:
Suite Setup-asetuksen avulla voimme suorittaa avainsanan ennen tiedoston ensimmäistä testitapausta, eli aluksi siis suoritetaan Open And Configure Browser joka määriteltiin tiedostossaresource.robotSuite Teardown-asetuksen avulla voimme suorittaa avainsanan tiedoston viimeisen testitapauksen jälkeen, tapauksessamme suljemme selaimen avainsanalla Close Browser
Huomaa, että toimiakseen testit edellyttävät, että sovellus on alussa tilassa missä laskurin arvo on 0. Uudelleenkäynnistä siis sovellus aina ennen testien suorittamista!
Kun olet suorittanut testit onnistuneesti ja tutustunut sovellukseen sekä testeihin on tämä tehtävä tehty.
3. Weblaskurin nollaus
Laajenna sovellusta siten, että nappi “Nollaa” nollaa laskurin arvon.
Tee Robot-testi, joka varmistaa, että nollaaminen toimii.
Tee testi tiedostoon reset.robot, testin näyttää suunnilleen seuraavalta
*** Settings ***
Resource resource.robot
Suite Setup Open And Configure Browser
Suite Teardown Close Browser
*** Test Cases ***
When counter has a nonzero value and it is reset the value becomes zero
...
Muista, että toimiakseen valmiina olevat testit edellyttävät että sovellus on alussa tilassa missä laskurin arvo on 0. Uudelleenkäynnistä siis sovellus aina ennen testien suorittamista!
4. Web-sovelluksen testien suorittaminen GitHub Actioneissa
Browser-kirjastoa käyttävät Robot-testit on melko helppo suorittaa myös GitHub Actioneissa.
Konfiguraatioihin on tehtävä muutama muutos.
Laajennetaan tiedostoa resource.robot seuraavasti:
*** Settings ***
Library Browser
*** Variables ***
${SERVER} localhost:5001
${DELAY} 500ms
${HOME_URL} http://${SERVER}
${BROWSER} chromium
${HEADLESS} false
*** Keywords ***
Open And Configure Browser
IF $HEADLESS == 'true'
New Browser browser=${BROWSER} headless=True
ELSE
New Browser browser=${BROWSER} headless=False slowMo=${DELAY}
END
New Context
New Page about:blank
Olemme nyt lisänneet muuttujan HEADLESS jolle arvon true asettamalla voimme suorittaa testit headless-selaimella, eli selaimella missä ei ole käyttöliittymää. Olemme myös määritelleet, että headlessina suoritettaessa operaatioiden väliin ei lisätä viivettä, jotta testit eivät hidastu tarpeettomasti.
Huomaa, että ehto on kirjoitettu muodossa
$HEADLESS == 'true'eikä${HEADLESS} == 'true'. Robot Framework tulkitseeIF-ehdon Python-lausekkeena, ja muoto$HEADLESSviittaa muuttujaan Python-muuttujan tapaan. Jos käyttäisimme muotoa${HEADLESS}, sijoittaisi Robot Framework muuttujan arvon lausekkeeseen sellaisenaan, ja lauseke olisi muotoafalse == 'true', mikä aiheuttaisi virheen.
Headless-suoritus tapahtuu seuraavasti:
robot --variable HEADLESS:true src/tests
Tee projektille GitHub Actionit määrittelevä konfiguraatio, joka näyttää seuraavalta:
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: Install uv
uses: astral-sh/setup-uv@v10.1.0
with:
python-version: '3.14'
- name: Install dependencies
run: uv sync
- name: Install Playwright browser
run: uv run rfbrowser install --with-deps chromium
- name: Run robot tests
run: bash run_robot_tests.sh
Ennen viimeisessä askeleessa tapahtuvaa testien suorittamista asennetaan testien käyttämä Chromium-selain. Valitsin --with-deps asentaa myös selaimen tarvitsemat käyttöjärjestelmän kirjastot. Koska testit suoritetaan GitHub Actionissa headless-tilassa, mitään näyttöä tai ajuria ei tarvita.
Jotta sovelluksen testit voidaan suorittaa GitHub Actionissa, tulee nämä askeleet suorittaa komentorivikomennoilla.
Tähän tarkoitukseen, voimme käyttää seuraavaa bash-skriptiä run_robot_tests.sh, joka löytyy tehtäväpohjassa:
#!/bin/bash
echo "Running tests"
# käynnistetään Flask-palvelin taustalle
uv run python3 src/index.py &
echo "started Flask server"
# odotetaan, että palvelin on valmiina ottamaan vastaan pyyntöjä
while [[ "$(curl -s -o /dev/null -w ''%{http_code}'' localhost:5001)" != "200" ]];
do sleep 1;
done
echo "Flask server is ready"
# suoritetaan testit
uv run robot --variable HEADLESS:true src/tests
status=$?
# pysäytetään Flask-palvelin portissa 5001
kill $(lsof -t -i:5001)
exit $status
Varmista, että run_robot_tests.sh on mukana projektissasi.
Pushaa tehtävän repositorio GitHubiin ja varmista, että GitHub Actions suorittaa testit onnistuneesti.
Laajenna vielä sovellusta siten, että siihen tulee mahdollisuus asettaa laskuri haluttuun arvoon.
Sovellus voi näyttää laajennuksen jälkeen seuraavalta:

Kertaa tarvittaessa täältä se miten Browser-kirjasto löytää sivun elementit.
Ohjeita lomakkeen käsittelyyn kurssin Tietokannat ja Web-ohjelmointi materiaalissa. HUOM: lomakkeen datan vastaanottamisen jälkeen tulee tehdä redirect, samoin kuin nappien painallusten käsittelyssä, ks. Post/Redirect/Get.
Tee ominaisuudelle Robot-testit.
Jos lomakkeessa on käytössä syötekenttä, jonka attribuutti name on arvoltaan value:
<input type="text" name="value" />
Robot-testi voi kirjoittaa kenttään arvon 10 avainsanan Fill Text avulla seuraavasti:
Fill Text input[name="value"] 10
Korjaa vielä testejä siten, että jokainen testitiedosto aloitetaan tilanteesta, missä laskurin arvo on nolla.
Robot Framework -testien debuggaaminen
Ennen kuin edetään seuraavaan tehtäväsarjaan, nostetaan esiin tärkeä teema.
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
Jos testit eivät mene läpi, ottaa Browser-kirjasto kuvakaappauksen siitä tilanteesta, jossa testi havaitsee ongelman. Kuvakaappaus löytyy testien suorituksen jälkeen syntyvästä testiraportista tarkentamalla epäonnistuneen testin raporttiin. Tämän viikon tehtävää 7 tehdessäni törmäsin seuraavaan:

Tässä tapauksessa ongelma oli erittäin helppo korjata.
Tutustutaan seuraavaksi muihin 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. Oletetaan, että olet tehnyt tehtävässä 4 laskurin arvon asettamiselle tiedostoon set.robot seuraavan testin:
When counter is set to 10 the value is ten
Go To ${HOME_URL}
Click button >> text=Paina
Fill Text input[name="value"] 10
Click button >> text=Aseta
Get Text body *= nappia painettu 10 kertaa
Jos testi epäonnistuu, voimme suorittaa ainoastaan sen seuraavalla komennolla:
robot -t "When counter is set to 10 the value is ten" src/tests/set.robot
Komennolle robot annetaan siis -t-valitsimen avulla 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 tutkia miten ohjelman suoritus etenee. Ensin on jollain tavalla rajattava, missä ongelma saattaisi olla. Vanha hyvä kikka eli komennolla print tehtävät aputulostukset vievät jo pitkälle.
Joissain tapauksissa saatetaan tarvita järeämpiä keinoja. Oletetaan, että edellisen esimerkin testi epäonnistuu, ja sivulla lukee nappia painettu 11 kertaa. Ongelma on luultavasti arvon asettavassa reitinkäsittelijässä. Voimme pysäyttää ohjelman suorituksen halutulle riville Pythonin sisäänrakennetun funktion breakpoint avulla, joka käynnistää pdb-debuggerin:
from flask import Flask, redirect, render_template, request
from counter import Counter
app = Flask(__name__)
cnt = Counter()
# ...
@app.route("/set", methods=["POST"])
def set_value():
value = int(request.form["value"])
# pysäytetään ohjelman suoritus tälle riville
breakpoint()
cnt.increment(value)
return redirect("/")
Käynnistä nyt sovellus uudelleen, jotta muutokset koodiin astuvat voimaan. Suorita sen jälkeen pelkästään epäonnistuva testitapaus edellä mainitun ohjeen mukaisesti. Kun testin painallus saa aikaan POST-pyynnön reitille /set, koodin suoritus pysähtyy ja sovellusta suorittavaan terminaaliin ilmestyy seuraavanlainen komentorivi:
> /polku/webcounter/src/app.py(20)set_value()
-> breakpoint()
(Pdb)
Kyseessä on interaktiivinen komentorivi, jossa voimme suorittaa koodia. Nuoli (->) osoittaa riviä, jolla suoritus on. Katsotaan, mitkä ovat muuttujan value ja laskurin arvot:
(Pdb) value
10
(Pdb) cnt.value
1
Annamme siis komentoriville syötteen ja painamme Enter-painiketta. Lomakkeelta tullut arvo on siis oikein, ja laskurin arvo on testin painalluksen jälkeen 1, kuten pitääkin. Edetään koodissa rivi kerrallaan komennolla next:
(Pdb) next
> /polku/webcounter/src/app.py(21)set_value()
-> cnt.increment(value)
(Pdb) next
> /polku/webcounter/src/app.py(22)set_value()
-> return redirect("/")
(Pdb) cnt.value
11
Vika löytyi: metodi increment kasvattaa laskurin arvoa annetulla määrällä, vaikka arvo pitäisi asettaa.
Tärkeimmät pdb:n komennot ovat:
| komento | toiminto |
|---|---|
next (tai n) |
suorittaa seuraavan rivin |
step (tai s) |
suorittaa seuraavan rivin ja menee funktiokutsun sisään |
list (tai l) |
näyttää koodia nykyisen rivin ympäriltä |
continue (tai c) |
jatkaa suoritusta seuraavaan pysähdyskohtaan asti |
Muuttujan tai lausekkeen arvon saa näkyviin kirjoittamalla sen komentoriville. Jos muuttujan nimi on sama kuin jokin pdb:n komennoista, esim. n tai c, käytä muotoa p n.
Kun olet lopettanut debuggaamisen, anna komento continue ja poista koodista breakpoint-kutsu.
Huom: Browser-kirjasto odottaa esim. avainsanan
Get Textehdon toteutumista oletusarvoisesti korkeintaan 10 sekuntia. Jos debuggaat tätä kauemmin, testi epäonnistuu aikakatkaisuun. Tästä ei ole haittaa, sillä debuggauksen tarkoituksena on selvittää, mitä sovelluksessa tapahtuu. Suorita testi uudelleen, kun olet korjannut vian.
5. WebLogin, osa 1
Tarkastellaan nyt rakenteeltaan hieman monimutkaisempaa Web-sovellusta, joka löytyy kurssirepositorion hakemistossa viikko3/login.
Hae projekti ja kopioi se palautusrepositorioosi, hakemiston viikko3 sisälle.
Asenna projektin riippuvuudet komennolla uv sync ja käynnistä se virtuaaliympäristössä komennolla python3 src/index.py.
Asenna myös testien käyttämä Chromium-selain komennolla uv run rfbrowser install chromium.
- Kyseessä on uusi projekti, jolla on oma virtuaaliympäristönsä. Tehtävässä 2 asennettu selain ei siis ole tämän projektin käytettävissä.
Sovelluksen käynnistymisen jälkeen pääset käyttämään sitä avaamalla selaimella osoitteen http://localhost:5001. Sovellus siis toimii localhostilla eli paikallisella koneellasi portissa 5001.
Sovellus on hyvin yksinkertainen, se tarjoaa vain kaksi toimintoa:
- käyttäjä voi rekisteröityä, eli luoda järjestelmään käyttäjätunnuksen
- rekisteröitynyt käyttäjä voi kirjautua järjestelmään

Tutustutaan seuraavaksi sovelluksen rakenteeseen. Sovellus noudattaa ns. kerrosarkkitehtuuria eli se on rakenteeltaan samanlainen kuin kurssin Ohjelmistotekniikka referenssisovellus.
Sovelluksen rakenne ja testien suhde siihen näyttää seuraavalta:
Sovelluksen käyttöliittymä on toteutettu edellisten tehtävien sovelluksen tapaan tiedostoon app.py sekä hakemistoon templates. Ohjelman käyttäjien hallintaan liittyvä sovelluslogiikka on sijoitettu omaan luokkaansa UserService.
Sovelluksen käytössä oleva versio ei tallenna käyttäjien tietoja varsinaisesti mihinkän pysyvämpään paikkaan, kuten tietokantaan tai tiedostoon, käyttäjien tiedot pidetään ainoastaan keskusmuistissa.
Eräs huomionarvoinen seikka on se, että UserService-olio ei tallenna muistiin suoraan User-oliota vaan epäsuorasti UserRepository-luokan olion kautta. Mistä on kysymys?
UserRepository-luokka abstrahoi eli piilottaa käyttäjien hallinnointiin liittyvän logiikan sovelluksen muilta olioilta.
Suunnittelumalli Repository
Tietoon kohdistuvien operaatioiden eriyttämiseen sovelluslogiikasta on olemassa 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, kuten find_all, create ja delete kautta. 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ä koneen muistiin. Jos päättäisimme tallentaa käyttäjät esimerkiksi PostgreSQL-tietokantaan, ei tämä vaatisi muutoksia luokan ulkopuolelle.
Seuraavassa vielä lyhyt katsaus sovelluksen käyttöliittymän, eli reitin käsittelijöiden sekä näkymien generoinnin toiminnasta.
Polulle “/” eli sovelluksen juureen, osoitteeseen http://localhost:5001 tulevat pyynnöt käsittelee 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 salasanan oikeellisuuden kutsumalla UserService-olion metodia check_credentials. Jos kirjautuminen onnistuu, ohjataan käyttäjä “/ohtu”-polun sivulle. Jos se epäonnistuu, check_credentials-metodi aiheuttaa poikkeuksen, 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.
Tutustuminen testeihin
Tutustutaan aluksi testitapauksien yhteisiin asetuksiin ja avainsanoihin, jotka löytyvät src/tests/resource.robot-tiedostosta. Tiedoston sisältö on seuraava:
*** Settings ***
Library Browser
Library ../AppLibrary.py
*** Variables ***
${SERVER} localhost:5001
${DELAY} 500ms
${HOME_URL} http://${SERVER}
${LOGIN_URL} http://${SERVER}/login
${REGISTER_URL} http://${SERVER}/register
${BROWSER} chromium
${HEADLESS} false
*** Keywords ***
Open And Configure Browser
IF $HEADLESS == 'true'
New Browser browser=${BROWSER} headless=True
ELSE
New Browser browser=${BROWSER} headless=False slowMo=${DELAY}
END
New Context
New Page about:blank
Login Page Should Be Open
Get Title == Login
Main Page Should Be Open
Get Title == Ohtu Application main page
Go To Login Page
Go To ${LOGIN_URL}
Tiedoston sisältö on samankaltainen kuin edellisissä tehtävissä. Tällä kertaa
*** Settings *** osiossa on otettu Browser-kirjaston lisäksi käyttöön myös projektin oma AppLibrary.py-kirjasto, joka määrittelee kaksi projektissa tarvittavaa avainsanaa, Reset Application ja Create User.
*** Keywords ***-osiossa on määritelty myös muutama yleiskäyttöinen avainsana:
Login Page Should Be OpenjaMain Page Should Be Open, joiden tarkoitus on tarkistaa, että käyttäjä on oikealla sivulla. Ne käyttävät Get Title -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ä Get Text -avainsanaaGo To Login Page-avainsana käyttää Go To -avainsanaa avatakseen selaimessa kirjautumis-sivun, jonka URL on tallennettuLOGIN_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 Reset Application Create User And Go To Login Page
Edellisten tehtävien testien lisäksi nyt on käytössä myös Test Setup, joka suorittaa avainsanan Reset Application Create User And Go To Login Page ennen jokaista testiä.
Tiedoston *** Keywords *** osiossa on testitapausten käyttämiä avainsanoja:
Login Should Succeed-avainsana tarkastaa, että käyttäjä on siirtynyt oikealla sivulle onnistuneen kirjautumisen jälkeenLogin Should Fail With Message-avainsana tarkastaa, että käyttäjä on kirjautumissivulla ja että sivulta löytyy tietty virheviesti. Tarkastuksessa käytetään Get Text -avainsanaa operaattorilla*=, joka tarkistaa, että sivulta löytyy haluttu tekstiSet Username- jaSet Password-avainsanat syöttävät annetut arvot tiettyihin kenttiin käyttämällä Fill Text - ja Fill Secret -avainsanoja. Kentät löydetään niidenid-attribuutin perusteella selektoreillaid=usernamejaid=password. Fill Secret toimii kuten Fill Text, mutta se ei kirjoita syötettyä arvoa testien lokiin. Tämän takia arvo annetaan sille muodossa$passwordeikä${password}Reset Application Create User And Go To Login Page-avainsana tyhjentää sovelluksen “tietokannan” eli sinne luodut käyttäjät, luo sovellukseen uuden käyttäjän ja avaa kirjautumissivun
Kertaa tarvittaessa täältä se miten Browser-kirjasto löytää sivun elementit.
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 Reset Application And Go To Starting Page
*** Test Cases ***
Click Login Link
Click text=Login
Login Page Should Be Open
Click Register Link
Click text=Register new user
Register Page Should Be Open
*** Keywords ***
Reset Application And Go To Starting Page
Reset Application
Go To Starting Page
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 -avainsanaa, jolle linkki yksilöidään sen tekstin perusteella selektorilla text=....
Suorita testit
Muista, että selaimen tulee olla asennettuna ja sovelluksen on oltava käynnissä kun suoritat testit, ks. tämä ohje
Seurauksena on todennäköisesti virheilmoitus kertoo mitä avainsanoja on määrittelemättä:
Click Register Link | FAIL |
Setup failed:
No keyword with name 'Go To Starting Page' found.
Toteuta testin käyttämät määrittelemättömät avainsanat.
HUOM: ideana on, että avainsana Go To Starting Page vie sovelluksen polkuun / eli aloitussivulle.
6. WebLogin, 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:5001"
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)
On oleellista, että testit alkavat aina samasta tilasta, erityisesti että sovelluksen tietokannan tila on testien alussa hyvin tunnettu.
Metodin reset_application määrittelemä avainsana 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. Kyseessä on siis ainoastaan testien käyttöön toteutettu tapa nollata tietokanta.
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
# ...
7. WebLogin, osa 3
Tee User storylle A new user account can be created if a proper unused username and a proper password are given seuraavat testitapaukset uuteen tiedostoon register.robot:
*** Settings ***
Resource resource.robot
Suite Setup Open And Configure Browser
Suite Teardown Close Browser
Test Setup Reset Application Create User And Go To Register Page
*** Test Cases ***
Register With Valid Username And Password
# ...
Register With Too Short Username And Valid Password
# ...
Register With Valid Username And Too Short Password
# ...
Register With Username That Is Already In Use
#
*** Keywords ***
#...
Käyttäjätunnus ja salasana noudattavat seuraavia sääntöjä:
- Käyttäjätunnuksen on oltava vähintään 3 merkin pituinen merkkijono, joka ei ole vielä käytössä
- Salasanan on oltava pituudeltaan vähintään 8 merkkiä
Salasanaan liittyy vielä muita vaatimuksia, mutta ne toteutetaan vasta tehtävässä 8.
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.
Pro tips:
- Etene yksi testitapaus ja sen toteuttama koodi kerrallaan
- Kertaa tarvittaessa täältä se miten Browser-kirjasto löytää sivun elementit
- Ota mallia kirjautumisen testeistä!
- Muista tämä, ja sieltä erityisesti tämä
Mihin avainsanat kannattaa sijoittaa?
Rekisteröitymisen testejä tehdessä huomaat pian tarvitsevasi samoja avainsanoja kuin kirjautumisen testeissä, esim. avainsanoja Set Username ja Set Password, jotka on määritelty tiedostossa login.robot. Avainsanat voi sijoittaa joko testitiedoston *** Keywords *** -osioon tai yhteiseen tiedostoon resource.robot. Nyrkkisääntö on seuraava:
- Testitiedostoon kuuluvat avainsanat, joita käytetään vain sen tiedoston testeissä. Esim.
Login Should SucceedjaLogin Should Fail With Messageliittyvät vain kirjautumiseen, joten ne sopivat tiedostoonlogin.robot. Samoin rekisteröitymisen tarkistukset, kutenRegister Should Succeed, sopivat tiedostoonregister.robot. - Tiedostoon
resource.robotkuuluvat avainsanat, joita käytetään useammassa testitiedostossa. Esimerkkejä ovat selaimen alustavaOpen And Configure Browser, sivulle siirtyvät avainsanat, kutenGo To Login PagejaGo To Register Page, sekä sivun tarkistavat avainsanat, kutenLogin Page Should Be OpenjaRegister Page Should Be Open. Myös kaikkien testien yhteiset muuttujat, kuten sivujen osoitteet, kuuluvat tänne.
Kun huomaat tarvitsevasi testitiedostossa avainsanaa, joka on jo määritelty toisessa testitiedostossa, älä kopioi sitä, vaan siirrä se tiedostoon resource.robot. Esim. jos register.robot tarvitsee avainsanoja Set Username ja Set Password, siirrä ne tiedostosta login.robot tiedostoon resource.robot. Kopioidut avainsanat erkaantuvat ajan myötä toisistaan, ja muutokset joudutaan tekemään moneen paikkaan.
Jos samanniminen avainsana on määritelty sekä testitiedostossa että tiedostossa resource.robot, Robot Framework käyttää testitiedoston omaa määrittelyä. Tämä voi aiheuttaa hämmentäviä tilanteita, joten kun siirrät avainsanan tiedostoon resource.robot, muista poistaa se alkuperäisestä paikasta.
8. Agentti ja hyväksymistestit [tekoäly]
Jatketaan viikon 2 tehtävässä 9 alkanutta [tekoäly]-tehtävien sarjaa.
Kertaa tarvittaessa viime viikolla lukemasi materiaalin Tekoäly ohjelmistotuotannossa luvut Mikä kielimalli on ja AI-avusteisen ohjelmoinnin muodot. Lue ennen tämän tehtävän tekemistä myös luvut Agenttinen koodaus, Kontekstin hallinta ja Vaatimukset promptina.
Tehtävässä käytetään VS Coden GitHub Copilotia. Voit toki tehdä tehtävän myös jollain muulla AI-avusteisella koodaustyökalulla.
Jatketaan WebLogin-sovelluksen parissa. Varmista, että tehtävien 5-7 muutokset on commitoitu ennen kuin annat agentin koskea koodiin.
Avaa VS Codessa hakemisto viikko3/login omana workspacenaan (esim. komennolla code . hakemistossa viikko3/login), näin agentti keskittyy vain tähän projektiin
Avaa Copilotin Chat-näkymä ja valitse tilaksi Agent
Generoi projektille ohjetiedosto kirjoittamalla chattiin /init
Agentti tutkii projektin ja luo tiedoston .github/copilot-instructions.md tai AGENTS.md.
Lue ohjetiedosto huolellisesti ja korjaa tai täydennä sitä tarpeen mukaan. Tiedostossa tulee kertoa ainakin seuraavat asiat:
- projekti on uv-projekti, ja komennot suoritetaan muodossa
uv run ... - miten sovellus käynnistetään ja missä portissa se toimii
- miten Robot Framework -testit suoritetaan (
uv run robot src/tests) ja että sovelluksen on oltava käynnissä testien aikana - missä käyttäjätunnuksen ja salasanan validointi tapahtuu
Pidä ohjetiedosto tiiviinä. Se liitetään jokaiseen agentille annettavaan pyyntöön.
Commitoi ohjetiedosto
Rekisteröitymiseen liittyy vielä kaksi vaatimusta, joita tehtävässä 7 ei toteutettu:
User story: Salasana ei saa koostua pelkästään kirjaimista
Hyväksymiskriteerit:
- salasana "kalle123" hyväksytään
- salasana "kalle#abc" hyväksytään
- salasana "kallekalle" hylätään ja käyttäjälle näytetään virheilmoitus
- salasana "KalleKalle" hylätään ja käyttäjälle näytetään virheilmoitus
User story: Salasanan ja salasanan vahvistuksen on oltava samat
Hyväksymiskriteerit:
- jos salasana ja sen vahvistus ovat samat, rekisteröityminen onnistuu
- jos salasana ja sen vahvistus eroavat, rekisteröityminen epäonnistuu ja käyttäjälle näytetään virheilmoitus
Tehtävän 7 vaatimus salasanan vähimmäispituudesta on edelleen voimassa.
Toteuta storyt agentin avulla yksi kerrallaan.
Pyydä agenttia kirjoittamaan ensin storyn hyväksymiskriteerit Robot Framework -testeiksi tiedostoon register.robot ja varmistamaan, että testit eivät mene läpi. Vasta tämän jälkeen agentin tulee toteuttaa toiminnallisuus niin, että testit menevät läpi
Anna agentin suorittaa sovellus ja testit itse. Lue jokainen komento ennen kuin hyväksyt sen suoritettavaksi.
Kun agentti on valmis, suorita kaikki testit vielä itse ja varmista, että ne menevät läpi
Käy agentin tekemä muutos läpi esim. komennolla git diff. Teki agentti muutoksia sellaisiin kohtiin, joihin sitä ei pyydetty koskemaan? Poista tai pyydä agenttia poistamaan tarpeeton koodi
Miten voit olla varma, että agentin kirjoittamat testit oikeasti testaavat uutta toiminnallisuutta?
Rikko toteutus itse tarkoituksella ainakin kahdella eri tavalla, esim. hyväksy pelkistä kirjaimista koostuva salasana tai poista salasanan vahvistuksen tarkistus, ja varmista jokaisen rikkomisen jälkeen, että ainakin yksi testi hajoaa
Palauta toimiva toteutus ja commitoi muutokset
Kirjoita raportti kokemuksistasi hakemistoon viikko3 talletettavaan tiedostoon ai.md
Kerro raportissa
- Minkälaisen ohjetiedoston agentti generoi, ja mitä jouduit korjaamaan?
- Noudattiko agentti pyyntöä kirjoittaa testit ennen toteutusta?
- Kuinka paljon jouduit ohjaamaan agenttia matkan varrella?
- Hajosivatko testit, kun rikoit toteutuksen tarkoituksella?
- Mitä uutta opit?
Lue tehtävän tekemisen jälkeen vielä AI-materiaalin luku Vastuu ja osaaminen
9. Retrospektiivitekniikat
Wikipedian mukaan retrospektiivi on “a meeting held by a project team at the end of a project or process (often after an iteration) to discuss what was successful about the project or time period covered by that retrospective, what could be improved, and how to incorporate the successes and improvements in future iterations or projects.”
Tutustu täällä esiteltyihin retrospektiivitekniikoihin Start, Stop, Continue, More of, Less of Wheel ja Glad, Sad, Mad.
Tee aiheesta noin 0.25 sivun (eli noin 125 sanaa) tiivistelmä palautusreporitorion hakemistoon viikko3 sijoitettavaan tiedostoon retro.md.
Pidä huoli siitä, että miniprojektitiimisi pitää ensimmäisen sprintin lopussa jotain tekniikkaa noudattavan retrospektiivin!
Tehtävien palautus
Laita palautusrepositoriosi tiedostoon README.md linkki tehtäviä 2-4 varten tehtyyn webcounter-repositoroosi.
Tehtävien palautus
Pushaa kaikki tekemäsi tehtävät (paitsi ne, joissa mainitaan, että tehtävää ei palauteta mihinkään) GitHubiin palautusrepositorioosi ja merkkaa tekemäsi tehtävät palautussovellukseen https://study.cs.helsinki.fi/stats/courses/ohtu-2026