Kihagyás

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. Ez a prekondícióból ered, hiszen egy előfeltételt vizsgálunk.

1
2
valasz = jatszunk_meg()
assert valasz

Amennyiben egy metódus helyességét akarjuk tesztelni, az assertet egy egyszerű feltételvizsgálattal kell meghívnunk.

1
2
valasz = square(3)
assert valasz == 9

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 library beimportálására. Ezek után tudja a fordító kezelni a weboldaltól kapott status code-ot. 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. Mivel a get nem egy egyszerű stringgel vagy inttel tér vissza hanem egészen pontosan 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.

1
2
web = requests.get("https://google.com")
assert web.status_code == 200

Teljes példa

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
def square(x):
    """Return the square of x.

    >>> square(2)
    6
    >>> square(-2)
    4
    """

    return x * x


if __name__ == '__main__':
    import doctest
    doctest.testmod()

Az alábbi hibaüzenetet kapjuk

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
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 az osztálynevével, esetleg lehet bővíteni, például TestKoPapirOlloOnlyWrongUsages.

1
2
3
4
5
6
7
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 objektumokat.

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.

1
self.assertEqual(AmitVizsgálunk, AmilyenÉréketVárunk, ÜzenetHaNemEgyenlő)

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.

1
self.assertFalse(self.game.akarmi())

Az alábbi példában látható, hogy az assertTrue/assertFalse használata néha csalóka lehet, a konverzió miatt:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
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.

1
self.assertIs(ElsoKiérékelés,MásodikKiértékelés,ÜzenetHaNemAronos)

Az előző példában látott nem teljesen jó vizsgálatok helyesen:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
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.

1
self.assertIn(AmitKeresünk, AmibenKeressük, ÜzenetHaNincsBenne)

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.

1
pip install mock

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).

1
2
3
4
@mock.patch("builtins.input")
def test_jatszunk_meg_true(self, inp):
    inp.return_value = "igen"
    self.assertIs(self.game.jatszunk_meg(), True)

Context manager használhatával:

1
2
3
4
def test_jatszunk_meg_true(self):
    with mock.patch("builtins.input") as inp:
        inp.return_value = "igen"
        self.assertIs(self.game.jatszunk_meg(), True)

Abban az esetben, ha szeretnénk a standart outputot is elkapni, a mockot és a StringIO-t kell közösen használni. Miután beimportáltuk a StringIO-t, a 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.)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
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())

Az utolsó osztály, amit megnézünk, az a MagicMock, ami egy mindenes objektum, a mi esetünkben az 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.

1
2
3
4
5
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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
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()

Feladatok

  1. A tesztek bővítése, hogy minden funkció tesztelve legyen.

  2. Korábbi, webről letöltős kódhoz teszt készítése.


Utolsó frissítés: 2021-05-31 12:08:53