{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "Piště testy, hned a pořádně - vyplatí se to!\n", "" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Jednotkové testy\n", "\n", "**Axiom 1**: Každý program obsahuje alespoň jednu chybu.\n", "\n", "**Axiom 2**: Každý program lze při zachování funkčnosti zkrátit o jednu instrukci.\n", "\n", "**Věta**: Každý program lze zkrátit na jednu chybnou instrukci. *Důkaz matematickou indukcí přenecháváme čtenáři.*\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "* Jednotkové testy slouží k automatizovanému ověření funkčnosti programu.\n", " * 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.\n", " * 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.\n", "* 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.\n", "\n", "*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.*" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Použití v praxi" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "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ě)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**Příklad**: Mějme pěknou třídu Auto, o které si myslím, že musí fungovat. Je s ní nějaký problém?" ] }, { "cell_type": "code", "execution_count": 1, "metadata": { "collapsed": false, "jupyter": { "outputs_hidden": false } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Writing auto.py\n" ] } ], "source": [ "%%file auto.py\n", "# -*- coding: utf8 -*-\n", "class Auto(object):\n", " def __init__(self, spotreba, rychlost):\n", " self.spotreba = spotreba\n", " self.rychlost = rychlost\n", " self.cas = 0\n", " self.vzdalenost = 0\n", " self.nadrz = 50\n", " \n", " def ujed(self, vzdalenost):\n", " self.vzdalenost += vzdalenost\n", " self.cas += vzdalenost / self.rychlost\n", " self.nadrz -= vzdalenost * self.spotreba\n", "\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Napíšeme si několik testů, protože si auto chceme \"proklepnout\", a najednou uvidíme, jak je naše třída děravá!" ] }, { "cell_type": "code", "execution_count": 2, "metadata": { "collapsed": false, "jupyter": { "outputs_hidden": false } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Writing auto_test.py\n" ] } ], "source": [ "%%file auto_test.py\n", "\n", "# -*- coding: utf8 -*-\n", "from auto import Auto\n", "import unittest\n", "\n", "class AutoTest(unittest.TestCase): # Dědíme z třídy unittest.TestCase\n", " def test_vypocet_spotreby(self):\n", " auto = Auto(10, 200) # Dost žere, ale je rychlé\n", " nadrz1 = auto.nadrz\n", " auto.ujed(100)\n", " nadrz2 = auto.nadrz\n", " self.assertEqual(10, nadrz1 - nadrz2) # Víme, že auto mělo spotřebovat 10 litrů\n", " \n", " def test_neprazdna_nadrz(self):\n", " auto = Auto(8, 100)\n", " with self.assertRaises(Exception):\n", " auto.ujed(1000) # Dojde benzín\n", " self.assertTrue(auto.nadrz == 0) # I poté musí nádrž být nejhůře prázdná \n", " \n", " def test_nesmyslnych_aut(self):\n", " with self.assertRaises(Exception):\n", " auto = Auto(0, 100) # Auto bez spotřeby!\n", " with self.assertRaises(Exception):\n", " auto = Auto(10, 0) # Auto, které neumí jezdit!\n", " with self.assertRaises(Exception):\n", " auto = Auto(-10, 100) # Auto, které vyrábí benzín.\n", "\n", " def test_nezaporna_vzdalenost(self): # Metody začínající na \"test_\" jsou automaticky spuštěny\n", " auto = Auto(8, 100)\n", " with self.assertRaises(Exception):\n", " auto.ujed(-1)\n", " \n", "if __name__ == \"__main__\":\n", " unittest.main() # Tímto pustíme testy" ] }, { "cell_type": "code", "execution_count": 3, "metadata": { "collapsed": false, "jupyter": { "outputs_hidden": false } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "FFFF\n", "======================================================================\n", "FAIL: test_neprazdna_nadrz (__main__.AutoTest)\n", "----------------------------------------------------------------------\n", "Traceback (most recent call last):\n", " File \"auto_test.py\", line 17, in test_neprazdna_nadrz\n", " auto.ujed(1000) # Dojde benzín\n", "AssertionError: Exception not raised\n", "\n", "======================================================================\n", "FAIL: test_nesmyslnych_aut (__main__.AutoTest)\n", "----------------------------------------------------------------------\n", "Traceback (most recent call last):\n", " File \"auto_test.py\", line 22, in test_nesmyslnych_aut\n", " auto = Auto(0, 100) # Auto bez spotřeby!\n", "AssertionError: Exception not raised\n", "\n", "======================================================================\n", "FAIL: test_nezaporna_vzdalenost (__main__.AutoTest)\n", "----------------------------------------------------------------------\n", "Traceback (most recent call last):\n", " File \"auto_test.py\", line 31, in test_nezaporna_vzdalenost\n", " auto.ujed(-1)\n", "AssertionError: Exception not raised\n", "\n", "======================================================================\n", "FAIL: test_vypocet_spotreby (__main__.AutoTest)\n", "----------------------------------------------------------------------\n", "Traceback (most recent call last):\n", " File \"auto_test.py\", line 12, in test_vypocet_spotreby\n", " self.assertEqual(10, nadrz1 - nadrz2) # Víme, že auto mělo spotřebovat 10 litrů\n", "AssertionError: 10 != 1000\n", "\n", "----------------------------------------------------------------------\n", "Ran 4 tests in 0.001s\n", "\n", "FAILED (failures=4)\n" ] } ], "source": [ "!python auto_test.py" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "* Neřešili jsme, v jakých jednotkách počítáme spotřebu.\n", "* Dovolili jsme si vytvořit nesmyslná auta!\n", "* Dovolili jsme přečerpání nádrže.\n", "* Nevadilo nám, že auto ujetím záporné vzdálenosti vyrábí benzín.\n", "\n", "Zkusíme tedy opravit..." ] }, { "cell_type": "code", "execution_count": 4, "metadata": { "collapsed": false, "jupyter": { "outputs_hidden": false } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Overwriting auto.py\n" ] } ], "source": [ "%%file auto.py\n", "\n", "# -*- coding: utf8 -*-\n", "from __future__ import division\n", "\n", "class Auto(object):\n", " def __init__(self, spotreba, rychlost):\n", " if spotreba <= 0:\n", " raise Exception(\"Auto musí mít nezápornou spotřebu.\")\n", " if rychlost <= 0:\n", " raise Exception(\"Auto musí jezdit nezápornou rychlostí.\")\n", " self.spotreba = spotreba\n", " self.rychlost = rychlost\n", " self.cas = 0\n", " self.vzdalenost = 0\n", " self.nadrz = 50\n", " \n", " def ujed(self, vzdalenost):\n", " if vzdalenost < 0:\n", " raise Exception(\"Vzdálenost musí být nezáporná.\")\n", " if (vzdalenost * self.spotreba / 100) > self.nadrz:\n", " # Auto ujede, kolik může a pak vyhodí výjimku\n", " skutecna_vzdalenost = 100 * (self.nadrz / self.spotreba) \n", " self.ujed(skutecna_vzdalenost) # Rekurze :-)\n", " raise Exception(\"Došel benzín\")\n", " self.vzdalenost += vzdalenost\n", " self.cas += vzdalenost / self.rychlost\n", " self.nadrz -= (vzdalenost * self.spotreba / 100)" ] }, { "cell_type": "code", "execution_count": 5, "metadata": { "collapsed": false, "jupyter": { "outputs_hidden": false } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "....\n", "----------------------------------------------------------------------\n", "Ran 4 tests in 0.000s\n", "\n", "OK\n" ] } ], "source": [ "!python auto_test.py" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "A je to. **Hurá!**" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## pytest\n", "\n", "Pokud vám přišla konstrukce pomocí tříd (`TestCase`) poněkud krkolomná, nejste zdaleka sami. Ve většině případů se používá pro testování nějaké rozšíření (framework), psaní a spouštění testů zjednoduší. Nejlepší volbou je v tuto chvíli [pytest](https://pytest.org).\n", "\n", "Předchozí test by vypadal asi takto:" ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Writing test_auto_pytest.py\n" ] } ], "source": [ "%%file test_auto_pytest.py\n", "\n", "# -*- coding: utf8 -*-\n", "from auto import Auto\n", "import pytest\n", "\n", "def test_vypocet_spotreby():\n", " auto = Auto(10, 200) # Dost žere, ale je rychlé\n", " nadrz1 = auto.nadrz\n", " auto.ujed(100)\n", " nadrz2 = auto.nadrz\n", " assert 10 == nadrz1 - nadrz2 # Víme, že auto mělo spotřebovat 10 litrů\n", "\n", "def test_neprazdna_nadrz():\n", " auto = Auto(8, 100)\n", " with pytest.raises(Exception):\n", " auto.ujed(1000) # Dojde benzín\n", " assert auto.nadrz == 0 # I poté musí nádrž být nejhůře prázdná \n", "\n", "def test_nesmyslnych_aut():\n", " with pytest.raises(Exception):\n", " auto = Auto(0, 100) # Auto bez spotřeby!\n", " with pytest.raises(Exception):\n", " auto = Auto(10, 0) # Auto, které neumí jezdit!\n", " with pytest.raises(Exception):\n", " auto = Auto(-10, 100) # Auto, které vyrábí benzín.\n", "\n", "def test_nezaporna_vzdalenost(): # Metody začínající na \"test_\" jsou automaticky spuštěny\n", " auto = Auto(8, 100)\n", " with pytest.raises(Exception):\n", " auto.ujed(-1)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Testy z tohoto souboru pak můžeme spustit takto:" ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\u001b[1m============================= test session starts ==============================\u001b[0m\n", "platform darwin -- Python 3.7.4, pytest-5.2.2, py-1.8.0, pluggy-0.13.0 -- /Users/kuba/miniconda3/envs/fjfi/bin/python\n", "cachedir: .pytest_cache\n", "rootdir: /Users/kuba/workspace/python-fjfi/numerical_python_course/lecture_notes.cz\n", "collected 4 items \u001b[0m\n", "\n", "test_auto_pytest.py::test_vypocet_spotreby \u001b[32mPASSED\u001b[0m\u001b[36m [ 25%]\u001b[0m\n", "test_auto_pytest.py::test_neprazdna_nadrz \u001b[32mPASSED\u001b[0m\u001b[36m [ 50%]\u001b[0m\n", "test_auto_pytest.py::test_nesmyslnych_aut \u001b[32mPASSED\u001b[0m\u001b[36m [ 75%]\u001b[0m\n", "test_auto_pytest.py::test_nezaporna_vzdalenost \u001b[32mPASSED\u001b[0m\u001b[36m [100%]\u001b[0m\n", "\n", "\u001b[32m\u001b[1m============================== 4 passed in 0.05s ===============================\u001b[0m\n" ] } ], "source": [ "!pytest -vv test_auto_pytest.py" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Ještě jednodušší je nechat `pytest` prohledat aktuální adresář a spustit všechny testy (včetně tradičních `unittest` testů)." ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\u001b[1m============================= test session starts ==============================\u001b[0m\n", "platform darwin -- Python 3.7.4, pytest-5.2.2, py-1.8.0, pluggy-0.13.0 -- /Users/kuba/miniconda3/envs/fjfi/bin/python\n", "cachedir: .pytest_cache\n", "rootdir: /Users/kuba/workspace/python-fjfi/numerical_python_course/lecture_notes.cz\n", "collected 8 items \u001b[0m\n", "\n", "auto_test.py::AutoTest::test_neprazdna_nadrz \u001b[32mPASSED\u001b[0m\u001b[36m [ 12%]\u001b[0m\n", "auto_test.py::AutoTest::test_nesmyslnych_aut \u001b[32mPASSED\u001b[0m\u001b[36m [ 25%]\u001b[0m\n", "auto_test.py::AutoTest::test_nezaporna_vzdalenost \u001b[32mPASSED\u001b[0m\u001b[36m [ 37%]\u001b[0m\n", "auto_test.py::AutoTest::test_vypocet_spotreby \u001b[32mPASSED\u001b[0m\u001b[36m [ 50%]\u001b[0m\n", "test_auto_pytest.py::test_vypocet_spotreby \u001b[32mPASSED\u001b[0m\u001b[36m [ 62%]\u001b[0m\n", "test_auto_pytest.py::test_neprazdna_nadrz \u001b[32mPASSED\u001b[0m\u001b[36m [ 75%]\u001b[0m\n", "test_auto_pytest.py::test_nesmyslnych_aut \u001b[32mPASSED\u001b[0m\u001b[36m [ 87%]\u001b[0m\n", "test_auto_pytest.py::test_nezaporna_vzdalenost \u001b[32mPASSED\u001b[0m\u001b[36m [100%]\u001b[0m\n", "\n", "\u001b[32m\u001b[1m============================== 8 passed in 0.06s ===============================\u001b[0m\n" ] } ], "source": [ "!pytest -vv ." ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.7.4" } }, "nbformat": 4, "nbformat_minor": 4 }