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 komennolla uv run rfbrowser install firefox tai uv run rfbrowser install webkit.
  • Argumentti headless=False määrittelee, että selaimen ikkuna on näkyvissä testien suorituksen ajan.
  • Argumentti slowMo asettaa jokaisen selainoperaation väliin DELAY-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 tiedostossa resource.robot
  • Suite 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 tulkitsee IF-ehdon Python-lausekkeena, ja muoto $HEADLESS viittaa muuttujaan Python-muuttujan tapaan. Jos käyttäisimme muotoa ${HEADLESS}, sijoittaisi Robot Framework muuttujan arvon lausekkeeseen sellaisenaan, ja lauseke olisi muotoa false == '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 Text ehdon 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 Open ja Main 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 -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      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ä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 Get Text -avainsanaa operaattorilla *=, joka tarkistaa, että sivulta löytyy haluttu teksti
  • Set Username- ja Set 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 niiden id-attribuutin perusteella selektoreilla id=username ja id=password. Fill Secret toimii kuten Fill Text, mutta se ei kirjoita syötettyä arvoa testien lokiin. Tämän takia arvo annetaan sille muodossa $password eikä ${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 Succeed ja Login Should Fail With Message liittyvät vain kirjautumiseen, joten ne sopivat tiedostoon login.robot. Samoin rekisteröitymisen tarkistukset, kuten Register Should Succeed, sopivat tiedostoon register.robot.
  • Tiedostoon resource.robot kuuluvat avainsanat, joita käytetään useammassa testitiedostossa. Esimerkkejä ovat selaimen alustava Open And Configure Browser, sivulle siirtyvät avainsanat, kuten Go To Login Page ja Go To Register Page, sekä sivun tarkistavat avainsanat, kuten Login Page Should Be Open ja Register 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