Viikko 4
Allaolevien tehtävien deadline on maanantai 24.11. klo 23:59
Apua tehtävien tekoon kurssin Discord-kanavalla sekä kampuksella pajassa BK107:
- ma 14.30-16.30
- ti 12-16
- to 12-16
- pe 12-14
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 23.11. klo 23:59:00 .
Tehtävissä 1-4 tutustutaan riippuvuuksien “mockaamiseen” yksikkötesteissä. Tehtävässä 5 tutustutaan Gitin tägeihin.
Viikon loppuun on lisätty vapaaehtoinen tehtävä, missä päästään tutustumaan testivetoiseen ohjelmistokehitykseen eli TDD:hen.
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/95023982/feedback
Ongelmia Poetryn 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/ohtu2025 välilehdelle “my submission”.
Kaikki tämän viikon tehtävät palautetaan jo edellisillä viikoilla käyttämääsi palautusrepositorioon, sinne tehtävän hakemiston viikko4 sisälle. Teknisesti ottaen tehtävän 7 palautus ei tosin luo repositorioon uutta sisältöä tiedostojen muodossa.
Katso tarkempi ohje palautusrepositoriota koskien täältä.
GitHub Education
Muutama myöhemmin kurssilla oleva tehtävä käyttää GitHubin Copilotia, jonka käyttö on ilmaista jos aktivoit GitHub Education -jäsenyyden. Jos et ole vielä jäsen, hae jäsenyyttä nyt. Hakemuksen hyväksyminen kestää internetin mukaan jopa viikon.
1. Yksikkötestaus ja riippuvuudet: mock-kirjasto, osa 1
Useimmilla luokilla on riippuvuuksia toisiin luokkiin. Esim. viikon 1 laskarien NHL-tilastot-tehtävässä luokka StatisticsService riippuu luokasta PlayerReader. Riippuvuuksien injektion avulla saimme mukavasti purettua suorat riippuvuudet luokkien väliltä.
Vaikka luokilla ei olisikaan suoria riippuvuuksia toisiin luokkiin, on tilanne edelleen se, että luokan oliot käyttävät joidenkin toisten luokkien olioiden palveluita. Tämä tekee yksikkötestauksesta välillä hankalaa. Miten esim. luokkaa StatisticsService tulisi testata? Tuleeko testeissä olla mukana toimivat versiot kaikista sen riippuvuuksista?
NHL-tilastot-tehtävässä ongelma ratkaistiin ohjelmoimalla riippuvuuden korvaava “tynkäkomponentti” PlayerReaderStub:
import unittest
from statistics_service import StatisticsService
from player import Player
class PlayerReaderStub:
def get_players(self):
return [
Player("Semenko", "EDM", 4, 12),
Player("Lemieux", "PIT", 45, 54),
Player("Kurri", "EDM", 37, 53),
Player("Yzerman", "DET", 42, 56),
Player("Gretzky", "EDM", 35, 89)
]
class TestStatisticsService(unittest.TestCase):
def setUp(self):
# annetaan StatisticsService-luokan oliolle "stub"-luokan olio
self.stats = StatisticsService(
PlayerReaderStub()
)
# ...
Pythonille kuten kaikille muillekin kielille on tarjolla myös valmiita kirjastoja tynkäkomponenttien, toiselta nimeltään mock-olioiden luomiseen.
Kuten pian huomaamme, mock-oliot eivät ole pelkkiä “tynkäolioita”, mockien avulla voi myös varmistaa, että testattava metodi tai funktio kutsuu olioiden metodeja asiaankuuluvalla tavalla.
Tutustumme nyt unittest-moduulin mock-kirjastoon. Kirjastosta voidaan tuoda luokka Mock.
Käynnistä Python-terminaali komennolla python3:
>>> from unittest.mock import Mock
>>> mock = Mock()
>>> mock
<Mock id='4568521696'>
Anna terminaaliin yksi kerrallaan samat syötteet kuin yo. esimerkissä. Enter-painikkeen painallus suorittaa annetun syötteen.
Muuttuja mock sisältää siis Mock-luokan olion. Mock-luokan olioilla on se mielenkiintoinen piirre, että niiden kaikki mahdolliset attribuutit ja metodit on toteutettu. Mitä tällä tarkoitetaan?
Kokeillaan:
>>> mock.foo
<Mock name='mock.foo' id='4568521648'>
>>> mock.foo.bar()
<Mock name='mock.foo.bar()' id='4570560112'>
Tee jälleen toimenpiteet myös itse!
Kaikki annetut operaatiot palauttavat siis uuden Mock-olion. Voimme antaa olion metodeille haluttuja paluuarvoja return_value-attribuutin avulla:
>>> mock.foo.bar.return_value = "Foobar"
>>> mock.foo.bar()
'Foobar'
Tee jälleen toimenpiteet myös itse!
Voimme myös antaa metodeille haluttuja toteutuksia side_effect-attribuutin avulla:
>>> mock.foo.bar.side_effect = lambda name: f"{name}: Foobar"
>>> mock.foo.bar("Kalle")
'Kalle: Foobar'
Do it!
Attribuutin side_effect arvo pitää olla kutsuttavissa, kuten funktio, metodi, tai lambda. Huomaa, että Mock-oliota voi käyttää myös funktion kaltaisesti:
>>> get_name_mock = Mock(return_value = "Matti")
>>> get_name_mock()
'Matti'
Mockeille voidaan määritellä toteutuksien lisäksi oletuksia. Voimme esimerkiksi olettaa, että Mock-oliota on kutsuttu:
>>> mock.foo.bar.assert_called()
>>> mock.foo.doo.assert_called()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/opt/homebrew/Cellar/python@3.11/3.11.6_1/Frameworks/Python.framework/Versions/3.11/lib/python3.12/unittest/mock.py", line 908, in assert_called
AssertionError: Expected 'doo' to have been called.
Kokeile myös ylläolevaa
Voimme siis kutsua tarkasteltavalle metodille assert_called-metodia. Huomaa, että mock.foo.bar-metodia on kutsuttu, mutta mock.foo.doo-metodia sen sijaan ei ole. Voimme myös tarkistaa, että metodia on kutsuttu oikeilla argumenteilla käyttämällä assert_called_with-metodia.
Kun Mock-oliot ovat tulleet tutuksi, voit sulkea terminaalin komennolla exit().
Hae seuraavaksi kurssirepositorion hakemistossa viikko4/mock-demo oleva projekti.
Tässä tehtävässä ei tehdä mitään koodia, joten projektia ei ole tarvetta välttämättä palauttaa. Voit halutessasi kopioida projektin palautusrepositorioosi, hakemiston viikko4 sisälle.
Projekti on yksinkertainen verkkokauppa, jonka sovelluslogiikan toteuttaa luokka Kauppa. Luokalla on riippuvuus Pankki- ja Viitegeneraattori-olioihin.
Kaupan toimintaperiaate on yksinkertainen:
my_net_bank = Pankki()
viitteet = Viitegeneraattori()
kauppa = Kauppa(my_net_bank, viitteet)
kauppa.aloita_ostokset()
kauppa.lisaa_ostos(5)
kauppa.lisaa_ostos(7)
kauppa.maksa("1111")
Ostokset aloitetaan tekemällä metodikutsu aloita_ostokset. Tämän jälkeen “ostoskoriin” lisätään tuotteita, joiden hinta kerrotaan metodin lisaa_ostos parametrina. Ostokset lopetetaan kutsumalla metodia maksa joka saa parametriksi tilinumeron, jolta summa veloitetaan.
Kauppa tekee veloituksen käyttäen tuntemaansa luokan Pankki oliota. Viitenumerona käytetään luokan Viitegeneraattori generoimaa numeroa. Sovelluksen rakenne siis näyttää seuraavalta:

Projektiin on kirjoitettu kuusi Mock-luokkaa hyödyntävää testiä. Testit varmistavat, että kauppa tekee ostoksiin liittyvän veloituksen oikein, eli että se kutsuu Pankki-luokan metodia maksa oikeilla parametreilla, ja että jokaiselle laskutukselle on kysytty viitenumero Viitegeneraattori-luokan metodilta uusi. Testit siis eivät kohdistu kauppa-olion tilaan vaan sen muiden olioiden kanssa käymän interaktion oikeellisuuteen. Testeissä kaupan riippuvuudet (Pankki ja Viitegeneraattori) on määritelty Mock-olioina.
Seuraavassa testi, joka testaa, että kauppa kutsuu pankin metodia oikealla tilinumerolla ja summalla:
def test_kutsutaan_pankkia_oikealla_tilinumerolla_ja_summalla(self):
pankki_mock = Mock()
viitegeneraattori_mock = Mock(wraps=Viitegeneraattori())
kauppa = Kauppa(pankki_mock, viitegeneraattori_mock)
kauppa.aloita_ostokset()
kauppa.lisaa_ostos(5)
kauppa.lisaa_ostos(5)
kauppa.maksa("1111")
# katsotaan, että ensimmäisen ja toisen parametrin arvo on oikea
pankki_mock.maksa.assert_called_with("1111", 10, ANY)
Testi siis aloittaa luomalla kaupan riippuvuuksista mock-oliot:
pankki_mock = Mock()
viitegeneraattori_mock = Mock(wraps=Viitegeneraattori())
kauppa = Kauppa(pankki_mock, viitegeneraattori_mock)
Mock-luokan konstruktorin wraps-parametrin avulla voimme käyttää olemassa olevan olion metodeja mock-olion kautta. Tämä mahdollistaa sen, ettei esimerkiksi uusi-metodille tarvitse määritellä toteutusta, vaan voimme käyttää sen oikeaa toteutusta.
Eli nyt viitegeneraattori on olio, jonka metodi uusi palauttaa arvot 1, 2, 3…
Testi tarkastaa, että kaupalle tehdyt metodikutsut aiheuttavat sen, että pankin Mock-olion metodia maksa on kutsuttu oikeilla parametreilla. Kolmanteen parametriin, eli viitenumeroon ei kiinnitetä huomiota:
pankki_mock.maksa.assert_called_with("1111", 10, ANY)
Kuten edellisistä esimerkeistä käy ilmi, Mock-olioiden metodikutsuille voi määrittää palautettavat arvot. Seuraavassa määritellään, että viitegeneraattori palauttaa arvon 55 kun sen metodia uusi kutsutaan:
def test_kaytetaan_maksussa_palautettua_viitetta(self):
pankki_mock = Mock()
viitegeneraattori_mock = Mock()
# palautetaan aina arvo 55
viitegeneraattori_mock.uusi.return_value = 55
kauppa = Kauppa(pankki_mock, viitegeneraattori_mock)
kauppa.aloita_ostokset()
kauppa.lisaa_ostos(5)
kauppa.lisaa_ostos(5)
kauppa.maksa("1111")
# katsotaan, että kolmannen parametrin arvo on oikea
pankki_mock.maksa.assert_called_with(ANY, ANY, 55)
Testin lopussa varmistetaan, että pankin Mock-oliota on kutsuttu oikeilla parametrinarvoilla, eli kolmantena parametrina tulee olla viitegeneraattorin palauttama arvo.
Tutustu projektiin ja sen kaikkiin testeihin:
Asenna projektin riippuvuudet komennolla poetry install ja suorita sen jälkeen testit virtuaaliympäristössä komennolla pytest
Riko jokin testi, esimerkiksi jokin edellä mainituista, muuttamalla sen ekspektaatiota esim. seuraavasti:
pankki_mock.maksa.assert_called_with(ANY, ANY, 1000)
Varmista, että testit eivät mene läpi. Katso miltä virheilmoitus näyttää.
Voit tutustua aiheeseen tarkemmin lukemalla mock-kirjaston dokumentaatiota.
2. Yksikkötestaus ja riippuvuudet: mock-kirjasto, osa 2
Hae kurssirepositorion hakemistossa viikko4/maksukortti-mock oleva projekti.
Kopioi projekti palautusrepositorioosi, hakemiston viikko4 sisälle.
Tässä tehtävässä on tarkoitus testata ja täydentää luokkaa Kassapaate, jonka hieman kehittyneempi versio lienee ainakin joillekin tuttu Ohjelmoinnin jatkokurssilta.
Maksukortin koodiin ei tehtävässä saa koskea ollenkaan! Testeissä ei myöskään ole tarkoitus luoda konkreettisia instansseja maksukortista, testien tarvitsemat kortit tulee luoda mock-kirjaston avulla.
Projektissa on valmiina kaksi testiä:
import unittest
from unittest.mock import Mock, ANY
from kassapaate import Kassapaate, HINTA
from maksukortti import Maksukortti
class TestKassapaate(unittest.TestCase):
def setUp(self):
self.kassa = Kassapaate()
def test_kortilta_velotetaan_hinta_jos_rahaa_on(self):
maksukortti_mock = Mock()
maksukortti_mock.saldo.return_value = 10
self.kassa.osta_lounas(maksukortti_mock)
maksukortti_mock.osta.assert_called_with(HINTA)
def test_kortilta_ei_veloteta_jos_raha_ei_riita(self):
maksukortti_mock = Mock()
maksukortti_mock.saldo.return_value = 4
self.kassa.osta_lounas(maksukortti_mock)
maksukortti_mock.osta.assert_not_called()
Ensimmäisessä testissä varmistetaan, että jos kortilla on riittävästi rahaa, kassapäätteen metodin osta_lounas kutsuminen veloittaa summan kortilta eli kutsuu kortin metodia osta.
Testi ottaa siis kantaa ainoastaan, siihen miten kassapääte kutsuu maksukortin metodeja. Maksukortin saldoa ei erikseen tarkasteta, sillä oletuksena on, että maksukortin omat testit varmistavat kortin toiminnan.
Toinen testi varmistaa, että jos kortilla ei ole riittävästi rahaa, kassapäätteen metodin osta_lounas kutsuminen ei veloita kortilta rahaa, eli että kortin metodia osta ei ole kutsuttu.
Testit eivät mene läpi. Korjaa kassapäätteen metodi osta_lounas.
Muistutus Maksukortin koodiin ei tehtävässä saa koskea ollenkaan! Maksukortin tilaa ei myöskään ole tarkoitus tutkia testeissä suoraan. Koska Maksukortti on mock, ei attribuuttien arvojen katsominen edes ole mahdollista/mielekästä.
Tee tämän jälkeen samaa periaatetta noudattaen seuraavat testit:
Kassapäätteen metodin lataa kutsu lisää maksukortille ladattavan rahamäärän käyttäen kortin metodia lataa, jos ladattava summa on positiivinen
Kassapäätteen metodin lataa kutsu ei tee maksukortille mitään, jos ladattava summa on negatiivinen
Huomio:
- Testeissä ei ole tarkoitus luoda konkreettisia instansseja maksukortista, testien tarvitsemat kortit tulee luoda mock-kirjaston avulla.
- Testit eivät myöskään testaa suoraan maksukortin tilaa, ainoastaan sitä onko maksukortin metodeja kutsuttu oikein.
Korjaa kassapäätettä siten, että testit menevät läpi.
3. Yksikkötestaus ja riippuvuudet: mock-kirjasto, osa 3
Kurssirepositorion hakemistossa viikko4/verkkokauppa löytyy hieman laajennettu versio tehtävän 1 verkkokaupasta.
Kopioi projekti palautusrepositorioosi, hakemiston viikko4 sisälle.
Ohjelma sisältää nyt hieman enemmän luokkia ja toiminnallisuus on monimutkaisempi. Kauppa hallinnoi kutakin ostostapahtumaa luokan Ostoskori olioina. Ostoskoriin laitetaan Tuote-olioita, jotka kuvaavat myynnissä olevia tuotteita. Varasto hallinnoi kaupan tuotevalikomaa. Yksinkertaisemman esimerkin tapaan kauppaan liittyy myös maksuliikenteen hoitava Pankki sekä Viitegeneraattori. Ohjelman rakenne luokkakaaviona:

Tutustu koodiin.
Piirrä sekvenssikaavio, joka kuvaa tiedostossa src/index.py olevan pääohjelman toimintaa (ensimmäisen ostostapahtuman verran). Kaaviota ei tarvitse palauttaa
Luokalle Kauppa injektoidaan konstruktorissa Pankki-, Viitelaskuri- ja Varasto-oliot. Näistä on tehty testeissä Mock-kirjaston avulla mockatut versiot.
Seuraavassa esimerkkinä testi, joka testaa, että ostostapahtuman jälkeen pankin metodia tilisiirto on kutsuttu:
import unittest
from unittest.mock import Mock, ANY
from kauppa import Kauppa
from viitegeneraattori import Viitegeneraattori
from varasto import Varasto
from tuote import Tuote
class TestKauppa(unittest.TestCase):
def test_maksettaessa_ostos_pankin_metodia_tilisiirto_kutsutaan(self):
pankki_mock = Mock()
viitegeneraattori_mock = Mock()
# palautetaan aina arvo 42
viitegeneraattori_mock.uusi.return_value = 42
varasto_mock = Mock()
# tehdään toteutus saldo-metodille
def varasto_saldo(tuote_id):
if tuote_id == 1:
return 10
# tehdään toteutus hae_tuote-metodille
def varasto_hae_tuote(tuote_id):
if tuote_id == 1:
return Tuote(1, "maito", 5)
# otetaan toteutukset käyttöön
varasto_mock.saldo.side_effect = varasto_saldo
varasto_mock.hae_tuote.side_effect = varasto_hae_tuote
# alustetaan kauppa
kauppa = Kauppa(varasto_mock, pankki_mock, viitegeneraattori_mock)
# tehdään ostokset
kauppa.aloita_asiointi()
# lisätään ostoskoriin tuote, jonka id on 1
kauppa.lisaa_koriin(1)
kauppa.tilimaksu("pekka", "12345")
# varmistetaan, että metodia tilisiirto on kutsuttu
pankki_mock.tilisiirto.assert_called()
# toistaiseksi ei välitetä kutsuun liittyvistä argumenteista
Varmista, että saat testit suoritettua.
Tee tämän jälkeen seuraavat testit:
Aloitetaan asiointi, koriin lisätään tuote, jota varastossa on ja suoritetaan ostos, eli kutsutaan metodia kaupan tilimaksu, varmista että kutsutaan pankin metodia tilisiirto oikealla asiakkaalla, tilinumeroilla ja summalla
Tämä on muuten copypaste ylläolevasta esimerkistä, mutta assert_called_with-metodia käytettävä, jotta voidaan tarkastaa, että parametreilla on oikeat arvot
Aloitetaan asiointi, koriin lisätään kaksi eri tuotetta, joita varastossa on ja suoritetaan ostos, varmista että kutsutaan pankin metodia tilisiirto oikealla asiakkaalla, tilinumerolla ja summalla
Aloitetaan asiointi, koriin lisätään kaksi samaa tuotetta, jota on varastossa tarpeeksi ja suoritetaan ostos, varmista että kutsutaan pankin metodia tilisiirto oikealla asiakkaalla, tilinumerolla ja summalla
Aloitetaan asiointi, koriin lisätään tuote, jota on varastossa tarpeeksi ja tuote joka on loppu ja suoritetaan ostos, varmista että kutsutaan pankin metodia tilisiirto oikealla asiakkaalla, tilinumerolla ja summalla
Muista, että kaikille testeille yhteiset alustukset on mahdollista tehdä setUp-metodissa, joka toistetaan ennen jokaista testiä:
class TestKauppa(unittest.TestCase):
def setUp(self):
self.pankki_mock = Mock()
# ...
4. Yksikkötestaus ja riippuvuudet: mock-kirjasto, osa 4
Jatketaan edellisen tehtävän koodin testaamista
Varmista, että metodin aloita_asiointi kutsuminen nollaa edellisen ostoksen tiedot (eli edellisen ostoksen hinta ei näy uuden ostoksen hinnassa), katso tarvittaessa apua tehtävän 1 projektin mock-demo testeistä!
Varmista, että kauppa pyytää uuden viitenumeron jokaiselle maksutapahtumalle. Apua löytyy jälleen tarpeen tullen mock-demon testeistä.
Tarkasta viikoilla 1 ja 2 käytetyn coveragen avulla mikä on luokan Kauppa testauskattavuus.
Jotain taitaa puuttua.
Lisää testi, joka nostaa kattavuuden noin sataan prosenttiin! Jos bugeja ilmenee, korjaa ne.
Mock-olioiden käytöstä
Mock-oliot saattoivat tuntua hieman monimutkaisilta edellisissä tehtävissä. Mockeilla on kuitenkin paikkansa. Jos testattavana olevan olion riippuvuutena oleva olio on monimutkainen, kuten esimerkiksi verkkokauppaesimerkissä luokka Pankki, kannattaa testattavana oleva olio testata ehdottomasti ilman todellisen riippuvuuden käyttöä testissä. Valeolion voi toki tehdä myös “käsin”, mutta tietyissä tilanteissa mock-kirjastoilla tehdyt mockit ovat käsin tehtyjä valeolioita kätevämpiä, erityisesti jos on syytä tarkastella testattavan olion riippuvuuksille tekemiä metodikutsuja.
5. git: tägit [versionhallinta]
Tutustutaan tässä tehtävässä Gitin tageihin:
Git has the ability to tag specific points in history as being important. Typically people use this functionality to mark release points (v1.0, and so on)
Lue ensin http://git-scm.com/book/en/v2/Git-Basics-Tagging (voit ohittaa kohdat ‘signed tags’ ja ‘verifying tags’).
Tee seuraavat samaan repositorioon, johon palautat tehtäväsi:
Tee tägi nimellä v1.0.0 (lightweight tag riittää)
Tee kolme committia (eli 3 kertaa muutos + add + commit)
Tee tägi nimellä v1.1.0
Katso gitk-komennolla miltä historiasi näyttää
Palaa tagin v.1.0.0 aikaan, eli anna komento git checkout v1.0.0
Varmista, että tagin jälkeisiä muutoksia ei näy
Palaa nykyaikaan. Tämä onnistuu komennolla git checkout main
Lisää tägi edelliseen committiin
- Operaatio onnistuu komennolla
git tag v1.0.1 HEAD^, eli HEAD^ viittaa nykyistä “headia” eli olinpaikkaa historiassa edelliseen committiin - Joissain Windowseissa muoto
HEAD^ei toimi, sen sijasta voit käyttää muotoaHEAD~ - Tai katsomalla commitin tunniste (pitkä numerosarja) joko komennolla
git logtai gitk:lla
Katso komennolla gitk miltä historia näyttää
Tagit eivät mene automaattisesti etärepositorioihin. Pushaa koodisi GitHubiin siten, että myös tagit siirtyvät mukana. Katso ohje täältä.
Varmista, että tagit siirtyvät GitHubiin:

Mitä hyötyä tageista on? Kun katsotaan commitien listaa komennolla git log, huomaamme, että Git yksilöi commitit ihmiselle hankalien tunnisteiden avulla:
commit 26c50e603aca79f02d478ca36a3d307f7ea10e14
Author: Matti Luukkainen <mluukkai@iki.fi>
Date: Mon Oct 30 16:35:04 2025 +0200
do not destroy answers if dl extended
commit 8026bd3ac416a7b1e6957d54d9296156e97571e6
Author: iPegii <51372604+iPegii@users.noreply.github.com>
Date: Sun Oct 29 14:25:31 2025 +0200
Show "Evaluation TDK" -special group in admin view
commit 0834035d0c113c7c46161c6fe8d655a9a90b2548
Merge: e5c09ae6 4dfcbf54
Author: iPegii <51372604+iPegii@users.noreply.github.com>
Date: Sun Oct 29 14:03:13 2025 +0200
Merge branch 'master' of github.com:UniversityOfHelsinkiCS/lomake
commit e5c09ae692ebf46cd0acfa15552ca3e85d7348fa
Author: iPegii <51372604+iPegii@users.noreply.github.com>
Date: Sun Oct 29 14:02:52 2025 +0200
update eslintignore to stop eslint hanging
Tagien avulla commitit on mahdollista merkitä ihmiselle selkeämmässä muodossa. Tyypillistä on merkitä tagien avulla ohjelmiston julkaistuja versioita. Jos julkaistussa ohjelmassa esiintyy bugi, on näin mahdollista palata helposti koodissa julkaisun versioon.
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/ohtu2025
Vapaaehtoinen lisätehtävä: Ostoskori TDD-tekniikalla
Jatketaan verkkokaupan parissa.
Hae kurssirepositorion hakemistossa viikko4/tdd-ostoskori oleva projekti.
Tässä tehtävässä muutamien luokkien toteutuksen logiikka on periaatteiltaan hieman erilainen kuin aiemmissa tehtävissä käsittelemässämme verkkokaupassa. Tehtävän fokuksessa on kolme luokkaa Ostoskori, Ostos ja Tuote joiden suhde on seuraava:

Ostoskori siis sisältää ostoksia, joista jokainen vastaa yhtä tiettyä tuotetta.
Luokka Tuote on hyvin suoraviivainen. Tuotteesta tiedetään nimi, hinta ja varastosaldo (jota ei tosin käytetä mihinkään):
class Tuote:
def __init__(self, nimi: str, hinta: int):
self._nimi = nimi
self._hinta = hinta
self._saldo = 0
def hinta(self):
return self._hinta
def nimi(self):
return self._nimi
def __repr__(self):
return f"{self._nimi} hinta {self._hinta} euroa"
Tuote siis kuvaa yhden tuotteen esim. Valion Plusmaito tiedot (nimi, hinta ja varastosaldo, tuotteella voisi olla myös esim. kuvaus ja muita sitä luonnehtivia kenttiä).
Ostoskoriin ei laiteta tuotteita vaan Ostoksia. Ostos viittaa tuotteeseen ja kertoo kuinka monesta tuotteesta on kysymys. Eli jos ostetaan esim. 24 maitoa, tulee ostoskoriin Ostos-olio, joka viittaa Maito-tuoteolioon, sekä kertoo, että tuotetta on korissa 24 kpl. Ostos-luokan koodi:
from tuote import Tuote
class Ostos:
def __init__(self, tuote: Tuote):
self.tuote = tuote
self._lukumaara = 1
def tuotteen_nimi(self):
return self.tuote.nimi()
def muuta_lukumaaraa(self, muutos: int):
self._lukumaara += muutos
if self._lukumaara < 0:
self._lukumaara = 0
def lukumaara(self):
return self._lukumaara
def hinta(self):
return self._lukumaara * self.tuote.hinta()
Tehtävänäsi on ohjelmoida luokka Ostoskori.
Ostoskorin API:n eli metodirajapinta on seuraava (metodien rungoissa on pass-komennot, jotta Python-tulkki ei valittaisi syntaksivirheistä):
from tuote import Tuote
from ostos import Ostos
class Ostoskori:
def __init__(self):
pass
# ostoskori tallettaa Ostos-oliota, yhden per korissa oleva Tuote
def tavaroita_korissa(self):
pass
# kertoo korissa olevien tavaroiden lukumäärän
# jos koriin lisätty 2 kpl tuotetta "maito",
# tulee metodin palauttaa 2
# jos korissa on 1 kpl tuotetta "maito" ja 1 kpl tuotetta "juusto",
# tulee metodin palauttaa 2
def hinta(self):
return 0
# kertoo korissa olevien ostosten yhteenlasketun hinnan
def lisaa_tuote(self, lisattava: Tuote):
# lisää tuotteen
pass
def poista_tuote(self, poistettava: Tuote):
# poistaa tuotteen
pass
def tyhjenna(self):
pass
# tyhjentää ostoskorin
def ostokset(self):
pass
# palauttaa listan jossa on korissa olevat ostos-oliot
# kukin ostos-olio siis kertoo mistä tuotteesta on kyse
# JA kuinka monta kappaletta kyseistä tuotetta korissa on
Kerrataan vielä: ostoskoriin lisätään Tuote-oliota metodilla lisaa_tuote. Ostoskori ei kuitenkaan talleta sisäisesti tuotteita vaan Ostos-luokan oliota (jotka viittaavat tuotteseen):

Jos ostoskoriin laitetaan useampi kappale samaa tuotetta, päivitetään vastaavaa Ostos-oliota, joka muistaa kyseisen tuotteen lukumäärän.
Ohjelmoi nyt ostoskori käyttäen Test Driven Development -tekniikkaa. Oikeaoppinen TDD etenee seuraavasti:
- Kirjoitetaan testiä sen verran että testi ei mene läpi. Ei siis luoda heti kaikkia luokan tai metodin testejä, vaan edetään yksi testi kerrallaan.
- Kirjoitetaan koodia sen verran, että testi saadaan menemään läpi. Ei yritetäkään heti kirjoittaa “lopullista” koodia.
- Jos huomataan koodin rakenteen menneen huonoksi (eli havaitaan koodissa esimerkiksi toisteisuutta tai liian pitkiä metodeja) refaktoroidaan koodin rakenne paremmaksi, ja huolehditaan koko ajan, että testit menevät edelleen läpi. Refaktoroinnilla tarkoitetaan koodin sisäisen rakenteen muuttamista siten, että sen rajapinta ja toiminnallisuus säilyy muuttumattomana.
- Jatketaan askeleesta 1
Tee seuraavat testit ja aina jokaisen testin jälkeen testin läpäisevä koodi. Jos haluat toimia oikean TDD:n hengessä, älä suunnittele koodiasi liikaa etukäteen, tee ainoastaan yksi askel kerrallaan ja paranna koodin rakennetta sitten kun koet sille tarvetta. Pidä kaikki testit koko ajan toimivina. Eli jos jokin muutos hajottaa testit, älä etene seuraavaan askeleeseen ennen kuin kaikki testit menevät taas läpi.
Luokkia Tuote ja Ostos ei tässä tehtävässä tarvitse muuttaa ollenkaan.
Lisää ja commitoi muutokset repositorioon jokaisen vaiheen jälkeen, anna kuvaava commit-viesti.
1. Luodun ostoskorin hinta ja tavaroiden määrä on 0.
Tehtäväpohjassa on yksi valmis testi
class TestOstoskori(unittest.TestCase):
def setUp(self):
self.kori = Ostoskori()
# step 1
def test_ostoskorin_hinta_ja_tavaroiden_maara_alussa(self):
self.assertEqual(self.kori.hinta(), 0)
Laajenna testiä siten, että se testaa myös tavaroiden määrän (metodin tavaroita_korissa paluuarvo). Kun testi on valmis, ohjelmoi ostoskoria sen verran että testi menee läpi. Tee ainoastaan minimaalisin mahdollinen toteutus, jolla saat testin läpi.
2. Yhden tuotteen lisäämisen jälkeen ostoskorissa on 1 tavara.
Huom: joudut siis luomaan testissäsi tuotteen jonka lisäät koriin:
class TestOstoskori(unittest.TestCase):
def setUp(self):
self.kori = Ostoskori()
# step 1
def test_ostoskorin_hinta_ja_tuotteiden_maara_alussa(self):
self.assertEqual(self.kori.hinta(), 0)
# ...
# step 2
def test_yhden_tuotteen_lisaamisen_jalkeen_korissa_yksi_tavara(self):
maito = Tuote("Maito", 3)
self.kori.lisaa_tuote(maito)
# ...
Muistutus: vaikka metodin lisaa_tuote parametrina on Tuote-olio, ostoskori ei tallenna tuotetta vaan luomansa Ostos-olion, joka “tietää” mistä tuotteesta on kysymys.
3. Yhden tuotteen lisäämisen jälkeen ostoskorin hinta on sama kuin tuotteen hinta.
4. Kahden eri tuotteen lisäämisen jälkeen ostoskorissa on 2 tavaraa
5. Kahden eri tuotteen lisäämisen jälkeen ostoskorin hinta on sama kuin tuotteiden hintojen summa
6. Kahden saman tuotteen lisäämisen jälkeen ostoskorissa on 2 tavaraa
7. Kahden saman tuotteen lisäämisen jälkeen ostoskorin hinta on sama kuin 2 kertaa tuotteen hinta
8. Yhden tuotteen lisäämisen jälkeen ostoskori sisältää yhden ostoksen
tässä testataan ostoskorin metodia ostokset:
# step 8
def test_yhden_tuotteen_lisaamisen_jalkeen_korissa_yksi_ostosolio(self):
maito = Tuote("Maito", 3)
self.kori.lisaa_tuote(maito)
ostokset = self.kori.ostokset()
# testaa että metodin palauttaman listan pituus 1
9. Yhden tuotteen lisäämisen jälkeen ostoskori sisältää ostoksen, jolla sama nimi kuin tuotteella ja lukumäärä 1
Testin on siis tutkittava jälleen korin metodin ostokset palauttamaa listaa:
# step 9
def test_yhden_tuotteen_lisaamisen_jalkeen_korissa_yksi_ostosolio_jolla_oikea_tuotteen_nimi_ja_maara(self):
maito = Tuote("Maito", 3)
self.kori.lisaa_tuote(maito)
ostos = self.kori.ostokset()[0]
# testaa täällä, että palautetun listan ensimmäinen ostos on halutunkaltainen.
10. Kahden eri tuotteen lisäämisen jälkeen ostoskori sisältää kaksi ostosta
11. Kahden saman tuotteen lisäämisen jälkeen ostoskori sisältää yhden ostoksen
Eli jos korissa on jo ostos “maito” ja koriin lisätään uusi “maito”, tulee tämän jälkeen korissa olla edelleen vain yksi ostos “maito”, lukumäärän tulee kuitenkin kasvaa kahteen.
12. Kahden saman tuotteen lisäämisen jälkeen ostoskori sisältää ostoksen, jolla sama nimi kuin tuotteella ja lukumäärä 2
13. Jos korissa on kaksi samaa tuotetta ja toinen näistä poistetaan, jää koriin ostos jossa on tuotetta 1 kpl
14. Jos koriin on lisätty tuote ja sama tuote poistetaan, on kori tämän jälkeen tyhjä
Tyhjä kori tarkoittanee että tuotteita ei ole, korin hinta on nolla ja ostoksien listan pituus nolla
15. Metodi tyhjenna tyhjentää korin
Jos ostoskorissasi on mukana jotain ylimääräistä, refaktoroi koodiasi niin että kaikki turha poistuu.
Erityisesti ylimääräisistä oliomuuttujista kannattaa hankkiutua eroon, tarvitset luokalle vain yhden oliomuuttujan, kaikki ylimääräiset tekevät koodista sekavamman ja vaikeammin ylläpidettävän.
Tehtävää ei tarvitse palauttaa, eikä siitä saa kurssipisteitä. Palkkio tehtävästä on lisääntynyt osaaminen ja toivottavasti hyvä mieli!