Kihagyás

Egységtesztelés alapjai

Szerkesztés alatt!

Az oldal további része szerkesztés alatt áll, a tartalma minden további értesítés nélkül többször, gyakran, jelentősen megváltozhat!

Tesztelni sokféleképpen és több szinten lehet. Tesztelési szintek például az egység, egység-integrációs, és rendszerteszt szintek. Ezekből az "alap" az egységteszt. Ez a program (általában a megvalósításhoz köthető) kisebb egységeit (függvény, osztály, modul) egymástól függetlenül, önállóan teszteli. Ha már a program valamelyik önálló kis egysége önmagában hibás, akkor mit várunk az ilyen egységek együttműködésétől (integrációs teszt) vagy a teljes rendszertől (rendszerteszt)?

No de hogyan lehet a program egységeit, itt most tipikusan az egyes függvényeit önmagukban tesztelni? Ehhez egy olyan tesztprogram kell, ami sorban meghívja ezeket, és ellenőrzi a visszaadott eredményeket. Az ilyen tesztek implementációja egyrészt ugyanazon a programozási nyelven történik, mint amiben az adott egység meg van írva, másrészt valamilyen keretrendszert szokás hozzájuk használni. Python nyelvre egy ilyen keretrendszer a pytest. (Használatához telepíteni kell a pytest csomagot.)

A pytest telepítése linux alatt sudo/root jogokkal mindenki számára

Ha van sudo jogod egy linuxos gépen (tudsz root jogokkal futtatni programokat), akkor a pytest-et tudod úgy telepíteni, hogy az minden felhasználó számára elérhető legyen:

1
$ sudo pip3 install pytest
vagy ha a fenti parancs azt mondja, hogy az adott környezet "külsőleg menedzselt" ("This environment is externally managed"), akkor jó eséllyel:
1
$ sudo apt install python3-pytest

A pytest telepítése sudo/root jog nélkül

Ha nincs megfelelő jogod, akkor a

1
$ sudo pip3 install pytest
csak a te részedre fogja telepíteni a pytest csomagot, illetve beállítástól függően az is lehet, hogy eleve csak virtualenv környezetben tudod majd telepíteni.

Példa

Adott egy képmanipuláló lib és program(család), ahol is most a libpnm-hez adunk egységteszteket. A pytest keretrendszer szerencsére segít bennünket.

A pytest alapvetően név alapján ismeri fel a teszteket. A test_ prefixszel rendelkező függvényeket tekinti teszteknek, és ezeket fogja futtatni. Amelyik teszt AssertionError-t dob, az bukott, amelyik nem, az átment.

Az egységtesztek megvalósítása

test/conftest.py

 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
"""
Szegedi Tudományegyetem
Informatikai Tanszékcsoport
Szoftverfejlesztés Tanszék
cc-by-bc-sa
"""

import pytest
import libpnm

TPW = 4
TPH = 6
TPM = (TPW - 1) * (TPH - 1)

def RED(X,Y):
    return (TPH - 1) * X

def GRN(X,Y):
    return (TPW - 1) * Y

def BLU(X,Y):
    return (TPM-TPH-TPW-2) + X + Y

def assert_pic_equals(pic, ref):
    for attr in ('width', 'height', 'maxi'):
        assert pic[attr] == ref[attr], f"Attribute: {attr}"
    for y in range(ref['height']):
        for x in range(ref['width']):
            assert pic['pixels'][y][x] == ref['pixels'][y][x], f"Position (row, col): {y},{x}"

@pytest.fixture
def default_picture():
    px = [ [ [RED(x,y), GRN(x,y), BLU(x,y)] for x in range(TPW) ] for y in range(TPH) ]
    return {
        'width':  TPW,
        'height': TPH,
        'maxi':   TPM,
        'pixels': px
    }

@pytest.fixture
def black_picture():
    px = [ [ [0, 0, 0] for x in range(TPW) ] for y in range(TPH) ]
    return {
        'width':  TPW,
        'height': TPH,
        'maxi':   TPM,
        'pixels': px
    }

test/test_type.py

 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
"""
Szegedi Tudományegyetem
Informatikai Tanszékcsoport
Szoftverfejlesztés Tanszék
cc-by-bc-sa
"""

import libpnm
import conftest as ct
import pytest

def test_rgbpic_create(black_picture):
    pic = libpnm.pnm_type.create(ct.TPW, ct.TPH, ct.TPM)
    assert pic is not None
    ct.assert_pic_equals(pic, black_picture)

def test_rgbpic_rgb_to_gray():
    black  = (  0,   0,   0)
    red    = (100,   0,   0)
    green  = (  0, 100,   0)
    blue   = (  0,   0, 100)
    yellow = (100, 100,   0)
    cyan   = (  0, 100, 100)
    purple = (100,   0, 100)
    white  = (100, 100, 100)
    assert libpnm.pnm_type.rgb_to_gray(black)  ==   0
    assert libpnm.pnm_type.rgb_to_gray(red)    ==  30
    assert libpnm.pnm_type.rgb_to_gray(green)  ==  59
    assert libpnm.pnm_type.rgb_to_gray(blue)   ==  11
    assert libpnm.pnm_type.rgb_to_gray(yellow) ==  89
    assert libpnm.pnm_type.rgb_to_gray(cyan)   ==  70
    assert libpnm.pnm_type.rgb_to_gray(purple) ==  41
    assert libpnm.pnm_type.rgb_to_gray(white)  == 100

def test_load():
    with pytest.raises(FileNotFoundError):
        pic = libpnm.pnm_type.load('nonexistent.file')

test/test_manipulation.py

  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
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
"""
Szegedi Tudományegyetem
Informatikai Tanszékcsoport
Szoftverfejlesztés Tanszék
cc-by-bc-sa
"""

import libpnm
import conftest as ct

def test_invert(default_picture):
    libpnm.pnm_manipulation.invert(default_picture)
    ref = libpnm.pnm_type.create(ct.TPW, ct.TPH, ct.TPM)
    for y in range(ct.TPH):
        for x in range(ct.TPW):
            ref['pixels'][y][x] = [ct.TPM-ct.RED(x,y), ct.TPM-ct.GRN(x,y), ct.TPM-ct.BLU(x,y)]
    ct.assert_pic_equals(default_picture, ref)

def test_invert_R(default_picture):
    libpnm.pnm_manipulation.invert_R(default_picture)
    ref = libpnm.pnm_type.create(ct.TPW, ct.TPH, ct.TPM)
    for y in range(ct.TPH):
        for x in range(ct.TPW):
            ref['pixels'][y][x] = [ct.TPM-ct.RED(x,y), ct.GRN(x,y), ct.BLU(x,y)]
    ct.assert_pic_equals(default_picture, ref)

def test_invert_G(default_picture):
    libpnm.pnm_manipulation.invert_G(default_picture)
    ref = libpnm.pnm_type.create(ct.TPW, ct.TPH, ct.TPM)
    for y in range(ct.TPH):
        for x in range(ct.TPW):
            ref['pixels'][y][x] = [ct.RED(x,y), ct.TPM-ct.GRN(x,y), ct.BLU(x,y)]
    ct.assert_pic_equals(default_picture, ref)

def test_invert_B(default_picture):
    libpnm.pnm_manipulation.invert_B(default_picture)
    ref = libpnm.pnm_type.create(ct.TPW, ct.TPH, ct.TPM)
    for y in range(ct.TPH):
        for x in range(ct.TPW):
            ref['pixels'][y][x] = [ct.RED(x,y), ct.GRN(x,y), ct.TPM-ct.BLU(x,y)]
    ct.assert_pic_equals(default_picture, ref)

def test_mirror_h(default_picture):
    libpnm.pnm_manipulation.mirror_h(default_picture)
    ref = libpnm.pnm_type.create(ct.TPW, ct.TPH, ct.TPM)
    for y in range(ct.TPH):
        for x in range(ct.TPW):
            ref['pixels'][y][ct.TPW - x - 1] = [ct.RED(x,y), ct.GRN(x,y), ct.BLU(x,y)]
    ct.assert_pic_equals(default_picture, ref)

def test_mirror_v(default_picture):
    libpnm.pnm_manipulation.mirror_v(default_picture)
    ref = libpnm.pnm_type.create(ct.TPW, ct.TPH, ct.TPM)
    for y in range(ct.TPH):
        for x in range(ct.TPW):
            ref['pixels'][ct.TPH - y - 1][x] = [ct.RED(x,y), ct.GRN(x,y), ct.BLU(x,y)]
    ct.assert_pic_equals(default_picture, ref)

def test_rotate_l(default_picture):
    libpnm.pnm_manipulation.rotate_l(default_picture)
    ref = libpnm.pnm_type.create(ct.TPH, ct.TPW, ct.TPM)
    for y in range(ct.TPH):
        for x in range(ct.TPW):
            ref['pixels'][ct.TPW - x - 1][y] = [ct.RED(x,y), ct.GRN(x,y), ct.BLU(x,y)]
    ct.assert_pic_equals(default_picture, ref)

def test_mirror_c(default_picture):
    libpnm.pnm_manipulation.mirror_c(default_picture)
    ref = libpnm.pnm_type.create(ct.TPW, ct.TPH, ct.TPM)
    for y in range(ct.TPH):
        for x in range(ct.TPW):
            ref['pixels'][ct.TPH - y - 1][ct.TPW - x - 1] = [ct.RED(x,y), ct.GRN(x,y), ct.BLU(x,y)]
    ct.assert_pic_equals(default_picture, ref)

def test_rotate_r(default_picture):
    libpnm.pnm_manipulation.rotate_r(default_picture)
    ref = libpnm.pnm_type.create(ct.TPH, ct.TPW, ct.TPM)
    for y in range(ct.TPH):
        for x in range(ct.TPW):
            ref['pixels'][x][ct.TPH - y - 1] = [ct.RED(x,y), ct.GRN(x,y), ct.BLU(x,y)]
    ct.assert_pic_equals(default_picture, ref)

def test_color_RG(default_picture):
    libpnm.pnm_manipulation.color_RG(default_picture)
    ref = libpnm.pnm_type.create(ct.TPW, ct.TPH, ct.TPM)
    for y in range(ct.TPH):
        for x in range(ct.TPW):
            ref['pixels'][y][x] = [ct.GRN(x,y), ct.RED(x,y), ct.BLU(x,y)]
    ct.assert_pic_equals(default_picture, ref)

def test_color_GB(default_picture):
    libpnm.pnm_manipulation.color_GB(default_picture)
    ref = libpnm.pnm_type.create(ct.TPW, ct.TPH, ct.TPM)
    for y in range(ct.TPH):
        for x in range(ct.TPW):
            ref['pixels'][y][x] = [ct.RED(x,y), ct.BLU(x,y), ct.GRN(x,y)]
    ct.assert_pic_equals(default_picture, ref)

def test_color_BR(default_picture):
    libpnm.pnm_manipulation.color_BR(default_picture)
    ref = libpnm.pnm_type.create(ct.TPW, ct.TPH, ct.TPM)
    for y in range(ct.TPH):
        for x in range(ct.TPW):
            ref['pixels'][y][x] = [ct.BLU(x,y), ct.GRN(x,y), ct.RED(x,y)]
    ct.assert_pic_equals(default_picture, ref)

def test_delete_R(default_picture):
    libpnm.pnm_manipulation.delete_R(default_picture)
    ref = libpnm.pnm_type.create(ct.TPW, ct.TPH, ct.TPM)
    for y in range(ct.TPH):
        for x in range(ct.TPW):
            ref['pixels'][y][x] = [0, ct.GRN(x,y), ct.BLU(x,y)]
    ct.assert_pic_equals(default_picture, ref)

def test_delete_G(default_picture):
    libpnm.pnm_manipulation.delete_G(default_picture)
    ref = libpnm.pnm_type.create(ct.TPW, ct.TPH, ct.TPM)
    for y in range(ct.TPH):
        for x in range(ct.TPW):
            ref['pixels'][y][x] = [ct.RED(x,y), 0, ct.BLU(x,y)]
    ct.assert_pic_equals(default_picture, ref)

def test_delete_B(default_picture):
    libpnm.pnm_manipulation.delete_B(default_picture)
    ref = libpnm.pnm_type.create(ct.TPW, ct.TPH, ct.TPM)
    for y in range(ct.TPH):
        for x in range(ct.TPW):
            ref['pixels'][y][x] = [ct.RED(x,y), ct.GRN(x,y), 0]
    ct.assert_pic_equals(default_picture, ref)

def test_convert_to_grey_avg(default_picture):
    libpnm.pnm_manipulation.convert_to_grey_avg(default_picture)
    ref = libpnm.pnm_type.create(ct.TPW, ct.TPH, ct.TPM)
    for y in range(ct.TPH):
        for x in range(ct.TPW):
            mono = (ct.RED(x,y) + ct.GRN(x,y) + ct.BLU(x,y)) // 3
            ref['pixels'][y][x] = [mono, mono, mono]
    ct.assert_pic_equals(default_picture, ref)

def test_convert_to_grey_lum(default_picture):
    libpnm.pnm_manipulation.convert_to_grey_lum(default_picture)
    ref = libpnm.pnm_type.create(ct.TPW, ct.TPH, ct.TPM)
    for y in range(ct.TPH):
        for x in range(ct.TPW):
            mono = (30 * ct.RED(x,y) + 59 * ct.GRN(x,y) + 11 * ct.BLU(x,y)) // 100
            ref['pixels'][y][x] = [mono, mono, mono]
    ct.assert_pic_equals(default_picture, ref)

def test_convert_R_to_grey(default_picture):
    libpnm.pnm_manipulation.convert_R_to_grey(default_picture)
    ref = libpnm.pnm_type.create(ct.TPW, ct.TPH, ct.TPM)
    for y in range(ct.TPH):
        for x in range(ct.TPW):
            ref['pixels'][y][x] = [ct.RED(x,y), ct.RED(x,y), ct.RED(x,y)]
    ct.assert_pic_equals(default_picture, ref)

def test_convert_G_to_grey(default_picture):
    libpnm.pnm_manipulation.convert_G_to_grey(default_picture)
    ref = libpnm.pnm_type.create(ct.TPW, ct.TPH, ct.TPM)
    for y in range(ct.TPH):
        for x in range(ct.TPW):
            ref['pixels'][y][x] = [ct.GRN(x,y), ct.GRN(x,y), ct.GRN(x,y)]
    ct.assert_pic_equals(default_picture, ref)

def test_convert_B_to_grey(default_picture):
    libpnm.pnm_manipulation.convert_B_to_grey(default_picture)
    ref = libpnm.pnm_type.create(ct.TPW, ct.TPH, ct.TPM)
    for y in range(ct.TPH):
        for x in range(ct.TPW):
            ref['pixels'][y][x] = [ct.BLU(x,y), ct.BLU(x,y), ct.BLU(x,y)]
    ct.assert_pic_equals(default_picture, ref)

def test_convert_to_bw_avg(default_picture):
    libpnm.pnm_manipulation.convert_to_bw_avg(default_picture)
    ref = libpnm.pnm_type.create(ct.TPW, ct.TPH, ct.TPM)
    for y in range(ct.TPH):
        for x in range(ct.TPW):
            mono = int(2 * (ct.RED(x,y) + ct.GRN(x,y) + ct.BLU(x,y)) >= 3 * ct.TPM) * ct.TPM
            ref['pixels'][y][x] = [mono, mono, mono]
    ct.assert_pic_equals(default_picture, ref)

def test_convert_to_bw_lum(default_picture):
    libpnm.pnm_manipulation.convert_to_bw_lum(default_picture)
    ref = libpnm.pnm_type.create(ct.TPW, ct.TPH, ct.TPM)
    for y in range(ct.TPH):
        for x in range(ct.TPW):
            mono = int((30 * ct.RED(x,y) + 59 * ct.GRN(x,y) + 11 * ct.BLU(x,y)) // 50 > ct.TPM) * ct.TPM
            ref['pixels'][y][x] = [mono, mono, mono]
    ct.assert_pic_equals(default_picture, ref)

A test_type.py tartalmazza a pnm_type modul, a test_manipulation.py pedig a pnm_manipulation modul tesztjeit. A test_manipulation.py pedig egyrészt a tesztkörnyezet beállításait, illetve a tesztekben használt segédfüggvényeket, konstansokat, fixture-öket, stb.

Futtatás

A tesztek futtatása a

1
python -m pytest
parancs segítségével történik. A pytest ilyenkor megkeresi a test_ kezdetű python fájlokat, majd ezekből lefuttatja a test_ kezdetű függvényeket.

Tippek

Egy assert per teszt

Szokás azt mondani, hogy egy teszteset egyetlen assert-et tartalmazzon, de ez néha nagyon elaprózhatja a teszteket. A lényeg, hogy egy teszteset logikailag egy dolgot teszteljen. Ahhoz például, hogy vajon két kép teljesen egyforma-e nyilván több ellenőrzés szükséges. Ezeket vagy külön-külön ellenőriztetjük egy-egy assert segítségével, vagy mi magunk összegezzük, és a végén mondunk egy pass-t vagy FAIL-t.

Több teszt per unit

Egy függvényhez több tesztfüggvényt is lehet írni, ami ilyen-olyan adatokkal próbálja ki. Lehet például a határérték analízis alapján egy függvényhez több külön tesztesetet rendelni, hogy lássuk, melyik határértéknél van a hiba.

Tesztesetek nevei

Unit tesztek esetén a tesztek (tesztfüggvények) neveit a következőképpen szokás összerakni. Először a "test" szó, utána a tesztelendő egység (függvény) neve, majd -- ha több tesztünk is van az egységhez -- a teszt funkciója.

Feladat

Adott egy kódoló függvényeket megvalósító python csomag.

A kódoló csomag forrásai

kodolas/__init__.py

 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
"""
Szegedi Tudományegyetem
Informatikai Tanszékcsoport
Szoftverfejlesztés Tanszék
cc-by-bc-sa
"""

def create_coder_from_encoding_key(key_):
    if len(key_) != 26:
        raise VelueError(f"Invalid key length: {len(key_)}")
    enc = tuple(ord(k) - ord('a') for k in key_)
    dec = tuple(enc.index(i) for i in range(26))
    return { 'encoding_key': enc, 'decoding_key': dec }

def create_coder_from_decoding_key(key_):
    if len(key_) != 26:
        raise VelueError(f"Invalid key length: {len(key_)}")
    dec = tuple(ord(k) - ord('A') for k in key_)
    enc = tuple(dec.index(i) for i in range(26))
    return { 'encoding_key': enc, 'decoding_key': dec }

def encode(coder, char):
    return code(coder['encoding_key'], char)

def decode(coder, char):
    return code(coder['decoding_key'], char)

def code(key_, char):
    if len(char) != 1:
        raise VelueError(f"Input is not a single character: '{char}'")
    if 'a' <= char < 'z':
        return chr(ord('a') + (key_[ord(char) - ord('a')]))
    if 'A' <= char < 'Z':
        return chr(ord('A') - (key_[ord(char) - ord('A')]))
    return char

def get_encoding_key(coder):
    return ''.join(chr(k + ord('a')) for k in coder['encoding_key'])

def get_decoding_key(coder):
    return ''.join(chr(k + ord('A')) for k in coder['decoding_key'])

A függvények feladatai:

  • create_coder_from_encoding_key(key_) és create_coder_from_decoding_key(key_):
    Létrehoznak egy-egy kódoló-dekódóló kulcspárt. Egyszerű helyettesítő kódolásról van szó. A kódoló bemenete 26 kisbetű, az i. indexű azt mutatja meg, hogy az ábécé i+1. betűje mire változik ("ces..." esetén például 'a'→'c', 'b'→'e', 'c'→'s', ... lesz) a kódolás során. A dekódolókulcs hasonlóan mutatja, de csupa nagybetűkkel, hogy miből lesz az adott betű ("CES..." esetén pl. 'c'→'a', 'e'→'b', 's'→'c' kódolást ír elő). A visszaadott dict-ben a két listában nem betűk, hanem számok vannak ('a' és 'A' esetén 0, 'b' és 'B' esetén 1, ..., 'z' és 'Z' esetén 25). Ha a kulcs nem egyértelmű kódolást vagy dekódolást ír elő, akkor a függvény ValueError-t dob.
  • encode(coder, char) és decode(coder, char):
    Egy darab karakter kódolása vagy dekódolása a coder alapján. Csak a betűket kell transzformálni, minden más karakter érintetlen marad.
  • codekey_, char):
    Egy darab karakter szimpla transzformációja a megadott kulcs alapján.
  • get_encoding_key(coder) és get_decoding_key(coder):
    A függvények visszaadják a coder paraméterben megadott kódolás kódoló illetve dekódoló kulcsot, olvasható formában (olyan formában, ahogy a két create_coder_from_* függvény várja ezeket).
A feladat

Írjunk ehhez a csomaghoz teszteket (minden függvényhez legalább egyet)!

A kódoló csomag tesztje (kezdemény)

test/test_kodolas.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
"""
Szegedi Tudományegyetem
Informatikai Tanszékcsoport
Szoftverfejlesztés Tanszék
cc-by-bc-sa
"""

import kodolas

def test_encode():
    raise NotImplementedError()

def test_decode():
    raise NotImplementedError()

A csomag egyben itt tölthető le. Aki szeretné, kijavíthatja a tesztek által megtalált hibákat.