8. gyakorlat¶
A gyakorlat anyaga¶
Assertek¶
Az assert
utasítás egy prekondíciós eszköz, amit arra használunk, hogy a program futása során egy előfeltételt teszteljünk, ami a program futása során később szükséges. Az assertnek igaz értéket kell kapnia annak érdekében hogy tovább fusson a program.
Amennyiben egy metódus helyességét akarjuk tesztelni, az assertet egy egyszerű feltételvizsgálattal kell meghívnunk.
A fenti példában látszik, hogy meghívtuk a square
függvényt, aminek az értékét a valasz
változóban eltároltuk, az assertben pedig ennek a változónak az értékét ellenőrizzük.
A test_answer
függvény harmadik elemének futásához szükségünk van a requests
függvénykönyvtár beimportálására (esetlegesen telepítésére).
Ebben az esetben a teszt arra irányul hogy a weboldal a 200-as kóddal tér-e vissza a GET kérés után.
A get
metódus nem egy egyszerű stringgel vagy egész számmal tér vissza, hanem egy Response
objektummal.
A status_code
használatával lekérhetjük az Response objektumtól, hogy a kérés milyen HTTP kóddal tér vissza.
Ezt már az előzőekben látott módszerrel tudjuk ellenőrizni, hogy helyes értéket kaptunk-e.
Teljes példa¶
import requests
def jatszunk_meg():
i = input(f"Játszunk még? ")
return i.lower() in ["i", "y", "yes", "igen", "nana"]
def square(x):
return x * x
def test_answer():
valasz = jatszunk_meg()
assert valasz
valasz = square(3)
assert valasz == 9
web = requests.get("https://google.com")
print(web)
print(type(web))
print(web.status_code)
print(type(web.status_code))
assert web.status_code == 200
print("Minden lefutott!")
test_answer()
Tudni kell az assertekről, hogy ha így akarjuk tesztelni a fügvényeinket, és nem jó értéket kap az program, az a programfutás elszálláshoz vezet, így egységtesztelésre ez a módszer nem feltétlenül ajánlott.
Doctest¶
A metódus helyességének tesztelésére egy sokkal elegánsabb módeszer a doctest használata. Ez a Pythonban egy beépített library, amit kifejezetten függvény tesztelésre alakítottak ki. Ennek az az előnye, hogy meg tudunk adni egy várt értéket amivel a függvénynek vissza kellene térnie a program dokumentációjában. Amennyiben az érték hibás, a Python egy rövid üzenettel jelzi számunkra hogy milyen értéket várt és, hogy milyen értéket kapott. Fontos, hogy ez csak a doctest futtatásakor derül(het) ki, a program normál működése során nem.
Hibás kód esetén mint például az alábbi.
def square(x):
"""Return the square of x.
>>> square(2)
6
>>> square(-2)
4
"""
return x * x
if __name__ == '__main__':
import doctest
doctest.testmod()
Failed example:
square(2)
Expected:
6
Got:
4
**********************************************************************
1 items had failures:
1 of 2 in __main__.square
***Test Failed*** 1 failures.
Process finished with exit code 0
Jól látszik, hogy melyik teszt milyen eredményt várt és kapott. Ha nincs hiba a tesztben, akkor a standard "Process finished with exit code 0"-ot fogjuk látni.
def square(x):
"""Return the square of x.
>>> square(2)
4
>>> square(-2)
4
"""
return x * x
if __name__ == '__main__':
import doctest
doctest.testmod()
Unittest¶
A tesztelésben a leggyakrabban használt eszköz a unittest, erre egy rövid példa amit lentebb látszik. Elsőnek meg kell alkotnunk az osztályt amit tesztelni akarunk.
import random
class KoPapirOllo:
def __init__(self, nev):
self.valasztas = None
self.gep = None
self.nev = nev
self.gyozelem = 0
self.vereseg = 0
self.dontetlen = 0
def jatszunk_meg(self):
i = input(f"Játszunk még, {self.nev}? ")
return i.lower() in ["i", "y", "yes", "igen", "nana"]
def jatekos_valaszt(self):
i = input("Mit választasz? (kő/papír/olló) ")
i = i.lower()
if i in ["kő", "ko", "k"]:
return "ko"
if i in ["papír", "papir", "p"]:
return "papir"
if i in ["olló", "ollo", "o"]:
return "ollo"
def gep_valaszt(self):
return random.choice(("ko", "papir", "ollo"))
def jatek(self):
self.valasztas = self.jatekos_valaszt()
self.gep = self.gep_valaszt()
print(f"A te választásod {self.valasztas}, a gép választása pedig {self.gep}")
if self.valasztas == self.gep:
print("Döntetlen, necces volt.")
if self.valasztas == "ko" and self.gep == "ollo":
print("Játékos nyert")
if self.valasztas == "ko" and self.gep == "papir":
print("Gép nyert")
if self.valasztas == "papir" and self.gep == "ollo":
print("Gép nyert")
if self.valasztas == "papir" and self.gep == "ko":
print("Játékos nyert")
if self.valasztas == "ollo":
if self.gep == "ko":
print("Gép nyert")
if self.gep == "papir":
print("Játékos nyert")
def statisztika(self):
print(f"Összes játék: {self.gyozelem + self.vereseg + self.dontetlen}")
print("Győzelem", self.gyozelem)
print("Vereség", self.vereseg)
print("Döntetlen", self.dontetlen)
def akarmi(self):
return None
def akarmi2(self, param):
print(len(param))
print(param)
print(param)
print(param)
def main():
print("Üdv a kő-papír-olló játékban.")
nev = input("Add meg a neved: ")
game = KoPapirOllo(nev)
jatek = True
while jatek:
game.jatek()
jatek = game.jatszunk_meg()
if __name__ == '__main__':
main()
Ahhoz hogy tesztelhessük a fenti kis játékunkat, szükségünk van a unittest
beimportálására és egy tesztelő osztályra, ami jelen esetben a TestKoPapirOllo
nevet kapta, és a unittest.TestCase
osztályból öröklődik. Minden osztályhoz legalább egy tesztosztály szokott tartozni, ami megegyezik a Test
+osztálynévvel, esetleg lehet bővíteni, például TestKoPapirOlloOnlyWrongUsages
.
import kpo
import unittest
class TestKoPapirOllo(unittest.TestCase):
def setUp(self):
self.game = kpo.KoPapirOllo("VALAMI")
A setUp
metódusunk egy inicializáló metódus ami minden teszt előtt (minden egyes tesztmetódus meghívása előtt) lefut. Általában itt szokás inicializálni a metódusok által közösen használt objektumokat. Persze, ha valamilyen erőforrást nyit meg a setUp
, akkor ezt a tearDown
segítségével lehet lezárni a tesztelést követően.
assertEqual¶
Nagyon hasonlít a fentebb bemutatott assertekre. Három paramétere van, az első kettő megadása szükséges, harmadik opcionális paraméterként vár egy szöveget, amit kiír, amennyiben nem egyezik meg a két érték.
assertTrue/False¶
Az assertTrue és assertFalse megvizsgálja, hogy igaz/hamis-e a paraméter. Itt kicsit óvatosabbnak kell lennünk a tesztelésnél, mivel itt könnyen hibába futhatunk, ugyanis itt történhet típuskonverzió, például a None
hamissá értékelődik ki (truthy vs. falsy).
Az alábbi példában látható, hogy az assertTrue/assertFalse használata néha csalóka lehet, a konverzió miatt:
class CsalokaAssertek(unittest.TestCase):
def testMindIgaz(self):
self.assertTrue(True)
self.assertTrue(21)
self.assertTrue("szoveg")
self.assertTrue(0.0000001)
def testMindHamis(self):
self.assertFalse(False)
self.assertFalse(0)
self.assertFalse("")
self.assertFalse(None)
assertIs¶
Az assertIs segítségével konkrét objektum egyezőséget vizsgálhatunk. Az assertTrue/assertFalse esetén látott problémákat így lehet megoldani. Technikailag megegyezik a self.assertTrue(a is b)
utasítással.
class Tesztek(unittest.TestCase):
def testTrue(self):
self.assertIs(fuggveny(), True)
def testFalse(self):
self.assertIs(fuggveny(), False)
self.assertIs(fuggveny(), 0)
self.assertIs(fuggveny(), "")
self.assertIs(fuggveny(), None)
assertIn¶
Az assertIn-t arra használhatjuk, hogy egy adott elemet megvizsgáljunk, hogy tartalmazza-e egy adott kollekció. Gyakorlatilag megegyezik az self.assertTrue(a in b)
utasítással, azonban jobb hibaüzenetet ad, amennyiben nem igaz a kifejezés.
Mock¶
Mivel a tesztjeink során nem feltételezhetjük azt, hogy mindig megfelelő internetkapcsolat áll rendelkezésre, sem pedig azt, hogy a tesztek futása alatt a gép előtt ott fog ülni egy ember, aki beírjon valamit, ha inputot vár a program (sőt, olyat, amilyen a teszt elvár!). A teszteket automatikusan szeretnénk futtatni, bármiféle emberi beavatkozás nélkül, nem biztos, hogy olyan gépen fog futni, ahol van internet, stb.
Illetve azt sem várhatjuk el, hogy egy egység tesztelése során egy másik modul, szolgáltatás garantáltan jól működik, így célszerű ezeket a metódusokat kimockolni. Ilyenkor egy objektumot vagy metódust elfedünk, vagyis a rendes működése helyett mi specifikálunk neki egy elvárt jó működést (például egy get kérés esetén mindig egy adott HTML-lel térünk vissza). Ez az egységek helyes működését biztosíthatja, de nem helyettesíti az egységek közötti integrációs teszteket!
Mivel nem beépített modul, a pip segítségével telepíthetjük a PyPI-ből.
A mock egyik lehetősége a patchelés, amit dekorátorként, vagy pedig kontextuskezelővel is használhatjuk. Amennyiben dekorátorként használjuk a patch metódust, szükséges megadnunk egy extra paramétert is a tesztfüggvénynek, ami által elérhetjük a mockolt objektumunkat (a példában a beépített input függvényt mockoljuk).
@mock.patch("builtins.input")
def test_jatszunk_meg_true(self, inp):
inp.return_value = "igen"
self.assertIs(self.game.jatszunk_meg(), True)
def test_jatszunk_meg_true(self):
with mock.patch("builtins.input") as inp:
inp.return_value = "igen"
self.assertIs(self.game.jatszunk_meg(), True)
mock.patch
-ben megadhatjuk, hogy a standard kimenetet hova szeretnénk átirányítani. (A standart output átirányítására már korábban láthattunk egy másik megoldás is.)
from io import StringIO
#...
@mock.patch("kpo.KoPapirOllo.jatekos_valaszt", return_value="ko")
@mock.patch("kpo.KoPapirOllo.gep_valaszt", return_value="ko")
def test_jatek_dontetlen(self, gep, jatekos):
with mock.patch('sys.stdout', new=StringIO()) as fake_out:
self.game.jatek()
self.assertIn("Döntetlen", fake_out.getvalue())
akarmi2
függvénynél vesszük hasznát, ami egy objektumot vár paraméterben, és megnézi a méretét, kiírja az alapértelmezett kimenetre. Erre használhatjuk a MagicMock osztályt, aminek bármilyen tulajdonságát lekérhetjük, bármilyen függvényét meghívhatjuk, nem kapunk hibát.
Az assert_called_once
metódus azt ellenőrzi, hogy meg lett-e hívva az adott függvény. Az assert_called
azt ellenőrzi, hogy az adott függvény meg lett-e hívva legalább egyszer.
def test_akarmi2(self):
with mock.MagicMock() as mock_obj:
self.game.akarmi2(mock_obj)
mock_obj.__len__.assert_called_once()
mock_obj.__str__.assert_called()
Teljes példa¶
import kpo
from io import StringIO
import mock
import unittest
class TestKoPapirOllo(unittest.TestCase):
def setUp(self):
self.game = kpo.KoPapirOllo("VALAMI")
def test_init(self):
self.assertEqual(self.game.nev, "VALAMI", "Nem megfelelő név")
self.assertEqual(self.game.gyozelem, 0, "Nem megfelelő gyozelem")
self.assertEqual(self.game.vereseg, 0, "Nem megfelelő vereseg")
self.assertEqual(self.game.dontetlen, 0, "Nem megfelelő dontetlen")
def test_gep_valaszt(self):
self.assertIn(self.game.gep_valaszt(), ["ko", "papir", "ollo"])
def test_gep_valaszt_called(self):
with mock.patch('random.choice') as mock_choice:
gep_valasztasa = self.game.gep_valaszt()
mock_choice.assert_called_once()
@mock.patch("builtins.input")
def test_jatszunk_meg_true(self, inp):
inp.return_value = "igen"
self.assertIs(self.game.jatszunk_meg(), True)
@mock.patch("builtins.input", return_value="nem")
def test_jatszunk_meg_false(self, inp):
inp.return_value = "nem"
self.assertIs(self.game.jatszunk_meg(), False)
inp.assert_called_once()
@mock.patch("kpo.KoPapirOllo.jatekos_valaszt", return_value="ko")
@mock.patch("kpo.KoPapirOllo.gep_valaszt", return_value="ko")
def test_jatek_dontetlen(self, gep, jatekos):
with mock.patch('sys.stdout', new=StringIO()) as fake_out:
self.game.jatek()
self.assertIn("Döntetlen", fake_out.getvalue())
def test_akarmi(self):
self.assertFalse(self.game.akarmi())
self.assertIs(self.game.akarmi(), False)
def test_akarmi2(self):
with mock.MagicMock() as mock_obj:
self.game.akarmi2(mock_obj)
mock_obj.__len__.assert_called_once()
mock_obj.__str__.assert_called()
if __name__ == '__main__':
unittest.main()
Órai feladatok¶
Megjegyzés
Az órai feladatok alatt lévő feladat(sor)ok azok a feladat(sor)ok, amelyek megoldása beleszámít a zh javításért megoldandó feladatok közé. Ezen feladat(sor)okból 6 darab megoldása szükséges ahhoz, hogy egy zh javíthatóvá váljon. Ha külön nem jelezzük, a teljes feladatsor megoldása esetén jár a zh javításért szerezhető pont.
Info
Az 1. feladat megoldása szükséges a zh javításért járó pont megszerzéséért.
- Teszteljük az alábbi kódot:
class Student:
def __init__(self, name="Nincs nevem", age=12):
self.name = name # aktualis objektum vs. parameter neve
self.age = age
self._grades = list() # ures lista (tomb)
def add_grade(self, grade):
self._grades.append(int(grade))
def avg(self):
sum_grades = 0
for grade in self._grades:
sum_grades += grade
return sum_grades/len(self._grades)
# return sum(self.grades) / len(self.grades)
def max_grade(self):
return max(self._grades)
def min_grade(self):
return min(self._grades)
def __eq__(self, other):
return self.__dict__ == other.__dict__
class ClassRoom:
def __init__(self):
self._students = list()
def new_student(self, student):
if isinstance(student, Student):
self._students.append(student)
else:
print("You can't do that")
def avg(self):
avgs = [student.avg() for student in self._students]
return sum(avgs) / len(avgs)
def __str__(self):
return ("[Just a class, which consist of "
"%d student(s).]" % len(self._students))
def __eq__(self, other):
return self.__dict__ == other.__dict__
def __add__(self, other):
if isinstance(other, ClassRoom):
returned = ClassRoom()
returned._students = self._students + other._students
return returned
-
A tesztek bővítése, hogy minden funkció tesztelve legyen.
-
Korábbi, webről letöltős kódhoz teszt készítése.