Obsah
1. Behavior-driven development v Pythonu s využitím knihovny Behave
2. Projekt s jedním modulem, který bude testován s využitím knihovny Behave
3. Zdrojový kód testovaného modulu, skript s testovacím scénářem
4. Pomocné soubory v adresáři s projektem
5. Soubor common.py s definicí testovacích kroků
6. Spuštění skriptu s testem, výsledek testů
7. Vylepšení implementace kroků testů i vlastního testovacího scénáře
8. Spuštění druhé varianty skriptu s testy, výsledek testů
9. Testování funkcí z nativních knihoven
10. Projekt, v němž budeme testovat vlastnosti nativní funkce
12. Upravená implementace testovacích kroků
13. Spuštění skriptu s testy nativní funkce, výsledek testů
14. Spuštění skriptu ve chvíli, kdy není nativní funkce nalezena
15. Další modifikace testovacího scénáře – určení typů parametrů předávaných nativní funkci
17. Spuštění modifikovaného skriptu s testy nativní funkce, výsledek testů
18. Repositář s demonstračními příklady
1. Behavior-driven development v Pythonu s využitím knihovny Behave
Ve dvou článcích [1] [2] o programovacím jazyku Clojure jsme se věnovali dnes poměrně populárnímu tématu: popisu integrace doménově specifického jazyka Gherkin určeného pro popis testovacích scénářů s jazykem Clojure. Ve skutečnosti ovšem není Gherkin v žádném případě určen pouze pro použití společně s Clojure, ale jedná se o DSL integrovatelný i do mnoha dalších programovacích jazyků. Dnes si představíme knihovnu Behave, s jejíž pomocí se Gherkin integruje do jazyka Python. Ve skutečnosti se bude jednat o téměř ideální spojení, protože Gherkin i Python používají podobný způsob zápisu, v němž i odsazení jednotlivých programových řádků je součástí syntaxe (naproti tomu se Gherkin a Clojure ze syntaktického hlediska zcela odlišují).
![](https://i.iinfo.cz/images/634/clojure-cucumber-1-prev.png)
Obrázek 1: Ukázka scénářů napsaných v doménově specifickém jazyce Gherkin.
2. Projekt s jedním modulem, který bude testován s využitím knihovny Behave
Jazyk Gherkin je navržen takovým způsobem, aby ho uživatelé (nemusí se totiž nutně jednat pouze o programátory) mohli začít používat prakticky okamžitě, tj. bez nutnosti studia sáhodlouhých manuálů. I z toho důvodu si možnosti tohoto doménově specifického jazyka postupně ukážeme na několika demonstračních příkladech. První příklad bude (prozatím) velmi jednoduchý, protože bude obsahovat jediný modul (naprogramovaný v Pythonu), který budeme chtít otestovat. I přesto se však bude jednat o plnohodnotný projekt, jehož struktura odpovídá struktuře projektů složitějších a sofistikovanějších. Adresář s projektem i s testovacím scénářem by měl vypadat následovně:
├── feature_list.txt ├── features │ ├── adder.feature │ └── steps │ └── common.py ├── requirements.in ├── requirements.txt ├── run_tests.sh └── src └── adder.py 3 directories, 7 files
V projektu můžeme vidět několik typů souborů:
Soubor | Popis |
---|---|
src/adder.py | vlastní modul, který budeme chtít otestovat |
requirements.in/requirements.txt | soubory pro pip (instalátor balíčků) |
feature_list.txt | seznam testovacích scénářů, které se mají spustit |
features/* | adresář obsahující testovací scénáře i implementaci jednotlivých kroků testů |
run_tests.sh | pomocný skript pro spuštění testovacích scénářů |
3. Zdrojový kód testovaného modulu, skript s testovacím scénářem
Modul adder.py, který vlastně tvoří celou testovanou aplikaci, je velmi stručný, protože obsahuje jedinou funkci nazvanou add, jež – jak ostatně její název naznačuje – slouží k součtu dvou numerických hodnot popř. i pro aplikaci operátoru + na všechny typy operandů, které tento operátor podporují (řetězce, seznamy, n-tice, instance uživatelem deklarovaných tříd atd. atd.). Zdrojový kód tohoto modulu tedy sestává z pouhých dvou řádků:
def add(x, y): return x + y
Druhým důležitým souborem je vlastní testovací scénář nazvaný adder.feature, který je uložen v podadresáři features. Tento testovací scénář je naprogramován v jazyku Gherkin, konkrétně v jeho výchozí anglické „mutaci“ (použitím jiných jazykových mutací se budeme zabývat příště). V následujícím výpisu jsou klíčová slova rozeznávaná interpretrem označena tučně:
Feature: Adder test Scenario: Check the function add() Given The function add is callable When I call function add with arguments 1 and 2 Then I should get 3 as a result
4. Pomocné soubory v adresáři s projektem
Další pomocné soubory, které v projektu nalezneme, mají následující obsah.
Prvním důležitým souborem je soubor pojmenovaný feature_list.txt. V tomto souboru je uložen seznam testovacích scénářů, které se mají spustit. Prozatím máme vytvořen jen jediný testovací scénář (popsaný v předchozí kapitole), takže tento soubor bude obsahovat jediný řádek s relativním jménem souboru se scénářem:
features/adder.feature
Další dva soubory requirements.in a requirements.txt obsahují seznam modulů (knihoven), které budeme v systému potřebovat proto, aby bylo možné testovací scénáře spustit. Prozatím je obsah i těchto souborů dosti minimalistický, protože budeme potřebovat dvě knihovny (kromě standardních knihoven Pythonu):
behave pytest
A konečně následuje soubor pojmenovaný run_tests.sh. Ten slouží ke spuštění testovacích scénářů. Povšimněte si, že na základě obsahu proměnné NOVENV využíváme virtuální prostředí Pythonu, což mj. znamená, že instalace potřebných knihoven (pytest a behave) bude provedena pouze lokálně, bez zásahu do systémových oblastí souborového systému. Pokud máte všechny moduly v systému již nainstalované, není nutné virtuální prostředí použít a proměnná NOVENV může zůstat nastavena na hodnotě 1. Skript je připraven pro Python 3.x, ovšem můžete si ho velmi snadno upravit i pro použití v Pythonu 2.x:
#!/bin/bash -ex export NOVENV=1 function prepare_venv() { virtualenv -p python3 venv && source venv/bin/activate && python3 `which pip3` install -r requirements.txt } [ "$NOVENV" == "1" ] || prepare_venv || exit 1 PYTHONDONTWRITEBYTECODE=1 python3 `which behave` --tags=-skip -D dump_errors=true @feature_list.txt $@
Pokud budete chtít všechny závislé moduly nainstalovat do virtuálního prostředí, proveďte v souboru run_tests.sh následující změnu:
export NOVENV=0
5. Soubor common.py s definicí testovacích kroků
Velmi důležitým souborem, na němž jsou celé testy postaveny, je soubor common.py, který je umístěný v podadresáři features/steps/. V tomto souboru jsou implementovány jednotlivé kroky testu, tj. implementace řádků začínajících na Given, When a Then.
Nejprve se podívejme, jak soubor vypadá, následně si jednotlivé řádky popíšeme:
from behave import given, then, when from src.adder import add @given('The function {function_name} is callable') def initial_state(context, function_name): pass @when('I call function {function} with arguments {x:d} and {y:d}') def call_add(context, function, x, y): context.result = add(x, y) @then('I should get {expected:d} as a result') def check_integer_result(context, expected): assert context.result == expected, \ "Wrong result: {r} != {e}".format(r=context.result, e=expected)
Na začátku souboru nejdříve provedeme import anotací given, when a then:
from behave import given, then, when
Následuje import vlastní testované funkce:
from src.adder import add
Dále jsou implementovány jednotlivé kroky testu. Jedná se o běžné funkce, které ovšem obsahují anotaci given, when nebo then. V anotaci je uveden text, který se Gherkin snaží spárovat s příslušnými řádky textu. V tomto textu je možné používat proměnné části, které jsou uzavřeny do složených závorek {} a obsahují buď pouze jméno parametru předaného funkci popř. jméno parametru následované typem (:d pro dekadické číslice atd.).
První funkce odpovídá klauzuli given. Povšimněte si, že se funkci, která implementuje tento krok, vždy předává parametr obsahující kontext (což je uživatelsky modifikovaný objekt) a taktéž parametr nazvaný function_name. Toto jméno odpovídá proměnné části textu z anotace. Při volání testovací funkce se do tohoto parametru dosadí skutečný text zapsaný uživatelem do souboru adder.feature:
@given('The function {function_name} is callable') def initial_state(context, function_name): pass
U druhé funkce je použita anotace, jejíž text obsahuje celkem tři proměnné části. Z toho důvodu je testovací funkce volána se čtyřmi parametry (na prvním místě je vždy kontext). Uvnitř testovací funkce zavoláme testovanou funkci add a zapamatujeme si její výsledek. Povšimněte si, že interpret jazyka Gherkin je celkem inteligentní, protože z popisu proměnných částí {x:d} a {y:d} poznal, že se jedná o celá čísla, takže tyto parametry skutečně na celá čísla sám převedl:
@when('I call function {function} with arguments {x:d} and {y:d}') def call_add(context, function, x, y): context.result = add(x, y)
Poslední funkce s popisem kroku testu porovnává očekávaný výsledek s výsledkem vypočteným funkcí add. Povšimněte si, že objekt předaný v parametru context výsledek skutečně obsahuje, protože právě tento objekt slouží k zapamatování stavu celého testovacího scénáře popř. jeho jednotlivých částí (celá situace je ve skutečnosti nepatrně složitější a budeme se jí zabývat příště):
@then('I should get {expected:d} as a result') def check_integer_result(context, expected): assert context.result == expected, \ "Wrong result: {r} != {e}".format(r=context.result, e=expected)
6. Spuštění skriptu s testem, výsledek testů
Pokud spustíme soubor run_tests.sh, měly by se na standardním výstupu objevit následující řádky (zde se předpokládá, že je zakázáno použití virtuálního prostředí Pythonu):
$ ./run_tests.sh + export NOVENV=1 + NOVENV=1 + '[' 1 == 1 ']' ++ which behave + PYTHONDONTWRITEBYTECODE=1 + LD_LIBRARY_PATH=lib + python3 /usr/local/bin/behave --tags=-skip -D dump_errors=true @feature_list.txt Feature: Adder test # features/adder.feature:1 Scenario: Check the function add() # features/adder.feature:4 Given The function add is callable # features/steps/common.py:5 0.000s When I call function add with arguments 1 and 2 # features/steps/common.py:10 0.000s Then I should get 3 as a result # features/steps/common.py:15 0.000s 1 feature passed, 0 failed, 0 skipped 1 scenario passed, 0 failed, 0 skipped 3 steps passed, 0 failed, 0 skipped, 0 undefined Took 0m0.000s
Zprávy jsou implicitně obarveny, takže ve skutečnosti bude výsledek vypadat spíše takto:
![](https://i.iinfo.cz/images/549/behave1-1-prev.png)
Obrázek 2: Terminál s výstupem vytvořeným testovacím scénářem.
V případě, že povolíte použití virtuálního prostředí Pythonu, bude výstup poněkud odlišný, protože se nejdřív toto prostředí nastaví a vzápětí se do něj nainstalují potřebné moduly pytest a behave:
Using base prefix '/usr' New python executable in venv/bin/python3 Also creating executable in venv/bin/python Installing setuptools, pip...done. Running virtualenv with interpreter /usr/bin/python3 Downloading/unpacking behave (from -r requirements.txt (line 1)) Downloading/unpacking pytest (from -r requirements.txt (line 2)) Downloading/unpacking six>=1.11 (from behave->-r requirements.txt (line 1)) Downloading six-1.11.0-py2.py3-none-any.whl Downloading/unpacking parse>=1.8.2 (from behave->-r requirements.txt (line 1)) Downloading parse-1.8.2.tar.gz Running setup.py (path:/home/tester/temp/python/python-behave-demos/test_python_function1/venv/build/parse/setup.py) egg_info for package parse Downloading/unpacking parse-type>=0.4.2 (from behave->-r requirements.txt (line 1)) Downloading parse_type-0.4.2-py2.py3-none-any.whl Downloading/unpacking more-itertools>=4.0.0 (from pytest->-r requirements.txt (line 2)) Requirement already satisfied (use --upgrade to upgrade): setuptools in ./venv/lib/python3.4/site-packages (from pytest->-r requirements.txt (line 2)) Downloading/unpacking py>=1.5.0 (from pytest->-r requirements.txt (line 2)) Downloading/unpacking pluggy>=0.5,<0.7 (from pytest->-r requirements.txt (line 2)) Downloading pluggy-0.6.0.tar.gz Running setup.py (path:/home/tester/temp/python/python-behave-demos/test_python_function1/venv/build/pluggy/setup.py) egg_info for package pluggy /usr/lib/python3.4/distutils/dist.py:260: UserWarning: Unknown distribution option: 'python_requires' warnings.warn(msg) warning: no files found matching 'CHANGELOG' warning: no previously-included files matching '*.pyc' found under directory '*' warning: no previously-included files matching '*.pyo' found under directory '*' Downloading/unpacking attrs>=17.4.0 (from pytest->-r requirements.txt (line 2)) Downloading attrs-17.4.0-py2.py3-none-any.whl Installing collected packages: behave, pytest, six, parse, parse-type, more-itertools, py, pluggy, attrs Running setup.py install for parse Running setup.py install for pluggy /usr/lib/python3.4/distutils/dist.py:260: UserWarning: Unknown distribution option: 'python_requires' warnings.warn(msg) warning: no files found matching 'CHANGELOG' warning: no previously-included files matching '*.pyc' found under directory '*' warning: no previously-included files matching '*.pyo' found under directory '*' Successfully installed behave pytest six parse parse-type more-itertools py pluggy attrs Cleaning up... Feature: Adder test # features/adder.feature:1 Scenario: Check the function add() # features/adder.feature:3 Given The function add is callable # features/steps/common.py:5 When I call function add with arguments 1 and 2 # features/steps/common.py:10 Then I should get 3 as a result # features/steps/common.py:15 1 feature passed, 0 failed, 0 skipped 1 scenario passed, 0 failed, 0 skipped 3 steps passed, 0 failed, 0 skipped, 0 undefined Took 0m0.000s
7. Vylepšení implementace kroků testů i vlastního testovacího scénáře
V této kapitole si popíšeme některá vylepšení, která lze provést v implementaci jednotlivých kroků testů i vlastního testovacího scénáře. V první řadě doplníme funkci initial_state o kontrolu, zda je testovaná funkce viditelná a zda ji lze zavolat (tj. zda se jedná o funkci a nikoli o objekt jiného typu). Soubor common.py se tedy změní takto:
from behave import given, then, when from src.adder import add @given('The function {function_name} is callable') def initial_state(context, function_name): g = globals() assert function_name in g, "Function is not visible" assert callable(g[function_name]), "Not a function" @when('I call function {function} with arguments {x:d} and {y:d}') def call_add(context, function, x, y): context.result = add(x, y) @then('I should get {expected:d} as a result') def check_integer_result(context, expected): assert context.result == expected, \ "Wrong result: {r} != {e}".format(r=context.result, e=expected)
Jedna z velmi zajímavých možností, jak testovací scénáře rozšířit, spočívá v tom, že se specifikuje tabulka či tabulky se vstupními hodnotami a očekávanými výsledky. Do testovacího scénáře přidáme tzv. osnovu (Scenario Outline), takže se celý scénář adder.feature změní následovně:
Feature: Adder test Scenario: Check the function add() Given The function add is callable When I call function add with arguments 1 and 2 Then I should get 3 as a result Scenario Outline: Thorough checking function add() Given The function add is callable When I call function add with arguments <x> and <y> Then I should get <result> as a result Examples: result |x|y|result| # basic arithmetic | 0| 0| 0| | 1| 2| 3| | 1|-2| -1| # no overflows at 16 bit limits | 32767| 1| 32768| | 65535| 1| 65536| # integer overflow in Python? | 2147483648| 1| 2147483649| |-2147483647|-1|-2147483648| |-2147483648|-1|-2147483649|
Tento scénář se bude pro každý řádek tabulky opakovat, přičemž v každé iteraci se namísto textů <x>, <y> a <result> dosadí hodnoty z příslušného sloupce tabulky. Jedná se přitom o pouhou textovou substituci, takže ve skutečnosti je možné s tabulkami provádět i dosti složité operace.
8. Spuštění druhé varianty skriptu s testy, výsledek testů
Pokud nyní spustíme testovací scénář již známým skriptem run_tests.sh, měl by výsledek vypadat přibližně následovně:
Feature: Adder test # features/adder.feature:1 Scenario: Check the function add() # features/adder.feature:4 Given The function add is callable # features/steps/common.py:5 When I call function add with arguments 1 and 2 # features/steps/common.py:12 Then I should get 3 as a result # features/steps/common.py:17 Scenario Outline: Thorough checking function add() -- @1.1 result # features/adder.feature:17 Given The function add is callable # features/steps/common.py:5 When I call function add with arguments 0 and 0 # features/steps/common.py:12 Then I should get 0 as a result # features/steps/common.py:17 Scenario Outline: Thorough checking function add() -- @1.2 result # features/adder.feature:18 Given The function add is callable # features/steps/common.py:5 When I call function add with arguments 1 and 2 # features/steps/common.py:12 Then I should get 3 as a result # features/steps/common.py:17 Scenario Outline: Thorough checking function add() -- @1.3 result # features/adder.feature:19 Given The function add is callable # features/steps/common.py:5 When I call function add with arguments 1 and -2 # features/steps/common.py:12 Then I should get -1 as a result # features/steps/common.py:17 Scenario Outline: Thorough checking function add() -- @1.4 result # features/adder.feature:21 Given The function add is callable # features/steps/common.py:5 When I call function add with arguments 32767 and 1 # features/steps/common.py:12 Then I should get 32768 as a result # features/steps/common.py:17 Scenario Outline: Thorough checking function add() -- @1.5 result # features/adder.feature:22 Given The function add is callable # features/steps/common.py:5 When I call function add with arguments 65535 and 1 # features/steps/common.py:12 Then I should get 65536 as a result # features/steps/common.py:17 Scenario Outline: Thorough checking function add() -- @1.6 result # features/adder.feature:24 Given The function add is callable # features/steps/common.py:5 When I call function add with arguments 2147483648 and 1 # features/steps/common.py:12 Then I should get 2147483649 as a result # features/steps/common.py:17 Scenario Outline: Thorough checking function add() -- @1.7 result # features/adder.feature:25 Given The function add is callable # features/steps/common.py:5 When I call function add with arguments -2147483647 and -1 # features/steps/common.py:12 Then I should get -2147483648 as a result # features/steps/common.py:17 Scenario Outline: Thorough checking function add() -- @1.8 result # features/adder.feature:26 Given The function add is callable # features/steps/common.py:5 When I call function add with arguments -2147483648 and -1 # features/steps/common.py:12 Then I should get -2147483649 as a result # features/steps/common.py:17 1 feature passed, 0 failed, 0 skipped 9 scenarios passed, 0 failed, 0 skipped 27 steps passed, 0 failed, 0 skipped, 0 undefined Took 0m0.002s
![](https://i.iinfo.cz/images/549/behave1-2-prev.png)
Obrázek 3: Terminál s výstupem vytvořeným druhou variantou testovacího scénáře.
9. Testování funkcí z nativních knihoven
Kombinaci Python + Gherkin je možné použít i ve chvíli, kdy je nutné vytvořit testovací scénáře popisující chování funkcí z nativních knihoven, například funkcí naprogramovaných v klasickém céčku. V této kapitole si tedy jen v rychlosti ukážeme, jakým způsobem je vlastně možné přeložit céčkovou funkci do sdílené knihovny, načíst tuto knihovnu do běžícího interpretru Pythonu a následně nativní funkci zavolat z Pythonu s využitím knihovny ctypes.
Základem bude zdrojový kód uložený do souboru adder.c. Tento zdrojový kód bude obsahovat jen jedinou funkci add:
extern int add(int x, int y) { return x+y; }
Soubor adder.c přeložíme do objektového souboru adder.o a následně z tohoto objektového souboru vytvoříme sdílenou knihovnu (shared library) pojmenovanou libadder.so:
gcc -Wall -ansi -c -fPIC adder.c -o adder.o gcc -shared -Wl,-soname,libadder.so -o libadder.so adder.o
Následuje důležitá část – načtení sdílené knihovny do běžícího interpretu jazyka Python, získání reference na nativní funkci add a zavolání této funkce z Pythonu (s poloautomatickou typovou konverzí):
import ctypes libname = "libadder.so" adder = ctypes.CDLL(libname) for x in range(0, 6): for y in range(0, 6): result = adder.add(x,y) print("{x} + {y} = {z}".format(x=x, y=y, z=result))
LD_LIBRARY_PATH=. python3 call_adder.py
Pokud byla knihovna vytvořena korektně, byla nalezena, načtena do interpretru a našla se v ní funkce add, měl by skript vypsat tento výstup:
0 + 0 = 0 0 + 1 = 1 0 + 2 = 2 0 + 3 = 3 0 + 4 = 4 0 + 5 = 5 1 + 0 = 1 1 + 1 = 2 1 + 2 = 3 1 + 3 = 4 1 + 4 = 5 1 + 5 = 6 2 + 0 = 2 2 + 1 = 3 2 + 2 = 4 2 + 3 = 5 2 + 4 = 6 2 + 5 = 7 3 + 0 = 3 3 + 1 = 4 3 + 2 = 5 3 + 3 = 6 3 + 4 = 7 3 + 5 = 8 4 + 0 = 4 4 + 1 = 5 4 + 2 = 6 4 + 3 = 7 4 + 4 = 8 4 + 5 = 9 5 + 0 = 5 5 + 1 = 6 5 + 2 = 7 5 + 3 = 8 5 + 4 = 9 5 + 5 = 10
Pokud naopak k nalezení a/nebo načtení knihovny z různých důvodů nedošlo, vypíše se odlišná zpráva a skript je ihned poté ukončen:
Traceback (most recent call last): File "call_adder.py", line 5, in <module> adder = ctypes.CDLL(libname) File "/usr/lib/python3.4/ctypes/__init__.py", line 351, in __init__ self._handle = _dlopen(self._name, mode) OSError: libadder.so: cannot open shared object file: No such file or directory
10. Projekt, v němž budeme testovat vlastnosti nativní funkce
Nyní se podívejme na strukturu projektu, v němž budeme testovat základní vlastnosti vybrané nativní funkce na základě testovacího scénáře. Celá struktura projektu bude založena na projektech předchozích, ovšem s tím rozdílem, že se v projektu nově objevil adresář lib obsahující zdrojový kód napsaný v jazyku C a skript určený pro vytvoření nativní knihovny:
. ├── feature_list.txt ├── features │ ├── environment.py │ ├── smoketest.feature │ └── steps │ └── common.py ├── lib │ ├── adder.c │ ├── clean.sh │ └── make_library.sh ├── requirements.in ├── requirements.txt └── run_tests.sh 3 directories, 10 files
11. Upravený testovací scénář
Testovací scénář nyní upravíme, a to takovým způsobem, že budeme očekávat přetečení výsledků za předpokladu, že velikost datového typu int je 32 bitů. Pokud tato podmínka nebude splněna, testy selžou:
Feature: Smoke test Scenario: Check the function int add(int, int) Given The library libadder.so is loaded When I call native function add with arguments 1 and 2 Then I should get 3 as a result Scenario Outline: Thorough checking function int add(int, int) Given The library libadder.so is loaded When I call native function add with arguments <x> and <y> Then I should get <result> as a result Examples: result |x|y|result| # basic arithmetic | 0| 0| 0| | 1| 2| 3| | 1|-2| -1| # no overflows at 16 bit limits | 32767| 1| 32768| | 65535| 1| 65536| # integer overflow | 2147483648| 1|-2147483647| |-2147483647|-1|-2147483648| |-2147483648|-1| 2147483647|
12. Upravená implementace testovacích kroků
Samozřejmě budeme muset upravit i implementaci testovacích kroků. Ovšem ještě předtím vytvoříme nový soubor nazvaný environment.py, který bude uložen do adresáře features, tj. do stejného adresáře, v němž se nachází i všechny soubory .feature. V tomto modulu budou deklarovány pomocné funkce volané automaticky interpretrem jazyka Gherkin. Nejdříve provedeme import potřebných modulů (ctypes pro načtení nativní knihovny, druhý modul pro logování, což prozatím nevyužijeme):
from behave.log_capture import capture import ctypes
V tomto modulu si dále vytvoříme pomocnou funkci určenou pro načtení nativní knihovny a uložení reference na ni do kontextu:
def _load_library(context, library_name): if context.tested_library is None: context.tested_library = ctypes.CDLL(library_name)
Důležitá je funkce pojmenovaná before_all. Tato funkce je zavolána automaticky před vlastním testovacím scénářem a typicky se v ní nastavuje počáteční stav kontextu (což je jediný parametr předaný funkci při jejím volání):
def before_all(context): """Perform setup before the first event.""" context.tested_library = None context.load_library = _load_library
Implementace kroků testu se opět nachází v souboru common.py. Povšimněte si provedených změn, především toho, že v given se pokusíme načíst nativní knihovnu zadaného jména. Pokud se načtení nativní knihovny podaří, bude v dalších krocích dostupná v atributu context.tested_library:
from behave import given, then, when @given('The library {library_name} is loaded') def initial_state(context, library_name): context.load_library(context, library_name) @when('I call native function add with arguments {x:d} and {y:d}') def call_add(context, x, y): context.result = context.tested_library.add(x, y) @then('I should get {result:d} as a result') def check_integer_result(context, result): assert context.result == result, "Expected result: {e}, returned value: {r}".format(e=result, r=context.result)
13. Spuštění skriptu s testy nativní funkce, výsledek testů
Pokud byla nativní funkce korektně přeložena, měly by se po spuštění skriptu run_tests.sh na standardním výstupu objevit následující řádky produkované knihovnou Behave:
Feature: Smoke test # features/smoketest.feature:1 @smoketest Scenario: Check the function int add(int, int) # features/smoketest.feature:4 Given The library libadder.so is loaded # features/steps/common.py:4 When I call native function add with arguments 1 and 2 # features/steps/common.py:9 Then I should get 3 as a result # features/steps/common.py:14 Scenario Outline: Thorough checking function int add(int, int) -- @1.1 result # features/smoketest.feature:17 Given The library libadder.so is loaded # features/steps/common.py:4 When I call native function add with arguments 0 and 0 # features/steps/common.py:9 Then I should get 0 as a result # features/steps/common.py:14 Scenario Outline: Thorough checking function int add(int, int) -- @1.2 result # features/smoketest.feature:18 Given The library libadder.so is loaded # features/steps/common.py:4 When I call native function add with arguments 1 and 2 # features/steps/common.py:9 Then I should get 3 as a result # features/steps/common.py:14 Scenario Outline: Thorough checking function int add(int, int) -- @1.3 result # features/smoketest.feature:19 Given The library libadder.so is loaded # features/steps/common.py:4 When I call native function add with arguments 1 and -2 # features/steps/common.py:9 Then I should get -1 as a result # features/steps/common.py:14 Scenario Outline: Thorough checking function int add(int, int) -- @1.4 result # features/smoketest.feature:21 Given The library libadder.so is loaded # features/steps/common.py:4 When I call native function add with arguments 32767 and 1 # features/steps/common.py:9 Then I should get 32768 as a result # features/steps/common.py:14 Scenario Outline: Thorough checking function int add(int, int) -- @1.5 result # features/smoketest.feature:22 Given The library libadder.so is loaded # features/steps/common.py:4 When I call native function add with arguments 65535 and 1 # features/steps/common.py:9 Then I should get 65536 as a result # features/steps/common.py:14 Scenario Outline: Thorough checking function int add(int, int) -- @1.6 result # features/smoketest.feature:24 Given The library libadder.so is loaded # features/steps/common.py:4 When I call native function add with arguments 2147483648 and 1 # features/steps/common.py:9 Then I should get -2147483647 as a result # features/steps/common.py:14 Scenario Outline: Thorough checking function int add(int, int) -- @1.7 result # features/smoketest.feature:25 Given The library libadder.so is loaded # features/steps/common.py:4 When I call native function add with arguments -2147483647 and -1 # features/steps/common.py:9 Then I should get -2147483648 as a result # features/steps/common.py:14 Scenario Outline: Thorough checking function int add(int, int) -- @1.8 result # features/smoketest.feature:26 Given The library libadder.so is loaded # features/steps/common.py:4 When I call native function add with arguments -2147483648 and -1 # features/steps/common.py:9 Then I should get 2147483647 as a result # features/steps/common.py:14 1 feature passed, 0 failed, 0 skipped 9 scenarios passed, 0 failed, 0 skipped 27 steps passed, 0 failed, 0 skipped, 0 undefined Took 0m0.003s
![](https://i.iinfo.cz/images/549/behave1-3-prev.png)
Obrázek 4: Výsledek běhu testovacích scénářů ve chvíli, kdy se nativní funkce chová podle očekávání.
14. Spuštění skriptu ve chvíli, kdy není nativní funkce nalezena
V případě, že jsme vynechali krok překladu nativní funkce popř. se nepodařilo nativní knihovnu vůbec načíst, bude výsledek testů mnohem delší a pesimističtější:
Feature: Smoke test # features/smoketest.feature:1 @smoketest Scenario: Check the function int add(int, int) # features/smoketest.feature:4 Given The library libadder.so is loaded # features/steps/common.py:4 Traceback (most recent call last): File "/usr/local/lib/python3.4/dist-packages/behave/model.py", line 1456, in run match.run(runner.context) File "/usr/local/lib/python3.4/dist-packages/behave/model.py", line 1903, in run self.func(context, *args, **kwargs) File "features/steps/common.py", line 6, in initial_state context.load_library(context, library_name) File "features/environment.py", line 7, in _load_library context.tested_library = ctypes.CDLL(library_name) File "/usr/lib/python3.4/ctypes/__init__.py", line 351, in __init__ self._handle = _dlopen(self._name, mode) OSError: libadder.so: cannot open shared object file: No such file or directory When I call native function add with arguments 1 and 2 # None Then I should get 3 as a result # None
…následují informace o pádu všech dalších testů…
Failing scenarios: features/smoketest.feature:4 Check the function int add(int, int) features/smoketest.feature:17 Thorough checking function int add(int, int) -- @1.1 result features/smoketest.feature:18 Thorough checking function int add(int, int) -- @1.2 result features/smoketest.feature:19 Thorough checking function int add(int, int) -- @1.3 result features/smoketest.feature:21 Thorough checking function int add(int, int) -- @1.4 result features/smoketest.feature:22 Thorough checking function int add(int, int) -- @1.5 result features/smoketest.feature:24 Thorough checking function int add(int, int) -- @1.6 result features/smoketest.feature:25 Thorough checking function int add(int, int) -- @1.7 result features/smoketest.feature:26 Thorough checking function int add(int, int) -- @1.8 result 0 features passed, 1 failed, 0 skipped 0 scenarios passed, 9 failed, 0 skipped 0 steps passed, 9 failed, 18 skipped, 0 undefined Took 0m0.002s
![](https://i.iinfo.cz/images/549/behave1-4-prev.png)
Obrázek 5: Výsledek běhu testovacího scénáře ve chvíli, kdy se nepodařilo načíst nativní funkci.
15. Další modifikace testovacího scénáře – určení typů parametrů předávaných nativní funkci
Nyní si testovací scénář nepatrně modifikujeme, protože uvedeme i typy parametrů předávaných do nativní funkce. V následujícím výpisu jsou změněné části zvýrazněny tučně:
Feature: Smoke test Scenario: Check the function int add(int, int) Given The library libadder.so is loaded When I call native function add with integer arguments 1 and 2 Then I should get 3 as a result Scenario Outline: Thorough checking function int add(int, int) Given The library libadder.so is loaded When I call native function add with integer arguments <x> and <y> Then I should get <result> as a result Examples: result |x|y|result| # basic arithmetic | 0| 0| 0| | 1| 2| 3| | 1|-2| -1| # no overflows at 16 bit limits | 32767| 1| 32768| | 65535| 1| 65536| # integer overflow | 2147483648| 1|-2147483647| |-2147483647|-1|-2147483648| |-2147483648|-1| 2147483647|
16. Modifikace kroků testů
I jednotlivé implementace testovacích kroků budeme modifikovat. Zejména se to týká kroku:
„I call native function add with integer arguments {x:d} and {y:d}“
který je nahrazen za obecnější krok, v němž se jméno funkce vyskytuje jako proměnný text:
„I call native function {function} with integer arguments {x:d} and {y:d}“
Podívejme se nyní na způsob zavolání funkce, jejíž jméno je proměnné:
from behave import given, then, when @given('The library {library_name} is loaded') def initial_state(context, library_name): context.load_library(context, library_name) @when('I call native function {function} with integer arguments {x:d} and {y:d}') def call_add(context, function, x, y): context.result = getattr(context.tested_library, function)(x, y) @then('I should get {result:d} as a result') def check_integer_result(context, result): assert context.result == result
17. Spuštění modifikovaného skriptu s testy nativní funkce, výsledek testů
Po spuštění modifikovaného skriptu s testy by se měly na standardním výstupu objevit tyto zprávy, které nás informují o tom, že se nativní funkce add skutečně chová podle očekávání:
Feature: Smoke test # features/smoketest.feature:1 Scenario: Check the function int add(int, int) # features/smoketest.feature:4 Given The library libadder.so is loaded # features/steps/common.py:4 When I call native function add with integer arguments 1 and 2 # features/steps/common.py:9 Then I should get 3 as a result # features/steps/common.py:14 Scenario Outline: Thorough checking function int add(int, int) -- @1.1 result # features/smoketest.feature:17 Given The library libadder.so is loaded # features/steps/common.py:4 When I call native function add with integer arguments 0 and 0 # features/steps/common.py:9 Then I should get 0 as a result # features/steps/common.py:14 Scenario Outline: Thorough checking function int add(int, int) -- @1.2 result # features/smoketest.feature:18 Given The library libadder.so is loaded # features/steps/common.py:4 When I call native function add with integer arguments 1 and 2 # features/steps/common.py:9 Then I should get 3 as a result # features/steps/common.py:14 Scenario Outline: Thorough checking function int add(int, int) -- @1.3 result # features/smoketest.feature:19 Given The library libadder.so is loaded # features/steps/common.py:4 When I call native function add with integer arguments 1 and -2 # features/steps/common.py:9 Then I should get -1 as a result # features/steps/common.py:14 Scenario Outline: Thorough checking function int add(int, int) -- @1.4 result # features/smoketest.feature:21 Given The library libadder.so is loaded # features/steps/common.py:4 When I call native function add with integer arguments 32767 and 1 # features/steps/common.py:9 Then I should get 32768 as a result # features/steps/common.py:14 Scenario Outline: Thorough checking function int add(int, int) -- @1.5 result # features/smoketest.feature:22 Given The library libadder.so is loaded # features/steps/common.py:4 When I call native function add with integer arguments 65535 and 1 # features/steps/common.py:9 Then I should get 65536 as a result # features/steps/common.py:14 Scenario Outline: Thorough checking function int add(int, int) -- @1.6 result # features/smoketest.feature:24 Given The library libadder.so is loaded # features/steps/common.py:4 When I call native function add with integer arguments 2147483648 and 1 # features/steps/common.py:9 Then I should get -2147483647 as a result # features/steps/common.py:14 Scenario Outline: Thorough checking function int add(int, int) -- @1.7 result # features/smoketest.feature:25 Given The library libadder.so is loaded # features/steps/common.py:4 When I call native function add with integer arguments -2147483647 and -1 # features/steps/common.py:9 Then I should get -2147483648 as a result # features/steps/common.py:14 Scenario Outline: Thorough checking function int add(int, int) -- @1.8 result # features/smoketest.feature:26 Given The library libadder.so is loaded # features/steps/common.py:4 When I call native function add with integer arguments -2147483648 and -1 # features/steps/common.py:9 Then I should get 2147483647 as a result # features/steps/common.py:14 1 feature passed, 0 failed, 0 skipped 9 scenarios passed, 0 failed, 0 skipped 27 steps passed, 0 failed, 0 skipped, 0 undefined Took 0m0.003s
18. Repositář s demonstračními příklady
Všech pět demonstračních projektů které jsme si dnes popsali, byly společně s projektem obsahujícím pouze nativní funkci volanou z Pythonu, uloženy do repositáře, který naleznete na adrese https://github.com/tisnik/python-behave-demos. V první tabulce jsou zobrazeny odkazy na tyto projekty:
Projekt | Popis | Cesta |
---|---|---|
test_python_function1 | první příklad s jedinou testovanou funkcí | https://github.com/tisnik/python-behave-demos/tree/master/test_python_function1 |
test_python_function2 | vylepšení o osnovu atd. | https://github.com/tisnik/python-behave-demos/tree/master/test_python_function2 |
test_python_function3 | změna struktury (více .features atd.) | https://github.com/tisnik/python-behave-demos/tree/master/test_python_function3 |
native_lib | ukázka volání nativní funkce | https://github.com/tisnik/python-behave-demos/tree/master/native_lib |
test_native_lib1 | test nativní funkce | https://github.com/tisnik/python-behave-demos/tree/master/test_native_lib1 |
test_native_lib2 | vylepšený test | https://github.com/tisnik/python-behave-demos/tree/master/test_native_lib2 |
Ve druhé tabulce jsou zobrazeny odkazy na demonstrační příklady, které budou použity ve druhé části tohoto článku:
github_test_version1 | https://github.com/tisnik/python-behave-demos/tree/master/github_test_version1 |
github_test_version2 | https://github.com/tisnik/python-behave-demos/tree/master/github_test_version2 |
github_test_version3 | https://github.com/tisnik/python-behave-demos/tree/master/github_test_version3 |
github_test_version4 | https://github.com/tisnik/python-behave-demos/tree/master/github_test_version4 |
19. Odkazy na Internetu
- Behave na GitHubu
https://github.com/behave/behave - behave 1.2.6 (PyPi)
https://pypi.python.org/pypi/behave - Dokumentace k Behave
http://behave.readthedocs.io/en/latest/ - Příklady použití Behave
https://github.com/behave/behave.example - Příklady použití Behave použité v dnešním článku
https://github.com/tisnik/python-behave-demos - Test Fixture (Wikipedia)
https://en.wikipedia.org/wiki/Test_fixture - Behavior-driven development (Wikipedia)
https://en.wikipedia.org/wiki/Behavior-driven_development - Cucumber
https://cucumber.io/ - Jasmine
https://jasmine.github.io/ - Pip (dokumentace)
https://pip.pypa.io/en/stable/ - Tox
https://tox.readthedocs.io/en/latest/ - Extrémní programování
https://cs.wikipedia.org/wiki/Extr%C3%A9mn%C3%AD_programov%C3%A1n%C3%AD - Programování řízené testy
https://cs.wikipedia.org/wiki/Programov%C3%A1n%C3%AD_%C5%99%C3%ADzen%C3%A9_testy - Test-driven development (Wikipedia)
https://en.wikipedia.org/wiki/Test-driven_development - Python namespaces
https://bytebaker.com/2008/07/30/python-namespaces/ - Namespaces and Scopes
https://www.python-course.eu/namespaces.php - pdb — The Python Debugger
https://docs.python.org/3.6/library/pdb.html - pdb – Interactive Debugger
https://pymotw.com/2/pdb/