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

  • 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?

In [1]:
%%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
Writing auto.py

Napíšeme si několik testů, protože si auto chceme "proklepnout", a najednou uvidíme, jak je naše třída děravá!

In [2]:
%%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
Writing auto_test.py
In [3]:
!python auto_test.py
FFFF
======================================================================
FAIL: test_neprazdna_nadrz (__main__.AutoTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "auto_test.py", line 17, in test_neprazdna_nadrz
    auto.ujed(1000)                    # Dojde benzín
AssertionError: Exception not raised

======================================================================
FAIL: test_nesmyslnych_aut (__main__.AutoTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "auto_test.py", line 22, in test_nesmyslnych_aut
    auto = Auto(0, 100)                # Auto bez spotřeby!
AssertionError: Exception not raised

======================================================================
FAIL: test_nezaporna_vzdalenost (__main__.AutoTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "auto_test.py", line 31, in test_nezaporna_vzdalenost
    auto.ujed(-1)
AssertionError: Exception not raised

======================================================================
FAIL: test_vypocet_spotreby (__main__.AutoTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "auto_test.py", line 12, in test_vypocet_spotreby
    self.assertEqual(10, nadrz1 - nadrz2)  # Víme, že auto mělo spotřebovat 10 litrů
AssertionError: 10 != 1000

----------------------------------------------------------------------
Ran 4 tests in 0.001s

FAILED (failures=4)
  • 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...

In [4]:
%%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)
Overwriting auto.py
In [5]:
!python auto_test.py
....
----------------------------------------------------------------------
Ran 4 tests in 0.001s

OK

A je to. Hurá!

Užitečné rozšiřující nástroje

nose - automatické spuštění všech testů v adresáři a další vymoženosti. Viz http://nose.readthedocs.org/en/latest/

Odkazy

Komentáře

Comments powered by Disqus