Jednotkové testy
Piště testy, hned a pořádně - vyplatí se to!
Jednotkové testy¶
Axiom 1: Každý program obsahuje alespoň jednu chybu.
Axiom 2: Každý program lze při zachování funkčnosti zkrátit o jednu instrukci.
Věta: Každý program lze zkrátit na jednu chybnou instrukci. Důkaz matematickou indukcí přenecháváme čtenáři.
- Jednotkové testy slouží k automatizovanému ověření funkčnosti programu.
- při vývoji jeho částí (v extrémním případě, tzv. test-driven development, dokonce nejdříve napíšeme testy, které popisují chování nějakého kódu pomocí podmínek, jež má splňovat, a pak vyvíjíme kód, dokud neprojde.
- v udržování kódu: opětovné puštění testu nás ujistí, že jsme při dalším vývoji nezanesli chybu do něčeho, co už fungovalo.
- Jednotkový test se zaměřuje vždy na nějakou "jednotku" (obvykle to bývá metoda, či jedno z jejích použití). Pokud to jde, snažíme se snížit výsledek testu na aktuálně netestovaných částech kódu.
V kompilovaných jazycích (C++, Java) za nás část (jen část!) kontroly kódu udělá kompilátor, který zkontroluje, že nepoužíváme nedeklarované proměnné, že jsou všechny proměnné správného typu, ... Sice se tak nevyhneme chybám v logice programu, ale obvykle se tak odchytí alespoň překlepy. V Pythonu (a jiných skriptovacích jazycích) žádná kontrola předem neexistuje - jediná kontrola při načítání kódu se týká syntaktické správnosti. Z toho vyplývá, že kód v Pythonu je mnohem náchylnějším vůči chybám a o to více bychom ho měli kontrolovat. Jednotkové testování je v tomto nejmocnějším nástrojem.
Použití v praxi¶
Ve standardní knihovně Pythonu je modul unittest, v němž jsou všechny potřebné třídy a metody. Naše testy by měly dědit unittest.TestCase (typicky v jedné třídě několik testů patřících k sobě)
Příklad: Mějme pěknou třídu Auto, o které si myslím, že musí fungovat. Je s ní nějaký problém?
%%file auto.py
# -*- coding: utf8 -*-
class Auto(object):
def __init__(self, spotreba, rychlost):
self.spotreba = spotreba
self.rychlost = rychlost
self.cas = 0
self.vzdalenost = 0
self.nadrz = 50
def ujed(self, vzdalenost):
self.vzdalenost += vzdalenost
self.cas += vzdalenost / self.rychlost
self.nadrz -= vzdalenost * self.spotreba
Napíšeme si několik testů, protože si auto chceme "proklepnout", a najednou uvidíme, jak je naše třída děravá!
%%file auto_test.py
# -*- coding: utf8 -*-
from auto import Auto
import unittest
class AutoTest(unittest.TestCase): # Dědíme z třídy unittest.TestCase
def test_vypocet_spotreby(self):
auto = Auto(10, 200) # Dost žere, ale je rychlé
nadrz1 = auto.nadrz
auto.ujed(100)
nadrz2 = auto.nadrz
self.assertEqual(10, nadrz1 - nadrz2) # Víme, že auto mělo spotřebovat 10 litrů
def test_neprazdna_nadrz(self):
auto = Auto(8, 100)
with self.assertRaises(Exception):
auto.ujed(1000) # Dojde benzín
self.assertTrue(auto.nadrz == 0) # I poté musí nádrž být nejhůře prázdná
def test_nesmyslnych_aut(self):
with self.assertRaises(Exception):
auto = Auto(0, 100) # Auto bez spotřeby!
with self.assertRaises(Exception):
auto = Auto(10, 0) # Auto, které neumí jezdit!
with self.assertRaises(Exception):
auto = Auto(-10, 100) # Auto, které vyrábí benzín.
def test_nezaporna_vzdalenost(self): # Metody začínající na "test_" jsou automaticky spuštěny
auto = Auto(8, 100)
with self.assertRaises(Exception):
auto.ujed(-1)
if __name__ == "__main__":
unittest.main() # Tímto pustíme testy
!python auto_test.py
- Neřešili jsme, v jakých jednotkách počítáme spotřebu.
- Dovolili jsme si vytvořit nesmyslná auta!
- Dovolili jsme přečerpání nádrže.
- Nevadilo nám, že auto ujetím záporné vzdálenosti vyrábí benzín.
Zkusíme tedy opravit...
%%file auto.py
# -*- coding: utf8 -*-
from __future__ import division
class Auto(object):
def __init__(self, spotreba, rychlost):
if spotreba <= 0:
raise Exception("Auto musí mít nezápornou spotřebu.")
if rychlost <= 0:
raise Exception("Auto musí jezdit nezápornou rychlostí.")
self.spotreba = spotreba
self.rychlost = rychlost
self.cas = 0
self.vzdalenost = 0
self.nadrz = 50
def ujed(self, vzdalenost):
if vzdalenost < 0:
raise Exception("Vzdálenost musí být nezáporná.")
if (vzdalenost * self.spotreba / 100) > self.nadrz:
# Auto ujede, kolik může a pak vyhodí výjimku
skutecna_vzdalenost = 100 * (self.nadrz / self.spotreba)
self.ujed(skutecna_vzdalenost) # Rekurze :-)
raise Exception("Došel benzín")
self.vzdalenost += vzdalenost
self.cas += vzdalenost / self.rychlost
self.nadrz -= (vzdalenost * self.spotreba / 100)
!python auto_test.py
A je to. Hurá!
%%file test_auto_pytest.py
# -*- coding: utf8 -*-
from auto import Auto
import pytest
def test_vypocet_spotreby():
auto = Auto(10, 200) # Dost žere, ale je rychlé
nadrz1 = auto.nadrz
auto.ujed(100)
nadrz2 = auto.nadrz
assert 10 == nadrz1 - nadrz2 # Víme, že auto mělo spotřebovat 10 litrů
def test_neprazdna_nadrz():
auto = Auto(8, 100)
with pytest.raises(Exception):
auto.ujed(1000) # Dojde benzín
assert auto.nadrz == 0 # I poté musí nádrž být nejhůře prázdná
def test_nesmyslnych_aut():
with pytest.raises(Exception):
auto = Auto(0, 100) # Auto bez spotřeby!
with pytest.raises(Exception):
auto = Auto(10, 0) # Auto, které neumí jezdit!
with pytest.raises(Exception):
auto = Auto(-10, 100) # Auto, které vyrábí benzín.
def test_nezaporna_vzdalenost(): # Metody začínající na "test_" jsou automaticky spuštěny
auto = Auto(8, 100)
with pytest.raises(Exception):
auto.ujed(-1)
Testy z tohoto souboru pak můžeme spustit takto:
!pytest -vv test_auto_pytest.py
Ještě jednodušší je nechat pytest
prohledat aktuální adresář a spustit všechny testy (včetně tradičních unittest
testů).
!pytest -vv .
Komentáře
Comments powered by Disqus