A tesztvezérelt fejlesztés áttekintése.
A tesztvezérelt fejlesztés (TDD) a programozásnak az a stílusa, amelyben teszteket írunk, hogy ezek irányítsák a programterv kódban való megvalósítását.
A kódolás előtt egy vagy több tesztet (különösen egységtesztet) írunk meg. A tesztek célja, hogy lefedjék a program viselkedésének egy-egy aspektusát, amely egyetlen függvényre vagy metódusra összpontosulhat. A tesztek megírása arra szolgál, hogy a program követelményeit és általános architektúráját egy adott megvalósításhoz szabott tervvé alakítsa. Lefuttatjuk a teszteket, és azoknak meg kell bukniuk, mert a kód még nincs megvalósítva. Ezután megvalósítjuk a kódot, és újra lefuttatjuk a teszteket. Ha a tesztek sikeresek, akkor vagy elkészültünk a viselkedés megvalósításával, vagy esetleg további szükséges teszteket kell még írnunk. Ha a tesztek nem sikeresek, akkor hibakeresést végzünk a kódon, és újra lefuttatjuk a teszteket. A tesztelés és a kódolás ciklusát addig ismételjük, amíg minden szükséges teszt sikeres nem lesz; ekkor a program viselkedésének erre az aspektusára vonatkozó megvalósítás elkészült... egyelőre.
A refaktorálás azt jelenti, hogy átírjuk a kódot, hogy javítsunk a tervezésén. Ez nem egyszerűen a kód átírása a bugok kijavítása céljából. Néha „refaktorálásnak” nevezik, amikor úgy módosítunk kódot, hogy az átmenjen a teszteken. Bár a kód módosítása tartalmazhatja a tervezés javítását is mint a tesztek teljesítésének eszközét, a puszta hibakeresés nem feltétlenül jelenti a kód tervezésének javítását, és így nem feltétlenül refaktorálás.
Az alábbi egy példa a refaktorálás nélküli hibakeresésre:
# A function intended to return x added to y.
# x and y are bad parameter names, but we ignore that for now.
def add(x, y):
# used multiply operator by mistake. It fails the tests.
return x * y
# Function corrected. It passes the tests. It has been debugged, but not refactored.
def add(x, y):
return x + y
Az alábbi egy példa a refaktorálásra, majd az azt követő hibakeresésre:
# Function name and parameter names are modified to something more meaningful. This is refactoring.
def lot_inventory(old_cars, new_cars):
# Introduced multiply operator by mistake. It fails the tests. This is why we test.
return old_cars * new_cars
# Function corrected. It passes the tests. This is debugging.
def lot_inventory(old_cars, new_cars):
return old_cars + new_cars
Az Exercism Python-kurzusa a TDD módszertanát alkalmazza a feladataiban. Az egységtesztek már meg vannak írva. A tanuló megnézheti a teszteket, hogy részletesebben megértse, mi szükséges ahhoz, hogy egy megoldás sikeres legyen. A tanuló kaphat egy megoldásvázat is.
Amikor egy Python-megoldásnál egy vagy több teszt sikertelen, a megfelelő részfeladat(ok) háttere nem lesz zöld. Az első sikertelen részfeladat területe ki lesz nyitva, és a fejléce valahogy így néz ki:
Task 1 Extract coordinates -
A mínuszjelre kattintva összecsukódik a részfeladat, így megnézhetjük a többi részfeladatot is, de egyelőre maradjunk ennél.
Alatta egy kinyitott Test terület lesz, amely valahogy így néz ki:
Test 1 ⌄
FAILED TisburyTreasure > get coordinate
ahol a Tisbury Treasure a feladatot jelöli, a get_coordinate pedig a sikertelen függvényt vagy metódust.
A Test 1 általában egyfajta sablon lesz, amelyben egy kódrészlet állítja be a teszteket.
Nem tartalmaz információt arról, hogy pontosan melyik teszt(ek) buktak meg.
Alul valami ilyesmit ír majd:
One or more variations of this test failed. Details can be found under each [variant#].
A ⌄-ra kattintva összecsukódik a teszt.
Alatta egy összecsukott teszt lesz, amely valahogy így néz ki:
Test 2 >
FAILED TisburyTreasure > get coordinate [variation #1] (item=
("Scrimshaw Whale's Tooth", '2A'), result='2A')
Az, hogy hogyan néz ki, a jobb oldali panel beállított szélességétől függ.
A >-ra kattintva kinyílik a teszt.
A bemeneti adatok és a várt eredmény adatai valószínűleg egy kódrészletben jelennek meg.
Az adatok az ehhez a részfeladathoz tartozó összes tesztre vonatkozhatnak.
Alul, a Test Failure szakaszban található a konkrét ok, amiért ez a teszt megbukott.
Valahogy így nézhet ki:
AssertionError: ['2A'] != '2A'
Ebben az esetben azt jelzi, hogy a visszaadott ['2A'] érték nem egyezett a várt '2A' értékkel.
Ha megnézzük a get_coordinate kódját, azt látjuk, hogy így van megvalósítva:
def get_coordinate(record):
return [record[1]]
Ha eltávolítjuk a szögletes zárójeleket a lista körül (pl. return record[1]), és újra lefuttatjuk a teszteket, az 1. részfeladat tesztjei sikeresek lesznek.
Ha egy vagy több részfeladat továbbra is sikertelen, akkor a fenti folyamatot mindegyikkel megismételjük, amíg minden teszt sikeres nem lesz.
Néha a várt és a visszaadott adatok túl nagyok ahhoz, hogy mind elférjenek a Test Failure szakaszban.
Valahogy így nézhet ki:
AssertionError: '("Sc[67 chars]\')\n\n(\'Brass Spyglass\', \'Abandoned Lighth[952 chars]')\n' != '("Sc[67 chars]\')\n(\'Brass Spyglass\', \'Abandoned Lighthou[928 chars]')\n'
Diff is 970 characters long. Set self.maxDiff to None to see it.
Lehet, hogy még így is elég adat áll rendelkezésre ahhoz, hogy lássuk, mi a probléma.
A fenti esetben két soremelés szerepel a visszaadott értékben (pl. \n\n(\'Brass Spyglass), holott csak egyre számítunk (pl. \n(\'Brass Spyglass).
Gratulálunk! Minden teszt sikeres lett. Mi a következő lépés? A megoldást rögtön közzé is teheted. Vagy, most hogy a kód működik, ha bármilyen okból refaktorálni szeretnéd, módosíthatod a kódot, és beküldhetsz egy újabb iterációt. Ha úgy gondolod, hogy a kód jobb lehetne, de nem tudod, hogyan, mentorálást kérhetsz a megoldásodhoz. Ha van elérhető mentor, az felkereshet a megoldás más megközelítéseire vonatkozó ötletekkel. Amikor közzéteszed a megoldásodat, engedélyezheted a hozzászólásokat, és más tanulók is élhetnek az alkalommal, hogy hozzászóljanak vagy kérdéseket tegyenek fel.
Bár „Az idő előtti optimalizálás minden gonosz gyökere” (Tony Hoare-nak és Donald Knuthnak is tulajdonított mondás), eljön az idő, amikor bár a megoldás működik, mégis szeretnénk javítani a megoldás teljesítményén.
Ilyen lehet az, amikor a megoldás bizonyos teszteken átmegy, másokon viszont időtúllépés miatt elbukik.
Hasznos lehet pontosan tudni, mennyi időt vesz igénybe egy kódrészlet.
A timeit modul segítségével nagyon kis időtartamokig mérhetjük a kód futási idejét.
A timeit függvény legfeljebb öt argumentumot fogadhat: timeit.timeit(stmt='pass', setup='pass', timer=<default timer>, number=1000000, globals=None).
A stmt paraméter határozza meg azt a kódot, amelyet lefuttatunk és időzítünk.
A number paraméter határozza meg, hogy a stmt kód hányszor fusson le.
A setup paraméter határozza meg azt a kódot, amelyet csak egyszer futtatunk le, hogy előkészítsük a stmt kód futtatását.
A setup kód futási ideje beleszámít a teljes időbe.
Minél többször fut le a stmt kód, annál kevésbé számít a setup ideje egy-egy iterációra vetítve.
A timer paraméterrel az alapértelmezettől eltérő Timer adható át.
A timer paraméter alapértelmezett argumentuma a perf_counter, amely a legtöbb esetben elegendő.
A number paraméter alapértelmezett argumentuma 1_000_000.
A globals paraméter megadja azt a névteret, amelyben a kód végrehajtódik.
A globals paraméter alapértelmezett argumentuma None.
Az alábbi példa arra mutatja, hogyan használhatjuk a timeit modult annak mérésére, mennyi ideig tart megállapítani, hogy egy mondat tartalmazza-e az összes angol magánhangzót:
import timeit
# run one million times
loops = 1_000_000
# first positional argument is for stmt
# second positional argument is for setup
# third (named) argument is for number
print(timeit.timeit("""has_all_vowels('Another piggy digs up the truffles.')""",
"""
VOWELS = "AEIOU"
def has_all_vowels(sentence):
return all(letter in sentence.casefold() for letter in VOWELS)
""", number=loops) / loops)
A kód egymilliószori lefuttatása átlagosan 4.965089999896008e-07 másodpercet vett igénybe hívásonként (nagyjából 497 nanoszekundum hívásonként).
A következő példa azt vizsgálja, hogy megtakarít-e bármennyi időt, ha kivesszük a casefold hívást a listaértelmezésből:
import timeit
loops = 1_000_000
print(timeit.timeit("""has_all_vowels('Another piggy digs up the truffles.')""",
"""
VOWELS = "AEIOU"
def has_all_vowels(sentence):
sentence = sentence.casefold()
return all(letter in sentence for letter in VOWELS)
""", number=loops) / loops)
A kód egymilliószori lefuttatása átlagosan 4.923898000270128e-07 másodpercet vett igénybe hívásonként (nagyjából 492 nanoszekundum hívásonként.)
Így a casefold kivétele a listaértelmezésből hívásonként nagyjából 5 nanoszekundumot takarított meg, ami egymillió hívás esetén összesen mintegy 5 ezredmásodperc.
A cProfile szintén használható kód profilozására; azonban nem ennyire finom felbontású, mivel csak ezredmásodperces időtartamokig megy le.