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í).
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:
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
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
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
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/