Hlavní navigace

Testování nativních funkcí s využitím programovacího jazyka Python

 Autor: Depositphotos
V dnešní části seriálu o testování aplikací s využitím Pythonu se budeme zabývat možná poněkud méně známým tématem. Bude se jednat o testování nativních funkcí (či celých nativních knihoven nebo dokonce aplikací) s využitím Pythonu.
Pavel Tišnovský 31. 7. 2020
Doba čtení: 56 minut

Sdílet

11. Volání céčkových funkcí akceptujících odlišné typy argumentů

12. Práce s datovými strukturami

13. Porovnání komplexních čísel

14. Rozšíření knihovny pro práci s komplexními čísly

15. Nativní knihovny a behavior-driven testing založený na použití knihovny Behave

16. Projekt, v němž budeme testovat vlastnosti nativní funkce

17. Modifikace testovacího scénáře – určení typů parametrů předávaných nativní funkci

18. Repositář s demonstračními příklady

19. Předchozí články s tématem testování (nejenom) v Pythonu

20. Odkazy na Internetu

1. Testování nativních funkcí s využitím programovacího jazyka Python

V dnešní části seriálu o testování aplikací s využitím programovacího jazyka Python se budeme zabývat možná poněkud méně známým tématem. Bude se jednat o testování nativních funkcí (či celých nativních knihoven nebo dokonce aplikací) s využitím Pythonu. Dnes použité demonstrační příklady, resp. přesněji řečeno jejich část, která má být překládána do nativního (strojového) kódu, budou psány v programovacím jazyku C. Propojíme tak céčko (což je stále nejpopulárnější nízkoúrovňový jazyk) s dnes pravděpodobně nejpopulárnějším vysokoúrovňovým programovacím jazykem současnosti. Ovšem stejně dobře lze použít i další překládané jazyky, zejména Rust a C++ – ovšem u obou těchto jazyků je nutné zakázat name mangling, tedy „dekorování“ jmen symbolů ve vytvářených objektových souborech i staticky či dynamicky linkovaných knihovnách.

Na straně Pythonu lze pro volání nativních funkcí použít například standardní modul ctypes nebo sice méně známý, ale o to povedenější modul pojmenovaný CFFI. Dnes použité příklady budou pro jednoduchost používat modul ctypes, který ve svém systému již pravděpodobně máte nainstalovaný. Modulem CFFI se budeme podrobněji zabývat v navazujícím článku.

Prezentovány budou dva typy testů. První typ je založen na nástroji pytest, což znamená, že samotné testy svoji strukturou připomínají jednotkové testy, stejné je jejich vyhodnocení apod. Pochopitelně – pokud tedy nepoužijeme ještě další nástroje – ovšem nezjistíme pokrytí nativního kódu těmito testy. V závěrečné části článku si ovšem ukážeme i behaviorální testy (BDD), které budou založeny na použití frameworku Behave a doménově specifického jazyka Gherkin. Obě zmíněné technologie již na Rootu byly představeny, takže závěrečná část článku je spíše shrnutím již známých věcí.

Poznámka: pravděpodobně si nyní pokládáte otázku, jestli je vůbec dobrým nápadem testovat nativní aplikace s využitím testů naprogramovaných v Pythonu. Co se týká Rustu (popř. Go či D), tak je s velkou pravděpodobností výhodnější použít přímo tooling určený pro daný ekosystém, ale v případě jazyka C může být (a velmi často bývá) použití Pythonu poměrně efektivním řešením, které je flexibilnější, než snaha psát jednotkové testy přímo v céčku.

2. Zdrojový kód funkce, která se má přeložit do dynamické knihovny

Základem několika dále použitých demonstračních příkladů bude zdrojový kód vyvinutý v programovacím jazyku C, který je uložený do souboru pojmenovaného adder.c. Tento zdrojový kód bude obsahovat pouze jedinou funkci se jménem add a triviálním kódem:

extern int add(int x, int y)
{
    return x+y;
}

Jak je ze zdrojového kódu patrné, akceptuje tato funkce dva parametry typu int a vrací hodnotu, která je taktéž typu int. To může být poněkud problematické, protože standard programovacího jazyka C sice poměrně přesně a striktně určuje vlastnosti datového typu int, ale již neříká, jaká je konkrétní bitová délka (což je ostatně ve většině případů dobře a v případě potřeby lze datový typ specifikovat přesněji, minimálně ve standardu C99). Podle použité architektury se může jednat o šestnáctibitová čísla, většinou půjde o čísla 32bitová, ovšem na specializovaných architekturách nalezneme například i 24bitové hodnoty (typické je použití 24bitových akumulátorů u některých digitálně-signálových procesorů neboli DSP, příkladem může být slavná řada TMS320).

3. Zdrojový kód Pythonovského skriptu, který má dynamickou knihovnu využít

Nyní je nutné vytvořit druhou část projektu naprogramovanou pro změnu ve vysokoúrovňovém Pythonu. Tato část bude prozatím velmi jednoduchá, protože sestává z jediného souboru pojmenovaného call_from_python.py uloženého v adresáři s projektem (což je opět řešení zvolené pouze pro jeho jednoduchost, v reálném světě bude situace poněkud odlišná):

"""Základní použití balíčku ctypes."""
 
import ctypes
 
 
def load_library(library_name):
    """Načtení nativní knihovny."""
    return ctypes.CDLL(library_name)
 
 
def main():
    """Otestování, jestli je možné zavolat nativní funkci."""
    library = load_library("libadder.so")
    print(library)
    result = library.add(1, 2)
    print(f"1+2=", result, sep="")
 
 
if __name__ == '__main__':
    # pouze se ujistíme, že lze zavolat nativní funkci
    main()

Povšimněte si, že se nejdříve ve funkci load_library pokusíme otevřít dynamickou knihovnu, k níž je uvedena plná cesta. To není obvyklý způsob, neboť v praxi je lepší se spolehnout na proměnnou prostředí LD_LIBRARY_PATH, což si ukážeme v dalších demonstračních projektech. Dále zavoláme nativní funkci add načtenou z této knihovny, získáme její výsledek a ten vypíšeme na standardní výstup.

Pokud se nyní pokusíme skript call_from_python.py spustit v interpretru Pythonu, dojde k chybě, protože načítaná knihovna nebude nalezena (existuje prozatím pouze ve zdrojové podobě, ale nebyla přeložena):

$ python3 call_from_python.py
 
Traceback (most recent call last):
  File "call_from_python.py", line 21, in
    main()
  File "call_from_python.py", line 13, in main
    library = load_library("libadder.so")
  File "call_from_python.py", line 8, in load_library
    return ctypes.CDLL(library_name)
  File "/usr/lib64/python3.6/ctypes/__init__.py", line 343, in __init__
    self._handle = _dlopen(self._name, mode)
OSError: libadder.so: cannot open shared object file: No such file or directory

4. Překlad projektu a pokus o zavolání nativní funkce z dynamické knihovny

Soubor adder.c nyní přeložíme do objektového souboru, který bude pojmenován adder.o. Povšimněte si použití volby PIC, kterou se zapíná takzvaný Position Independent Code (tedy instrukcí nepoužívajících absolutní skoky) a která se používá při vytváření sdílených knihoven na některých architekturách (na x86_64 však v našem jednoduchém příkladu dostaneme stejný výsledek i bez použití této volby):

$ gcc -Wall -ansi -c -fPIC adder.c -o adder.o

Následně z tohoto objektového souboru vytvoříme sdílenou knihovnu (shared library) pojmenovanou libadder.so (přípona .so značí „shared object“):

$ gcc -shared -Wl,-soname,libadder.so -o libadder.so adder.o

Přesvědčíme se, že soubor libadder.so skutečně vznikl:

$ file libadder.so 
 
libadder.so: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, BuildID[sha1]=85fe85114c2f6582a8764ca22ba578df35381fec, not stripped

Popř. se můžeme podívat i na symboly, které jsou v tomto souboru definovány. Použijeme k tomu nástroj nm:

00000000000005ca T add
0000000000201018 B __bss_start
0000000000201018 b completed.6984
                 w __cxa_finalize@@GLIBC_2.2.5
00000000000004f0 t deregister_tm_clones
0000000000000580 t __do_global_dtors_aux
0000000000200e30 t __do_global_dtors_aux_fini_array_entry
0000000000200e38 d __dso_handle
0000000000200e40 d _DYNAMIC
0000000000201018 D _edata
0000000000201020 B _end
00000000000005e0 T _fini
00000000000005c0 t frame_dummy
0000000000200e28 t __frame_dummy_init_array_entry
0000000000000688 r __FRAME_END__
0000000000201000 d _GLOBAL_OFFSET_TABLE_
                 w __gmon_start__
00000000000005ec r __GNU_EH_FRAME_HDR
00000000000004b8 T _init
                 w _ITM_deregisterTMCloneTable
                 w _ITM_registerTMCloneTable
0000000000000530 t register_tm_clones
0000000000201018 d __TMC_END__

Můžeme se dokonce podívat, jakým způsobem se funkce add přeložila do strojového kódu. K tomuto účelu použijeme nástroj objdump a pro získání disassemblovaného textu zvolené funkce použijeme trik s awk, který z výpisu „vykousne“ pouze požadovanou funkci:

$ objdump -d -M intel libadder.so | awk -F"\n" -v RS="\n\n" '$1 ~ /add/'
 
00000000000005ca <add>:
 5ca:   55                      push   rbp
 5cb:   48 89 e5                mov    rbp,rsp
 5ce:   89 7d fc                mov    DWORD PTR [rbp-0x4],edi
 5d1:   89 75 f8                mov    DWORD PTR [rbp-0x8],esi
 5d4:   8b 55 fc                mov    edx,DWORD PTR [rbp-0x4]
 5d7:   8b 45 f8                mov    eax,DWORD PTR [rbp-0x8]
 5da:   01 d0                   add    eax,edx
 5dc:   5d                      pop    rbp
 5dd:   c3
Poznámka: aby bylo možné sdílenou knihovnu nalézt, je nutné nastavit proměnnou prostředí LD_LIBRARY_PATH (jinak by se knihovna hledala v /usr/lib, popř. /usr/lib64 atd. ale nikoli v aktuálním adresáři.). V našem konkrétním případě to znamená, že se interpret Pythonu zavolá takto:
$ LD_LIBRARY_PATH=. python3 call_from_python.py

Tento řádek je uložen ve skriptu run.sh, který je možné přímo použít:

$ ./run.sh
 
<CDLL 'libadder.so', handle 55ac5f4a1bf0 at 0x7fde59f99080>
1+2=3
Poznámka: všechny zdrojové soubory a skripty potřebné pro překlad a spuštění dnešního prvního demonstračního příkladu jsou dostupné na adrese https://github.com/tisnik/testing-in-python/tree/master/native_lib­s/project1.

5. Malá úprava demonstračního příkladu – obalení nativní funkce Pythonovským kódem

Předchozí demonstrační příklad upravíme, a to takovým způsobem, že se nativní funkce add obalí Pythonovskou funkcí taktéž nazvanou add. Navíc do nového modulu adder.py přidáme kód pro načtení a inicializaci dynamicky sdílené knihovny:

"""Základní použití balíčku ctypes, modul pro import."""
 
import ctypes
 
 
library = None
 
 
def load_library(library_name):
    """Načtení nativní knihovny."""
    return ctypes.CDLL(library_name)
 
 
def add(x, y):
    """Zavolání externí funkce."""
    return library.add(x, y)
 
 
def init():
    global library
    library = load_library("libadder.so")
 
 
def main():
    """Otestování, jestli je možné zavolat nativní funkci."""
    init()
    print(library)
    result = add(1, 2)
    print(f"1+2=", result, sep="")
 
 
if __name__ == '__main__':
    # pouze se ujistíme, že lze zavolat nativní funkci
    main()
Poznámka: ve skutečnosti ovšem nic nezabrání ostatnímu kódu, aby se funkce add zavolala s rozdílnými parametry, například s dvojicí řetězců.

Nový modul adder se použije relativně snadno – pouze musíme zavolat inicializační funkci a následně je již možné používat funkci add:

"""Základní použití balíčku ctypes."""
 
from adder import init, add
 
 
def main():
    """Otestování, jestli je možné zavolat nativní funkci."""
    init()
    result = add(1, 2)
    print(f"1+2=", result, sep="")
 
 
if __name__ == '__main__':
    # pouze se ujistíme, že lze zavolat nativní funkci
    main()

Překlad céčkového kódu do nativní sdílené knihovny opět zajišťuje příslušný skript:

$ ./make_library.sh

Pokud k překladu a vytvoření knihovny došlo, dopadne spuštění příkladu podle očekávání – vypíše se výsledek součtu:

$ ./run.sh
 
1+2=3

V předchozím textu bylo uvedeno, že funkce add nijak nekontroluje typ parametrů – pouze je předá do nativního kódu. To například umožňuje, aby se tato funkce zavolala s dvojicí řetězců, přičemž výsledkem pochopitelně bude nesmysl (sečtou se hodnoty dvou ukazatelů nebo jejich částí):

def main():
    """Otestování, jestli je možné zavolat nativní funkci."""
    init()
    result = add("foo", "bar")
    print(f"'foo'+'bar'=", result, sep="")

Špatné chování si můžeme snadno otestovat:

$ ./run_bad.sh
 
'foo'+'bar'=155795872
Poznámka: v tomto konkrétním případě by nemělo dojít k pádu, protože nativní funkce pouze sčítá dvojici celých čísel. Pokud by se ovšem očekávaly ukazatele (na pole atd.), mohlo by k pádu celé aplikace snadno dojít.

Druhý demonstrační příklad naleznete na adrese https://github.com/tisnik/testing-in-python/tree/master/native_lib­s/project2.

6. Jednotkové testy naprogramované v Pythonu, které volají a testují nativní funkci

Naši nativní céčkovou funkci pojmenovanou add:

extern int add(int x, int y)
{
    return x+y;
}

která je obalena vlastním Pythonovským balíčkem:

"""Základní použití balíčku ctypes, modul pro import."""
 
import ctypes
 
 
library = None
 
 
def load_library(library_name):
    """Načtení nativní knihovny."""
    return ctypes.CDLL(library_name)
 
 
def add(x, y):
    """Zavolání externí funkce."""
    return library.add(x, y)
 
 
def init():
    global library
    library = load_library("libadder.so")
 
 
def main():
    """Otestování, jestli je možné zavolat nativní funkci."""
    init()
    print(library)
    result = add(1, 2)
    print(f"1+2=", result, sep="")
 
 
if __name__ == '__main__':
    # pouze se ujistíme, že lze zavolat nativní funkci
    main()

pochopitelně můžeme relativně snadno otestovat přímo z Pythonu, a to pomocí nám již známého nástroje pytest. Kostra jednotkového testu může vypadat následovně:

"""Implementace jednotkových testů."""
 
import pytest
 
from adder import init, add
 
 
def test_add_basic():
    """Otestování výpočtu součtu dvou celých čísel."""
    init()
    result = add(1, 2)
    expected = 3
    assert result == expected, "Očekávaná hodnota {}, vráceno {}".format(expected, result)
Poznámka: všechny soubory třetího demonstračního příkladu jsou dostupné na adrese https://github.com/tisnik/testing-in-python/tree/master/native_lib­s/project3.

7. Spuštění jednotkových testů

Jednotkové testy (resp. přesněji řečeno jediný jednotkový test, který jsme prozatím vytvořili) lze spustit příkazem pytest. Nesmíme ovšem zapomenout na nastavení proměnné prostředí LD_LIBRARY_PATH. Pokud by tato proměnná prostředí nebyla nastavena (popř. byla nastavena nesprávně), nebylo by možné načíst nativní knihovnu a testy by ihned na začátku zhavarovaly. Spuštění jednotkových testů by tedy mohlo vypadat takto:

$ LD_LIBRARY_PATH=.;pytest -v

V případě, že nativní knihovna byla nalezena, měly by testy projít bez pádu:

============================= test session starts ==============================
platform linux -- Python 3.6.6, pytest-5.4.2, py-1.5.2, pluggy-0.13.1 -- /usr/bin/python3
cachedir: .pytest_cache
rootdir: /home/ptisnovs/src/python/testing-in-python/native_libs/project3
plugins: print-0.1.3, voluptuous-1.0.2
collecting ... collected 1 item
 
test_add.py::test_add_basic PASSED                                       [100%]
 
============================== 1 passed in 0.01s ===============================

8. Rozdíly mezi datovými typy jazyka C a Pythonu

Mezi programovacími jazyky C a Pythonem existuje velké množství rozdílů. Ty se týkají mj. i datových typů obou jazyků. A právě při volání nativních funkcí se někdy musí provádět konverze mezi různými datovými typy. Výjimek je jen několik a týkají se celých čísel (int) v 32bitovém či 64bitovém rozsahu, popř. polí bajtů, které jsou přenášeny jako řetězce (Unicode řetězce jako ukazatele na wchar_t). Při použití ostatních typů je nutné provádět konverze. K tomu slouží speciální datové typy knihovny ctypes, každý se svým konstruktorem:

# Typ v knihovně ctypes Typ v céčku Typ v Pythonu
1 c_bool _Bool bool
2 c_char char 1-character bytes object
3 c_wchar wchar_t 1-character string
4 c_byte char int
5 c_ubyte unsigned char int
6 c_short short int
7 c_ushort unsigned short int
8 c_int int int
9 c_uint unsigned int int
10 c_long long int
11 c_ulong unsigned long int
12 c_longlong __int64 nebo long long int
13 c_ulonglong unsigned __int64 nebo unsigned long long int
14 c_size_t size_t int
15 c_ssize_t ssize_t nebo Py_ssize_t int
16 c_float float float
17 c_double double float
18 c_longdouble long double float
19 c_char_p char * (NUL terminated) bytes object nebo None
20 c_wchar_p wchar_t * (NUL terminated) string nebo None
21 c_void_p void * int nebo None
Poznámka 1: typem int je v Pythonu myšlen datový typ s rozsahem omezeným jen kapacitou poskytované operační paměti a výpočetního výkonu. Jedná se tedy o datový typ zcela odlišný od céčka.
Poznámka 2: jméno typu z knihovny ctypes je současně i konstruktorem daného typu. Stačí tedy napsat například následující kód pro získání hodnoty kompatibilní s céčkovým typem float:
$ python3
Python 3.6.6 (default, Jul 19 2018, 16:29:00)
[GCC 7.3.1 20180303 (Red Hat 7.3.1-5)] on linux
Type "help", "copyright", "credits" or "license" for more information.
 
>>> import ctypes
>>> ctypes.c_float(1/2)
c_float(0.5)

9. Přetečení při operacích s celými čísly v C

V předchozí kapitole jsme si řekli, že typ int v Pythonu má prakticky neomezený rozsah. To však v žádném případě neplatí pro stejně pojmenovaný datový typ int z céčka, který může mít šířku typicky šestnáct bitů, 32bitů, 64bitů ale i 24 bitů (dříve podle použité platformy, dnes někdy i na základě tradice). Proto se nativní funkce add bude chovat rozdílně oproti operátoru + přímo v Pythonu. To si ostatně můžeme velmi snadno ověřit rozšířením jednotkových testů o sčítání hodnot, které se blíží 32bitovému limitu:

"""Implementace jednotkových testů."""
 
import pytest
 
from adder import init, add
 
 
def test_add_basic():
    """Otestování výpočtu součtu dvou celých čísel."""
    init()
    result = add(1, 2)
    expected = 3
    assert result == expected, "Očekávaná hodnota {}, vráceno {}".format(expected, result)
 
 
def test_add_large_ints():
    """Otestování výpočtu součtu dvou větších celých čísel."""
    init()
    result = add(2**31-2, 1)
    expected = 2**31-1
    assert result == expected, "Očekávaná hodnota {}, vráceno {}".format(expected, result)
 
 
def test_add_even_larger_ints():
    """Otestování výpočtu součtu dvou velkých celých čísel."""
    init()
    result = add(2**31-1, 1)
    expected = 2**31
    assert result == expected, "Očekávaná hodnota {}, vráceno {}".format(expected, result)

Při spuštění těchto jednotkových testů se může stát, že třetí test havaruje, a to právě kvůli přetečení výsledku v nativní funkci:

============================= test session starts ==============================
platform linux -- Python 3.6.6, pytest-5.4.2, py-1.5.2, pluggy-0.13.1 -- /usr/bin/python3
cachedir: .pytest_cache
rootdir: /home/ptisnovs/src/python/testing-in-python/native_libs/project4
plugins: print-0.1.3, voluptuous-1.0.2
collecting ... collected 3 items
 
test_add.py::test_add_basic PASSED                                       [ 33%]
test_add.py::test_add_large_ints PASSED                                  [ 66%]
test_add.py::test_add_even_larger_ints FAILED                            [100%]
 
=================================== FAILURES ===================================
__________________________ test_add_even_larger_ints ___________________________
 
    def test_add_even_larger_ints():
        """Otestování výpočtu součtu dvou velkých celých čísel."""
        init()
        result = add(2**31-1, 1)
        expected = 2**31
>       assert result == expected, "Očekávaná hodnota {}, vráceno {}".format(expected, result)
E       AssertionError: Očekávaná hodnota 2147483648, vráceno -2147483648
E       assert -2147483648 == 2147483648
E         +-2147483648
E         -2147483648
 
test_add.py:29: AssertionError
=========================== short test summary info ============================
FAILED test_add.py::test_add_even_larger_ints - AssertionError: Očekávaná hod...
========================= 1 failed, 2 passed in 0.03s ==========================
Poznámka: bitovou šířku typu int lze zjistit snadno:
#include <stdio.h>
 
int main(void) {
    printf("Bit size of int: %lu\n", 8*sizeof(int));
    return 0;
}

10. Jednoduchý test volající funkce ze standardní knihovny jazyka C

Kromě vlastních nativních knihoven pochopitelně můžeme z Pythonu volat i funkce ze standardní knihovny programovacího jazyka C. Na Linuxu jsou standardní funkce dostupné v knihovně libc.so.6 typicky umístěné v /usr/lib64/ nebo /usr/lib/. Všechny externí (volatelné) symboly (tedy především funkce) si ostatně můžeme zobrazit:

$ nm -g /usr/lib64/libc.so.6
 
0000000000042390 T a64l
0000000000035be0 T abort
00000000003b1d00 B __abort_msg
0000000000037880 T abs
00000000000f6070 T accept
00000000000f6a40 T accept4
00000000000e69f0 W access
00000000000ece00 T acct
...
...
...
0000000000127710 T xdr_vector
0000000000127880 T xdr_void
0000000000128240 T xdr_wrapstring
0000000000127380 T xencrypt
00000000000e6190 T __xmknod
00000000000e61f0 T __xmknodat
0000000000043c60 T __xpg_basename
0000000000034e50 W __xpg_sigpause
000000000008cb30 T __xpg_strerror_r
00000000001254c0 T xprt_register
00000000001255f0 T xprt_unregister
00000000000e60a0 T __xstat
00000000000e60a0 T __xstat64

Je tedy možné zavolat a otestovat základní funkcionalitu standardních funkcí time() a abs(). U první funkce ověříme, že vrácený čas (počet sekund od začátku Unixové éry) platí pro rok 2020 a další roky (vlastní výpočet je ovšem značně nepřesný – nepočítá například přestupné roky). U druhé funkce ověříme, jestli se skutečně počítá a vrací absolutní hodnota:

"""Implementace jednotkových testů."""
 
import pytest
import ctypes
 
libc = None
 
 
def setup_module(module):
    """Zavoláno při inicializaci modulu s testem."""
    global libc
    libc = ctypes.CDLL("libc.so.6")
 
 
def test_time():
    """Otestování nativní funkce time()."""
    t = libc.time(None)
    # velmi nepřesný odhad počtu sekund pro 2000-01-01
    t2020 = (2020-1970)*365*24*60*60
    assert t > t2020, "Neočekávaná hodnota {}".format(t)
 
 
def test_abs():
    """Otestování nativní funkce abs()."""
    x = libc.abs(-1)
    assert x == 1, "Neočekávaná hodnota {}".format(x)
 
    x = libc.abs(1)
    assert x == 1, "Neočekávaná hodnota {}".format(x)

Jednotkové testy nyní můžeme spustit bez nutnosti nastavování proměnné prostředí LD_LIBRARY_PATH:

$ pytest -v
 
============================= test session starts ==============================
platform linux -- Python 3.6.6, pytest-5.4.2, py-1.5.2, pluggy-0.13.1 -- /usr/bin/python3
cachedir: .pytest_cache
rootdir: /home/ptisnovs/src/python/testing-in-python/native_libs/project5
plugins: print-0.1.3, voluptuous-1.0.2
collecting ... collected 2 items
 
test_stdlib.py::test_time PASSED                                         [ 50%]
test_stdlib.py::test_abs PASSED                                          [100%]
 
============================== 2 passed in 0.01s ===============================

11. Volání céčkových funkcí akceptujících odlišné typy argumentů

Pokusme se nyní zavolat standardní funkci strlen() a předat jí řetězec v Pythonu:

def test_strlen():
    """Otestování nativní funkce strlen()."""
    x = libc.strlen("Hello")
    assert x == 5, "Neočekávaná hodnota {}".format(x)

Tento jednotkový test zařadíme do demonstračního příkladu k předchozím dvěma testům:

"""Implementace jednotkových testů."""
 
import pytest
import ctypes
 
libc = None
 
 
def setup_module(module):
    """Zavoláno při inicializaci modulu s testem."""
    global libc
    libc = ctypes.CDLL("libc.so.6")
 
 
def test_time():
    """Otestování nativní funkce time()."""
    t = libc.time(None)
    # velmi nepřesný odhad počtu sekund pro 2000-01-01
    t2020 = (2020-1970)*365*24*60*60
    assert t > t2020, "Neočekávaná hodnota {}".format(t)
 
 
def test_abs():
    """Otestování nativní funkce abs()."""
    x = libc.abs(-1)
    assert x == 1, "Neočekávaná hodnota {}".format(x)
 
    x = libc.abs(1)
    assert x == 1, "Neočekávaná hodnota {}".format(x)
 
 
def test_strlen():
    """Otestování nativní funkce strlen()."""
    x = libc.strlen("Hello")
    assert x == 5, "Neočekávaná hodnota {}".format(x)

Vidíme, že test kontroluje, jestli je délka řetězce „Hello“ skutečně rovna pěti znakům. Ovšem při pokusu o spuštění testů dojde k chybě:

$ pytest -v
 
============================= test session starts ==============================
platform linux -- Python 3.6.6, pytest-5.4.2, py-1.5.2, pluggy-0.13.1 -- /usr/bin/python3
cachedir: .pytest_cache
rootdir: /home/ptisnovs/src/python/testing-in-python/native_libs/project6
plugins: print-0.1.3, voluptuous-1.0.2
collecting ... collected 3 items
 
test_stdlib.py::test_time PASSED                                         [ 33%]
test_stdlib.py::test_abs PASSED                                          [ 66%]
test_stdlib.py::test_strlen FAILED                                       [100%]
 
=================================== FAILURES ===================================
_________________________________ test_strlen __________________________________
 
    def test_strlen():
        """Otestování nativní funkce strlen()."""
        x = libc.strlen("Hello")
>       assert x == 5, "Neočekávaná hodnota {}".format(x)
E       AssertionError: Neočekávaná hodnota 1
E       assert 1 == 5
E         +1
E         -5
 
test_stdlib.py:35: AssertionError
=========================== short test summary info ============================
FAILED test_stdlib.py::test_strlen - AssertionError: Neočekávaná hodnota 1
========================= 1 failed, 2 passed in 0.03s ==========================

Proč tomu tak je? V Pythonu 3 se používají Unicode řetězce, které jsou do céčkových funkcí přenášeny přes ukazatel na wchar_t. My však pracujeme s klasickými ASCIIZ řetězci céčka, musíme tedy použít pole bajtů (opět – platí pro Python 3):

"""Implementace jednotkových testů."""
 
import pytest
import ctypes
 
libc = None
 
 
def setup_module(module):
    """Zavoláno při inicializaci modulu s testem."""
    global libc
    libc = ctypes.CDLL("libc.so.6")
 
 
def test_time():
    """Otestování nativní funkce time()."""
    t = libc.time(None)
    # velmi nepřesný odhad počtu sekund pro 2000-01-01
    t2020 = (2020-1970)*365*24*60*60
    assert t > t2020, "Neočekávaná hodnota {}".format(t)
 
 
def test_abs():
    """Otestování nativní funkce abs()."""
    x = libc.abs(-1)
    assert x == 1, "Neočekávaná hodnota {}".format(x)
 
    x = libc.abs(1)
    assert x == 1, "Neočekávaná hodnota {}".format(x)
 
 
def test_strlen():
    """Otestování nativní funkce strlen()."""
    x = libc.strlen(b"Hello")
    assert x == 5, "Neočekávaná hodnota {}".format(x)

Nyní již jednotkové testy projdou bez problémů:

$ pytest -v
 
============================= test session starts ==============================
platform linux -- Python 3.6.6, pytest-5.4.2, py-1.5.2, pluggy-0.13.1 -- /usr/bin/python3
cachedir: .pytest_cache
rootdir: /home/ptisnovs/src/python/testing-in-python/native_libs/project7
plugins: print-0.1.3, voluptuous-1.0.2
collecting ... collected 3 items
 
test_stdlib.py::test_time PASSED                                         [ 33%]
test_stdlib.py::test_abs PASSED                                          [ 66%]
test_stdlib.py::test_strlen PASSED                                       [100%]
 
============================== 3 passed in 0.01s ===============================
Poznámka: jedná se o velmi častou chybu, na kterou se mnohdy přijde až dosti pozdě.

12. Práce s datovými strukturami

Ve druhé části dnešního článku si ukážeme způsob předávání datových struktur mezi jazykem C a Pythonem. Připomeňme si, že v C představují struktury základní (a do značné míry jedinou) technologii určenou pro tvorbu uživatelsky definovaných heterogenních datových struktur (naproti tomu pole jsou struktury homogenní). Oproti primitivním datovým typům je předávání datových struktur poněkud složitější, a to zejména na straně Pythonu, protože je nutné explicitně specifikovat typy prvků a samozřejmě i jejich pořadí. Další problém, který je někdy nutné řešit, představuje předávání struktur odkazem, tj. s využitím ukazatelů. I s touto problematikou se postupně seznámíme, ovšem až v navazujícím článku.

Ukažme si nyní velmi jednoduchou aplikaci, v níž bude používána datová struktura reprezentující komplexní číslo a v C bude navíc implementována funkce pro součet dvou komplexních čísel.

Část aplikace psaná v programovacím jazyku C bude nejprve obsahovat deklaraci struktury nazvané Complex:

typedef struct {
    float real;
    float imag;
} Complex;

Ve stejném zdrojovém kódu je taktéž uvedena funkce určená pro součet dvou komplexních čísel. Jedná se o skutečnou funkci, která nijak nemění své parametry, ale vytváří nové komplexní číslo (to ovšem nemusí být příliš efektivní, například při práci s vektory či maticemi komplexních čísel):

extern Complex add(Complex x, Complex y)
{
    Complex result;
    result.real = x.real + y.real;
    result.imag = x.imag + y.imag;
    return result;
}

Skript naprogramovaný v Pythonu, který bude volat céčkovou funkci pro součet komplexních čísel, je již poměrně složitý. Nejdříve si uveďme je úplnou podobu a potom se zaměříme na popis jednotlivých částí:

"""Základní použití balíčku ctypes, modul pro import."""
 
import ctypes
 
 
library = None
 
 
def load_library(library_name):
    """Načtení nativní knihovny."""
    return ctypes.CDLL(library_name)
 
 
def add(x, y):
    """Zavolání externí funkce."""
    return library.add(x, y)
 
 
def init():
    global library
    library = load_library("libadder.so")
 
    library.add.argtypes = (Complex, Complex)
    library.add.restype = Complex
 
 
class Complex(ctypes.Structure):
    _fields_ = [("real", ctypes.c_float), ("imag", ctypes.c_float)]
 
    def __str__(self):
        return "Complex: %f + i%f" % (self.real, self.imag)
 
 
def main():
    """Otestování, jestli je možné zavolat nativní funkci."""
    init()
    print(library)
 
    c1 = Complex(1.0, 2.0)
    c2 = Complex(3.0, 4.0)
 
    c3 = add(c1, c2)
 
    print(c1)
    print(c2)
    print(c3)
 
 
if __name__ == '__main__':
    # pouze se ujistíme, že lze zavolat nativní funkci
    main()

Na začátku pouze naimportujeme funkce a typy z modulu ctypes:

import ctypes

Následně se pokusíme načíst novou dynamickou knihovnu s deklarací struktury Complex i s funkcí add(). Samozřejmě zde můžete odstranit cestu ke knihovně a použít přístup s proměnnou prostředí LD_LIBRARY_PATH:

library = load_library("libadder.so")

Další část je poměrně záludná, ale důležitá, a to konkrétní určení typů parametrů funkce add() a taktéž přesného návratového typu této funkce::

library.add.argtypes = (Complex, Complex)
library.add.restype = Complex

Následuje poměrně složitá část skriptu, v níž (znovu) deklarujeme datovou strukturu Complex, tentokrát ovšem takovým způsobem, aby pořadí a typy atributů (fields) přesně odpovídaly rustovské deklaraci. Všimněte si, jak se atributy popisují – uvádí se jejich jméno a datový typ (což je důležité, aby interpret Pythonu mohl strukturu vytvořit tak, aby byla binárním obrazem céčkovské či céčkové struktury). Navíc si – zcela nezávisle na původní struktuře – můžeme přidat metody, například metodu __str__, která nám umožní nechat si vypsat obsah komplexního číslo (tedy reálné a imaginární složky).

class Complex(ctypes.Structure):
    _fields_ = [("real", ctypes.c_float), ("imag", ctypes.c_float)]
 
    def __str__(self):
        return "Complex: %f + i%f" % (self.real, self.imag)

Nyní se již můžeme začít chovat ke třídě Complex i k funkci add() běžným způsobem – následující kód již neobsahuje žádné speciality a při pohledu na něj ani nelze říct, že by třída Complex či funkce add_complex() byla něčím výjimečná:

c1 = Complex(1.0, 2.0)
c2 = Complex(3.0, 4.0)
 
c3 = library.add(c1, c2)
 
print(c1)
print(c2)
print(c3)
Complex: 1.000000 + i2.000000
Complex: 3.000000 + i4.000000
Complex: 4.000000 + i6.000000

Korektní chování si ověříme i jednotkovými testy:

"""Implementace jednotkových testů."""
 
import pytest
 
from adder import Complex, init, add
 
 
def test_add_basic():
    """Otestování výpočtu součtu dvou komplexních čísel."""
    init()
 
    c1 = Complex(1.0, 2.0)
    c2 = Complex(3.0, 4.0)
 
    result = add(c1, c2)
    expected = Complex(4.0, 6.0)
    assert result.real == expected.real and result.imag == expected.imag, \
        "Očekávaná hodnota {}, vráceno {}".format(expected, result)

S výsledkem – ok:

============================= test session starts ==============================
platform linux -- Python 3.6.6, pytest-5.4.2, py-1.5.2, pluggy-0.13.1 -- /usr/bin/python3
cachedir: .pytest_cache
rootdir: /home/ptisnovs/src/python/testing-in-python/native_libs/project8
plugins: print-0.1.3, voluptuous-1.0.2
collecting ... collected 1 item
 
test_add.py::test_add_basic PASSED                                       [100%]
 
============================== 1 passed in 0.01s ===============================

13. Porovnání komplexních čísel

Nyní zdrojový kód pro nativní knihovnu rozšíříme o funkci sloužící k porovnání dvou komplexních čísel. Připomeňme si, že v céčku jsou pravdivostní hodnoty představovány typem int:

typedef struct {
    float real;
    float imag;
} Complex;
 
extern Complex add(Complex x, Complex y)
{
    Complex result;
    result.real = x.real + y.real;
    result.imag = x.imag + y.imag;
    return result;
}
 
extern int equal(Complex x, Complex y)
{
    return x.real == y.real && x.imag == y.imag;
}

Příslušnou funkci „obalíme“ stejně pojmenovanou funkcí v Pythonu – opět se bude jmenovat equal:

"""Základní použití balíčku ctypes, modul pro import."""
 
import ctypes
 
 
library = None
 
 
def load_library(library_name):
    """Načtení nativní knihovny."""
    return ctypes.CDLL(library_name)
 
 
def add(x, y):
    """Zavolání externí funkce."""
    return library.add(x, y)
 
 
def equal(x, y):
    """Zavolání externí funkce."""
    return library.equal(x, y)
 
 
def init():
    global library
    library = load_library("libadder.so")
 
    library.add.argtypes = (Complex, Complex)
    library.add.restype = Complex
 
    library.equal.argtypes = (Complex, Complex)
    library.equal.restype = bool
 
 
class Complex(ctypes.Structure):
    _fields_ = [("real", ctypes.c_float), ("imag", ctypes.c_float)]
 
    def __str__(self):
        return "Complex: %f + i%f" % (self.real, self.imag)
 
 
def main():
    """Otestování, jestli je možné zavolat nativní funkci."""
    init()
    print(library)
 
    c1 = Complex(1.0, 2.0)
    c2 = Complex(3.0, 4.0)
 
    c3 = add(c1, c2)
 
    print(c1)
    print(c2)
    print(c3)
 
    print("c1==c2?", equal(c1, c2))
    print("c2==c2?", equal(c2, c2))
 
 
if __name__ == '__main__':
    # pouze se ujistíme, že lze zavolat nativní funkci
    main()

Jednotkové testy se příslušným způsobem zjednoduší – budeme moci zavolat funkci equal a předat jí dvojici komplexních čísel. Využívá se zde faktu, že i v Pythonu lze pro reprezentaci nepravdy použít nulu a pravdy libovolnou jinou celočíselnou hodnotu:

"""Implementace jednotkových testů."""
 
import pytest
 
from adder import Complex, init, add, equal
 
 
def test_add_basic():
    """Otestování výpočtu součtu dvou komplexních čísel."""
    init()
 
    c1 = Complex(1.0, 2.0)
    c2 = Complex(3.0, 4.0)
 
    result = add(c1, c2)
    expected = Complex(4.0, 6.0)
    assert equal(result, expected), \
        "Očekávaná hodnota {}, vráceno {}".format(expected, result)

S výsledkem – znovu ok:

============================= test session starts ==============================
platform linux -- Python 3.6.6, pytest-5.4.2, py-1.5.2, pluggy-0.13.1 -- /usr/bin/python3
cachedir: .pytest_cache
rootdir: /home/ptisnovs/src/python/testing-in-python/native_libs/project8
plugins: print-0.1.3, voluptuous-1.0.2
collecting ... collected 1 item
 
test_add.py::test_add_basic PASSED                                       [100%]
 
============================== 1 passed in 0.01s ===============================

14. Rozšíření knihovny pro práci s komplexními čísly

Vzhledem k tomu, že v Pythonu je struktura reprezentující komplexní číslo implementována třídou, nic nám nebrání vytvořit si (tedy přetížit) vlastní verzi operátoru porovnávání (==):

class Complex(ctypes.Structure):
    _fields_ = [("real", ctypes.c_float), ("imag", ctypes.c_float)]
 
    def __str__(self):
        return "Complex: %f + i%f" % (self.real, self.imag)
 
    def __eq__(self, other):
        return equal(self, other)

Výsledná podoba skriptu:

"""Základní použití balíčku ctypes, modul pro import."""
 
import ctypes
 
 
library = None
 
 
def load_library(library_name):
    """Načtení nativní knihovny."""
    return ctypes.CDLL(library_name)
 
 
def add(x, y):
    """Zavolání externí funkce."""
    return library.add(x, y)
 
 
def equal(x, y):
    """Zavolání externí funkce."""
    return library.equal(x, y)
 
 
def init():
    global library
    library = load_library("libadder.so")
 
    library.add.argtypes = (Complex, Complex)
    library.add.restype = Complex
 
    library.equal.argtypes = (Complex, Complex)
    library.equal.restype = bool
 
 
class Complex(ctypes.Structure):
    _fields_ = [("real", ctypes.c_float), ("imag", ctypes.c_float)]
 
    def __str__(self):
        return "Complex: %f + i%f" % (self.real, self.imag)
 
    def __eq__(self, other):
        return equal(self, other)
 
 
def main():
    """Otestování, jestli je možné zavolat nativní funkci."""
    init()
    print(library)
 
    c1 = Complex(1.0, 2.0)
    c2 = Complex(3.0, 4.0)
 
    c3 = add(c1, c2)
 
    print(c1)
    print(c2)
    print(c3)
 
    print("c1==c2?", equal(c1, c2))
    print("c2==c2?", equal(c2, c2))
 
 
if __name__ == '__main__':
    # pouze se ujistíme, že lze zavolat nativní funkci
    main()

Jeho spušení ověří základní chování přetíženého operátoru:

<CDLL 'libadder.so', handle 55fb1a6a4760 at 0x7fe480018390>
Complex: 1.000000 + i2.000000
Complex: 3.000000 + i4.000000
Complex: 4.000000 + i6.000000
c1==c2? False
c2==c2? True

Dosti zásadním způsobem se zjednoduší jednotkové testy, v nichž bude v konstrukci assert možné porovnat dvě komplexní čísla pomocí operátoru ==. Toto je finální podoba jednotkových testů:

"""Implementace jednotkových testů."""
 
import pytest
 
from adder import Complex, init, add, equal
 
 
def test_add_basic():
    """Otestování výpočtu součtu dvou komplexních čísel."""
    init()
 
    c1 = Complex(1.0, 2.0)
    c2 = Complex(3.0, 4.0)
 
    result = add(c1, c2)
    expected = Complex(4.0, 6.0)
    assert result == expected, \
        "Očekávaná hodnota {}, vráceno {}".format(expected, result)
 
 
def test_add_failure():
    """Otestování výpočtu součtu dvou komplexních čísel."""
    init()
 
    c1 = Complex(1.0, 2.0)
    c2 = Complex(0.0, 0.0)
 
    result = add(c1, c2)
    expected = Complex(4.0, 6.0)
    assert result == expected, \
        "Očekávaná hodnota {}, vráceno {}".format(expected, result)

Do skriptu byl naschvál vložen test, který po svém spuštění detekuje chybu, aby bylo patrné, že i konstrukce asssert dokáže díky existenci metod __eq__ a __str__ produkovat velmi čitelné výsledky:

============================= test session starts ==============================
platform linux -- Python 3.6.6, pytest-5.4.2, py-1.5.2, pluggy-0.13.1 -- /usr/bin/python3
cachedir: .pytest_cache
rootdir: /home/ptisnovs/src/python/testing-in-python/native_libs/projectA
plugins: print-0.1.3, voluptuous-1.0.2
collecting ... collected 2 items
 
test_add.py::test_add_basic PASSED                                       [ 50%]
test_add.py::test_add_failure FAILED                                     [100%]
 
=================================== FAILURES ===================================
_______________________________ test_add_failure _______________________________
 
    def test_add_failure():
        """Otestování výpočtu součtu dvou komplexních čísel."""
        init()
 
        c1 = Complex(1.0, 2.0)
        c2 = Complex(0.0, 0.0)
 
        result = add(c1, c2)
        expected = Complex(4.0, 6.0)
>       assert result == expected, \
            "Očekávaná hodnota {}, vráceno {}".format(expected, result)
E       AssertionError: Očekávaná hodnota Complex: 4.000000 + i6.000000, vráceno Complex: 1.000000 + i2.000000
E       assert <adder.Comple...x7f05abf32c80> == <adder.Comple...x7f05abf32bf8>
E         +<adder.Complex object at 0x7f05abf32c80>
E         -<adder.Complex object at 0x7f05abf32bf8>
 
test_add.py:30: AssertionError
=========================== short test summary info ============================
FAILED test_add.py::test_add_failure - AssertionError: Očekávaná hodnota Comp...
========================= 1 failed, 1 passed in 0.04s ==========================

15. Nativní knihovny a behavior-driven testing založený na použití knihovny Behave

S BDD testy jsme se již ve stručnosti seznámili v úvodním článku seriálu o testování s využitím Pythonu. Připomeňme si, že i když je BDD poměrně široký pojem, mnohdy si pod ním představujeme využití doménově specifického jazyka nazvaného Gherkin. Tento jazyk je díky existenci mnoha rozhraní integrovatelný do většiny mainstreamových programovacích jazyků, pochopitelně včetně Pythonu. Dnes si ve stručnosti (znovu) představíme knihovnu Behave, s jejíž pomocí se Gherkin integruje právě 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 (podobně je řešen i jazyk YAML). A protože se zabýváme nativními knihovnami a jejich testováním, budou (dva) projekty, které si představíme, testovat právě nativní céčkovou knihovnu (obsahující funkci pro součet dvou čísel).

Poznámka: s kombinací Python+Behave jsme se již na stránkách Rootu dříve setkali, takže tuto kapitolu i obě kapitoly následující je vhodné chápat spíše jako doplňující materiál.

Obrázek 1: Ukázka scénářů napsaných v doménově specifickém jazyce Gherkin.

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
│   ├── smoketest.feature
│   ├── basic.feature
│   ├── advanced.feature
│   ├── ...
│   ├── ...
│   ├── ...
│   ├── environment.py
│   └── steps
│       ├── common.py
│       ├── ...
│       ├── ...
│       └── ...
├── requirements.in
├── requirements.txt
├── run_tests.sh
└── src
    └── application.py
 
3 directories, 7 files

V projektu můžeme vidět několik typů souborů:

# Soubor Popis
1 src/application.py vlastní modul, který budeme chtít otestovat
2 requirements.in/requirements.txt soubory pro pip (instalátor balíčků jazyka Python)
3 feature_list.txt seznam testovacích scénářů, které se mají spustit
4 features/* adresář obsahující testovací scénáře i implementaci jednotlivých kroků testů
5 features/*.feature jednotlivé testovací scénáře
6 features/environment.py projekt s definicí funkcí vyvolaných před spuštěním testů, před jednotlivými kroky atd.
7 features/steps/ implementace jednotlivých testovacích kroků
8 run_tests.sh pomocný skript pro spuštění testovacích scénářů

16. 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 běžných projektech založených na frameworku Behave, 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

Následně je nutné připravit testovací scénář, konkrétně soubor smoketest.feature. Scénář připravíme 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ů (a jak jsme mohli vidět v první části článku, tato situace běžně nastane). V případě, že 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|

Samozřejmě budeme muset připravit 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):
    """Načtení a inicializace nativní knihovny."""
    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 nachází v souboru nazvaném common.py (ovšem může být pojmenovaný i jinak – import všech souborů v daném adresáři se provádí automaticky). Povšimněte si provedených změn, především toho, že v klauzuli 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)

V případě, že nativní funkce byla 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 2: Výsledek běhu testovacích scénářů ve chvíli, kdy se nativní funkce chová podle očekávání.

V případě, že jsme vynechali krok překladu nativní funkce, popř. se nepodařilo nativní knihovnu vůbec načíst (nebo najít), 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 3: Výsledek běhu testovacího scénáře ve chvíli, kdy se nepodařilo načíst nativní funkci.

17. 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|

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
Poznámka: tímto způsobem je vlastně možné po nepatrné úpravě scénáře testovat libovolnou (nativní) funkci.

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í:

MIF20

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

Zdrojové kódy všech dnes použitých demonstračních příkladů byly uloženy do nového Git repositáře, který je dostupný na adrese https://github.com/tisnik/testing-in-python. V případě, že nebudete chtít klonovat celý repositář (ten je ovšem – alespoň prozatím – velmi malý, dnes má přibližně několik desítek kilobajtů), můžete namísto toho použít odkazy na jednotlivé demonstrační příklady a jejich části, které naleznete v následující tabulce:

# Zdrojový soubor Stručný popis Cesta
1 project1/adder.c implementace nativní funkce add https://github.com/tisnik/testing-in-python/tree/master/native_lib­s/project1/adder.c
2 project1/call_from_python.py zavolání nativní funkce z Pythonu https://github.com/tisnik/testing-in-python/tree/master/native_lib­s/project1/call_from_python­.py
3 project1/make_library.sh skript pro překlad nativní knihovny https://github.com/tisnik/testing-in-python/tree/master/native_lib­s/project1/make_library.sh
4 project1/run.sh skript pro spuštění příkladu https://github.com/tisnik/testing-in-python/tree/master/native_lib­s/project1/run.sh
5 project1/clean.sh skript pro vyčištění přeložené knihovny i objektového souboru https://github.com/tisnik/testing-in-python/tree/master/native_lib­s/project1/clean.sh
       
6 project2/adder.c implementace nativní funkce add https://github.com/tisnik/testing-in-python/tree/master/native_lib­s/project2/adder.c
7 project2/adder.py obalení nativní funkce Pythonovským kódem https://github.com/tisnik/testing-in-python/tree/master/native_lib­s/project2/adder.py
8 project2/call_from_python.py zavolání nativní funkce z Pythonu https://github.com/tisnik/testing-in-python/tree/master/native_lib­s/project2/call_from_python­.py
9 project2/make_library.sh skript pro překlad nativní knihovny https://github.com/tisnik/testing-in-python/tree/master/native_lib­s/project2/make_library.sh
10 project2/run.sh skript pro spuštění příkladu https://github.com/tisnik/testing-in-python/tree/master/native_lib­s/project2/run.sh
11 project2/clean.sh skript pro vyčištění přeložené knihovny i objektového souboru https://github.com/tisnik/testing-in-python/tree/master/native_lib­s/project2/clean.sh
       
12 project3/adder.c implementace nativní funkce add https://github.com/tisnik/testing-in-python/tree/master/native_lib­s/project3/adder.c
13 project3/adder.py obalení nativní funkce Pythonovským kódem https://github.com/tisnik/testing-in-python/tree/master/native_lib­s/project3/adder.py
14 project3/test_add.py implementace jednotkového testu https://github.com/tisnik/testing-in-python/tree/master/native_lib­s/project3/test_add.py
15 project3/make_library.sh skript pro překlad nativní knihovny https://github.com/tisnik/testing-in-python/tree/master/native_lib­s/project3/make_library.sh
16 project3/test.sh skript pro spuštění jednotkových testů https://github.com/tisnik/testing-in-python/tree/master/native_lib­s/project3/test.sh
17 project3/clean.sh skript pro vyčištění přeložené knihovny i objektového souboru https://github.com/tisnik/testing-in-python/tree/master/native_lib­s/project3/clean.sh
       
18 project4/adder.c implementace nativní funkce add https://github.com/tisnik/testing-in-python/tree/master/native_lib­s/project4/adder.c
19 project4/adder.py obalení nativní funkce Pythonovským kódem https://github.com/tisnik/testing-in-python/tree/master/native_lib­s/project4/adder.py
20 project4/test_add.py implementace jednotkového testu https://github.com/tisnik/testing-in-python/tree/master/native_lib­s/project4/test_add.py
21 project4/make_library.sh skript pro překlad nativní knihovny https://github.com/tisnik/testing-in-python/tree/master/native_lib­s/project4/make_library.sh
22 project4/test.sh skript pro spuštění jednotkových testů https://github.com/tisnik/testing-in-python/tree/master/native_lib­s/project4/test.sh
23 project4/clean.sh skript pro vyčištění přeložené knihovny i objektového souboru https://github.com/tisnik/testing-in-python/tree/master/native_lib­s/project4/clean.sh
       
24 project5/test_stdlib.py otestování nativních funkcí time() a abs() https://github.com/tisnik/testing-in-python/tree/master/native_lib­s/project5/test_stdlib.py
       
25 project6/test_stdlib.py otestování nativní funkce strlen() pro Unicode string https://github.com/tisnik/testing-in-python/tree/master/native_lib­s/project6/test_stdlib.py
       
26 project7/test_stdlib.py otestování nativní funkce strlen() pro bajtové pole https://github.com/tisnik/testing-in-python/tree/master/native_lib­s/project7/test_stdlib.py
       
27 project8/adder.c implementace nativní funkce add pro komplexní čísla https://github.com/tisnik/testing-in-python/tree/master/native_lib­s/project8/adder.c
28 project8/adder.py obalení nativní funkce Pythonovským kódem https://github.com/tisnik/testing-in-python/tree/master/native_lib­s/project8/adder.py
29 project8/test_add.py implementace jednotkového testu https://github.com/tisnik/testing-in-python/tree/master/native_lib­s/project8/test_add.py
30 project8/make_library.sh skript pro překlad nativní knihovny https://github.com/tisnik/testing-in-python/tree/master/native_lib­s/project8/make_library.sh
31 project8/test.sh skript pro spuštění jednotkových testů https://github.com/tisnik/testing-in-python/tree/master/native_lib­s/project8/test.sh
32 project8/clean.sh skript pro vyčištění přeložené knihovny i objektového souboru https://github.com/tisnik/testing-in-python/tree/master/native_lib­s/project8/clean.sh
       
33 project9/adder.c implementace nativní funkce add pro komplexní čísla https://github.com/tisnik/testing-in-python/tree/master/native_lib­s/project9/adder.c
34 project9/adder.py obalení nativní funkce Pythonovským kódem https://github.com/tisnik/testing-in-python/tree/master/native_lib­s/project9/adder.py
35 project9/test_add.py implementace jednotkového testu https://github.com/tisnik/testing-in-python/tree/master/native_lib­s/project9/test_add.py
36 project9/make_library.sh skript pro překlad nativní knihovny https://github.com/tisnik/testing-in-python/tree/master/native_lib­s/project9/make_library.sh
37 project9/test.sh skript pro spuštění jednotkových testů https://github.com/tisnik/testing-in-python/tree/master/native_lib­s/project9/test.sh
38 project9/clean.sh skript pro vyčištění přeložené knihovny i objektového souboru https://github.com/tisnik/testing-in-python/tree/master/native_lib­s/project9/clean.sh
       
39 projectA/adder.c implementace nativní funkce add pro komplexní čísla https://github.com/tisnik/testing-in-python/tree/master/native_lib­s/projectA/adder.c
40 projectA/adder.py obalení nativní funkce Pythonovským kódem https://github.com/tisnik/testing-in-python/tree/master/native_lib­s/projectA/adder.py
41 projectA/test_add.py implementace jednotkového testu https://github.com/tisnik/testing-in-python/tree/master/native_lib­s/projectA/test_add.py
42 projectA/make_library.sh skript pro překlad nativní knihovny https://github.com/tisnik/testing-in-python/tree/master/native_lib­s/projectA/make_library.sh
43 projectA/test.sh skript pro spuštění jednotkových testů https://github.com/tisnik/testing-in-python/tree/master/native_lib­s/projectA/test.sh
44 projectA/clean.sh skript pro vyčištění přeložené knihovny i objektového souboru https://github.com/tisnik/testing-in-python/tree/master/native_lib­s/projectA/clean.sh
       
45 test_native_lib1 projekt založený na frameworku Behave https://github.com/tisnik/testing-in-python/tree/master/native_lib­s/test_native_lib1/
       
46 test_native_lib1 projekt založený na frameworku Behave https://github.com/tisnik/testing-in-python/tree/master/native_lib­s/test_native_lib1/

19. Předchozí články s tématem testování (nejenom) v Pythonu

Tématem testování jsme se již na stránkách Rootu několikrát zabývali. Jedná se mj. o následující články:

  1. Použití Pythonu pro tvorbu testů: od jednotkových testů až po testy UI
    https://www.root.cz/clanky/pouziti-pythonu-pro-tvorbu-testu-od-jednotkovych-testu-az-po-testy-ui/
  2. Použití Pythonu pro tvorbu testů: použití třídy Mock z knihovny unittest.mock
    https://www.root.cz/clanky/pouziti-pythonu-pro-tvorbu-testu-pouziti-tridy-mock-z-knihovny-unittest-mock/
  3. Použití nástroje pytest pro tvorbu jednotkových testů a benchmarků
    https://www.root.cz/clanky/pouziti-nastroje-pytest-pro-tvorbu-jednotkovych-testu-a-benchmarku/
  4. Nástroj pytest a jednotkové testy: fixtures, výjimky, parametrizace testů
    https://www.root.cz/clanky/nastroj-pytest-a-jednotkove-testy-fixtures-vyjimky-parametrizace-testu/
  5. Nástroj pytest a jednotkové testy: životní cyklus testů, užitečné tipy a triky
    https://www.root.cz/clanky/nastroj-pytest-a-jednotkove-testy-zivotni-cyklus-testu-uzitecne-tipy-a-triky/
  6. Struktura projektů s jednotkovými testy, využití Travis CI
    https://www.root.cz/clanky/struktura-projektu-s-jednotkovymi-testy-vyuziti-travis-ci/
  7. Omezení stavového prostoru testovaných funkcí a metod
    https://www.root.cz/clanky/omezeni-stavoveho-prostoru-testovanych-funkci-a-metod/
  8. Testování aplikací s využitím nástroje Hypothesis
    https://www.root.cz/clanky/testovani-aplikaci-s-vyuzitim-nastroje-hypothesis/
  9. Testování aplikací s využitím nástroje Hypothesis (dokončení)
    https://www.root.cz/clanky/testovani-aplikaci-s-vyuzitim-nastroje-hypothesis-dokonceni/
  10. Testování webových aplikací s REST API z Pythonu
    https://www.root.cz/clanky/testovani-webovych-aplikaci-s-rest-api-z-pythonu/
  11. Testování webových aplikací s REST API z Pythonu (2)
    https://www.root.cz/clanky/testovani-webovych-aplikaci-s-rest-api-z-pythonu-2/
  12. Testování webových aplikací s REST API z Pythonu (3)
    https://www.root.cz/clanky/testovani-webovych-aplikaci-s-rest-api-z-pythonu-3/
  13. Behavior-driven development v Pythonu s využitím knihovny Behave
    https://www.root.cz/clanky/behavior-driven-development-v-pythonu-s-vyuzitim-knihovny-behave/
  14. Behavior-driven development v Pythonu s využitím knihovny Behave (druhá část)
    https://www.root.cz/clanky/behavior-driven-development-v-pythonu-s-vyuzitim-knihovny-behave-druha-cast/
  15. Behavior-driven development v Pythonu s využitím knihovny Behave (závěrečná část)
    https://www.root.cz/clanky/behavior-driven-development-v-pythonu-s-vyuzitim-knihovny-behave-zaverecna-cast/
  16. Validace datových struktur v Pythonu pomocí knihoven Schemagic a Schema
    https://www.root.cz/clanky/validace-datovych-struktur-v-pythonu-pomoci-knihoven-schemagic-a-schema/
  17. Validace datových struktur v Pythonu (2. část)
    https://www.root.cz/clanky/validace-datovych-struktur-v-pythonu-2-cast/
  18. Validace datových struktur v Pythonu (dokončení)
    https://www.root.cz/clanky/validace-datovych-struktur-v-pythonu-dokonceni/
  19. Univerzální testovací nástroj Robot Framework
    https://www.root.cz/clanky/univerzalni-testovaci-nastroj-robot-framework/
  20. Univerzální testovací nástroj Robot Framework a BDD testy
    https://www.root.cz/clanky/univerzalni-testovaci-nastroj-robot-framework-a-bdd-testy/
  21. Úvod do problematiky fuzzingu a fuzz testování
    https://www.root.cz/clanky/uvod-do-problematiky-fuzzingu-a-fuzz-testovani/
  22. Úvod do problematiky fuzzingu a fuzz testování – složení vlastního fuzzeru
    https://www.root.cz/clanky/uvod-do-problematiky-fuzzingu-a-fuzz-testovani-slozeni-vlastniho-fuzzeru/
  23. Knihovny a moduly usnadňující testování aplikací naprogramovaných v jazyce Clojure
    https://www.root.cz/clanky/knihovny-a-moduly-usnadnujici-testovani-aplikaci-naprogramovanych-v-jazyce-clojure/
  24. Validace dat s využitím knihovny spec v Clojure 1.9.0
    https://www.root.cz/clanky/validace-dat-s-vyuzitim-knihovny-spec-v-clojure-1–9–0/
  25. Testování aplikací naprogramovaných v jazyce Go
    https://www.root.cz/clanky/testovani-aplikaci-naprogramovanych-v-jazyce-go/
  26. Knihovny určené pro tvorbu testů v programovacím jazyce Go
    https://www.root.cz/clanky/knihovny-urcene-pro-tvorbu-testu-v-programovacim-jazyce-go/
  27. Testování aplikací psaných v Go s využitím knihoven Goblin a Frisby
    https://www.root.cz/clanky/testovani-aplikaci-psanych-v-go-s-vyuzitim-knihoven-goblin-a-frisby/
  28. Testování Go aplikací s využitím knihovny GΩmega a frameworku Ginkgo
    https://www.root.cz/clanky/testovani-go-aplikaci-s-vyuzitim-knihovny-gomega-mega-a-frameworku-ginkgo/
  29. Tvorba BDD testů s využitím jazyka Go a nástroje godog
    https://www.root.cz/clanky/tvorba-bdd-testu-s-vyuzitim-jazyka-go-a-nastroje-godog/
  30. Použití Go pro automatizaci práce s aplikacemi s interaktivním příkazovým řádkem
    https://www.root.cz/clanky/pouziti-go-pro-automatizaci-prace-s-aplikacemi-s-interaktivnim-prikazovym-radkem/
  31. Použití Go pro automatizaci práce s aplikacemi s interaktivním příkazovým řádkem (dokončení)
    https://www.root.cz/clanky/pouziti-go-pro-automatizaci-prace-s-aplikacemi-s-interaktivnim-prikazovym-radkem-dokonceni/
  32. Použití jazyka Gherkin při tvorbě testovacích scénářů pro aplikace psané v Clojure
    https://www.root.cz/clanky/pouziti-jazyka-gherkin-pri-tvorbe-testovacich-scenaru-pro-aplikace-psane-v-nbsp-clojure/
  33. Použití jazyka Gherkin při tvorbě testovacích scénářů pro aplikace psané v Clojure (2)
    https://www.root.cz/clanky/pouziti-jazyka-gherkin-pri-tvorbe-testovacich-scenaru-pro-aplikace-psane-v-nbsp-clojure-2/

20. Odkazy na Internetu

  1. Dokumentace k CFFI
    https://cffi.readthedocs.i­o/en/latest/overview.html
  2. How can use CFFI to call an existing C function given the source code?
    https://stackoverflow.com/qu­estions/37936045/how-can-use-cffi-to-call-an-existing-c-function-given-the-source-code
  3. Python Bindings: Calling C or C++ From Python
    https://realpython.com/python-bindings-overview/
  4. Wrapping a C library in Python: C, Cython or ctypes?
    https://stackoverflow.com/qu­estions/1942298/wrapping-a-c-library-in-python-c-cython-or-ctypes
  5. Calling Rust From Python
    https://bheisler.github.i­o/post/calling-rust-in-python/
  6. Calling Rust in Python (komentáře k předchozímu článku)
    https://www.reddit.com/r/rus­t/comments/63iy5a/calling_rus­t_in_python/
  7. CFFI Documentation
    https://cffi.readthedocs.i­o/en/latest/
  8. Build Script Support
    http://doc.crates.io/build-script.html
  9. Creating a shared and static library with the gnu compiler [gcc]
    http://www.adp-gmbh.ch/cpp/gcc/create_lib.html
  10. ctypes — A foreign function library for Python
    https://docs.python.org/2/li­brary/ctypes.html
  11. FFI: Foreign Function Interface
    https://doc.rust-lang.org/book/ffi.html
  12. Primitive Type pointer
    https://doc.rust-lang.org/std/primitive.pointer.html
  13. Requests: HTTP for Humans (dokumentace)
    http://docs.python-requests.org/en/master/
  14. Requests: Introduction
    http://docs.python-requests.org/en/latest/user/intro/
  15. Requests na GitHubu
    https://github.com/requests/requests
  16. Requests (software) na Wikipedii
    https://en.wikipedia.org/wi­ki/Requests_%28software%29
  17. Pip (dokumentace)
    https://pip.pypa.io/en/stable/
  18. 20 Python libraries you can’t live without
    https://pythontips.com/2013/07/30/20-python-libraries-you-cant-live-without/
  19. What are the top 10 most useful and influential Python libraries and frameworks?
    https://www.quora.com/What-are-the-top-10-most-useful-and-influential-Python-libraries-and-frameworks
  20. Python: useful modules
    https://wiki.python.org/mo­in/UsefulModules
  21. Top 15 most popular Python libraries
    https://keyua.org/blog/most-popular-python-libraries/
  22. Hypertext Transfer Protocol
    https://en.wikipedia.org/wi­ki/Hypertext_Transfer_Pro­tocol
  23. List of HTTP header fields
    https://en.wikipedia.org/wi­ki/List_of_HTTP_header_fi­elds
  24. List of HTTP status codes
    https://en.wikipedia.org/wi­ki/List_of_HTTP_status_co­des
  25. Python requests deep dive
    https://medium.com/@antho­nypjshaw/python-requests-deep-dive-a0a5c5c1e093
  26. The awesome requests module
    https://www.pythonforbegin­ners.com/requests/the-awesome-requests-module
  27. Send HTTP Requests in Python
    https://code-maven.com/http-requests-in-python
  28. Introducing JSON
    http://json.org/
  29. Writing tests for RESTful APIs in Python using requests – part 1: basic tests
    https://www.ontestautomati­on.com/writing-tests-for-restful-apis-in-python-using-requests-part-1-basic-tests/
  30. Step by Step Rest API Testing using Python + Pytest + Allure
    https://www.udemy.com/course/api-testing-python/
  31. Prime formulas and polynomial functions
    https://en.wikipedia.org/wi­ki/Formula_for_primes#Pri­me_formulas_and_polynomial_fun­ctions
  32. Prime-Generating Polynomial
    https://mathworld.wolfram.com/Prime-GeneratingPolynomial.html
  33. Hoare logic
    https://en.wikipedia.org/wi­ki/Hoare_logic
  34. Goto Fail, Heartbleed, and Unit Testing Culture
    https://martinfowler.com/ar­ticles/testing-culture.html
  35. PEP-484
    https://www.python.org/dev/peps/pep-0484/
  36. In-depth: Functional programming in C++
    https://www.gamasutra.com/vi­ew/news/169296/Indepth_Fun­ctional_programming_in_C.php
  37. mypy
    http://www.mypy-lang.org/
  38. Welcome to Mypy documentation!
    https://mypy.readthedocs.i­o/en/latest/index.html
  39. mypy na GitHubu
    https://github.com/python/mypy
  40. mypy 0.770 na PyPi
    https://pypi.org/project/mypy/
  41. Extensions for mypy (separated out from mypy/extensions)
    https://github.com/python/my­py_extensions
  42. The Mypy Blog
    https://mypy-lang.blogspot.com/2020/03/mypy-0770-released.html
  43. Our journey to type checking 4 million lines of Python
    https://dropbox.tech/application/our-journey-to-type-checking-4-million-lines-of-python
  44. Type-Checking Python Programs With Type Hints and mypy
    https://www.youtube.com/wat­ch?v=2×WhaALHTvU
  45. Refactoring to Immutability – Kevlin Henney
    https://www.youtube.com/wat­ch?v=APUCMSPiNh4
  46. Bernat Gabor – Type hinting (and mypy) – PyCon 2019
    https://www.youtube.com/wat­ch?v=hTrjTAPnA_k
  47. Stanford Seminar – Optional Static Typing for Python
    https://www.youtube.com/wat­ch?v=GiZKuyLKvAA
  48. mypy Getting to Four Million Lines of Typed Python – Michael Sullivan
    https://www.youtube.com/wat­ch?v=FT_WHV4-QcU
  49. Shebang
    https://en.wikipedia.org/wi­ki/Shebang_(Unix)
  50. pytest 5.4.2 na PyPi
    https://pypi.org/project/pytest/
  51. Hillel Wayne – Beyond Unit Tests: Taking Your Testing to the Next Level – PyCon 2018
    https://www.youtube.com/wat­ch?v=MYucYon2-lk
  52. Awesome Python – testing
    https://github.com/vinta/awesome-python#testing
  53. pytest Plugins Compatibility
    http://plugincompat.herokuapp.com/
  54. Selenium (pro Python)
    https://pypi.org/project/selenium/
  55. Getting Started With Testing in Python
    https://realpython.com/python-testing/
  56. unittest.mock — mock object library
    https://docs.python.org/3­.5/library/unittest.mock.html
  57. mock 2.0.0
    https://pypi.python.org/pypi/mock
  58. An Introduction to Mocking in Python
    https://www.toptal.com/python/an-introduction-to-mocking-in-python
  59. Mock – Mocking and Testing Library
    http://mock.readthedocs.io/en/stable/
  60. Python Mocking 101: Fake It Before You Make It
    https://blog.fugue.co/2016–02–11-python-mocking-101.html
  61. Nauč se Python! – Testování
    http://naucse.python.cz/les­sons/intro/testing/
  62. Flexmock (dokumentace)
    https://flexmock.readthedoc­s.io/en/latest/
  63. Test Fixture (Wikipedia)
    https://en.wikipedia.org/wi­ki/Test_fixture
  64. Mock object (Wikipedia)
    https://en.wikipedia.org/wi­ki/Mock_object
  65. Extrémní programování
    https://cs.wikipedia.org/wi­ki/Extr%C3%A9mn%C3%AD_pro­gramov%C3%A1n%C3%AD
  66. Programování řízené testy
    https://cs.wikipedia.org/wi­ki/Programov%C3%A1n%C3%AD_%C5%99%­C3%ADzen%C3%A9_testy
  67. Pip (dokumentace)
    https://pip.pypa.io/en/stable/
  68. Tox
    https://tox.readthedocs.io/en/latest/
  69. pytest: helps you write better programs
    https://docs.pytest.org/en/latest/
  70. doctest — Test interactive Python examples
    https://docs.python.org/dev/li­brary/doctest.html#module-doctest
  71. unittest — Unit testing framework
    https://docs.python.org/dev/li­brary/unittest.html
  72. Python namespaces
    https://bytebaker.com/2008/07/30/pyt­hon-namespaces/
  73. Namespaces and Scopes
    https://www.python-course.eu/namespaces.php
  74. Stránka projektu Robot Framework
    https://robotframework.org/
  75. GitHub repositář Robot Frameworku
    https://github.com/robotfra­mework/robotframework
  76. Robot Framework (Wikipedia)
    https://en.wikipedia.org/wi­ki/Robot_Framework
  77. Tutoriál Robot Frameworku
    http://www.robotframeworktu­torial.com/
  78. Robot Framework Documentation
    https://robotframework.or­g/robotframework/
  79. Robot Framework Introduction
    https://blog.testproject.i­o/2016/11/22/robot-framework-introduction/
  80. robotframework 3.1.2 na PyPi
    https://pypi.org/project/ro­botframework/
  81. Robot Framework demo (GitHub)
    https://github.com/robotfra­mework/RobotDemo
  82. Robot Framework web testing demo using SeleniumLibrary
    https://github.com/robotfra­mework/WebDemo
  83. Robot Framework for Mobile Test Automation Demo
    https://www.youtube.com/wat­ch?v=06LsU08slP8
  84. Gherkin
    https://cucumber.io/docs/gherkin/
  85. Selenium
    https://selenium.dev/
  86. SeleniumLibrary
    https://robotframework.org/
  87. The Practical Test Pyramid
    https://martinfowler.com/ar­ticles/practical-test-pyramid.html
  88. Acceptance Tests and the Testing Pyramid
    http://www.blog.acceptance­testdrivendevelopment.com/ac­ceptance-tests-and-the-testing-pyramid/
  89. Tab-separated values
    https://en.wikipedia.org/wiki/Tab-separated_values
  90. A quick guide about Python implementations
    https://blog.rmotr.com/a-quick-guide-about-python-implementations-aa224109f321
  91. radamsa
    https://gitlab.com/akihe/radamsa
  92. Fuzzing (Wikipedia)
    https://en.wikipedia.org/wiki/Fuzzing
  93. american fuzzy lop
    http://lcamtuf.coredump.cx/afl/
  94. Fuzzing: the new unit testing
    https://go-talks.appspot.com/github.com/dvyukov/go-fuzz/slides/fuzzing.slide#1
  95. Corpus for github.com/dvyukov/go-fuzz examples
    https://github.com/dvyukov/go-fuzz-corpus
  96. AFL – QuickStartGuide.txt
    https://github.com/google/AF­L/blob/master/docs/QuickStar­tGuide.txt
  97. Introduction to Fuzzing in Python with AFL
    https://alexgaynor.net/2015/a­pr/13/introduction-to-fuzzing-in-python-with-afl/
  98. Writing a Simple Fuzzer in Python
    https://jmcph4.github.io/2018/01/19/wri­ting-a-simple-fuzzer-in-python/
  99. How to Fuzz Go Code with go-fuzz (Continuously)
    https://fuzzit.dev/2019/10/02/how-to-fuzz-go-code-with-go-fuzz-continuously/
  100. Golang Fuzzing: A go-fuzz Tutorial and Example
    http://networkbit.ch/golang-fuzzing/
  101. Fuzzing Python Modules
    https://stackoverflow.com/qu­estions/20749026/fuzzing-python-modules
  102. 0×3 Python Tutorial: Fuzzer
    http://www.primalsecurity.net/0×3-python-tutorial-fuzzer/
  103. fuzzing na PyPi
    https://pypi.org/project/fuzzing/
  104. Fuzzing 0.3.2 documentation
    https://fuzzing.readthedoc­s.io/en/latest/
  105. Randomized testing for Go
    https://github.com/dvyukov/go-fuzz
  106. HTTP/2 fuzzer written in Golang
    https://github.com/c0nrad/http2fuzz
  107. Ffuf (Fuzz Faster U Fool) – An Open Source Fast Web Fuzzing Tool
    https://hacknews.co/hacking-tools/20191208/ffuf-fuzz-faster-u-fool-an-open-source-fast-web-fuzzing-tool.html
  108. Continuous Fuzzing Made Simple
    https://fuzzit.dev/
  109. Halt and Catch Fire
    https://en.wikipedia.org/wi­ki/Halt_and_Catch_Fire#In­tel_x86
  110. Random testing
    https://en.wikipedia.org/wi­ki/Random_testing
  111. Monkey testing
    https://en.wikipedia.org/wi­ki/Monkey_testing
  112. Fuzzing for Software Security Testing and Quality Assurance, Second Edition
    https://books.google.at/bo­oks?id=tKN5DwAAQBAJ&pg=PR15&lpg=PR15&q=%­22I+settled+on+the+term+fuz­z%22&redir_esc=y&hl=de#v=o­nepage&q=%22I%20settled%20on%20the%20ter­m%20fuzz%22&f=false
  113. libFuzzer – a library for coverage-guided fuzz testing
    https://llvm.org/docs/LibFuzzer.html
  114. fuzzy-swagger na PyPi
    https://pypi.org/project/fuzzy-swagger/
  115. fuzzy-swagger na GitHubu
    https://github.com/namuan/fuzzy-swagger
  116. Fuzz testing tools for Python
    https://wiki.python.org/mo­in/PythonTestingToolsTaxo­nomy#Fuzz_Testing_Tools
  117. A curated list of awesome Go frameworks, libraries and software
    https://github.com/avelino/awesome-go
  118. gofuzz: a library for populating go objects with random values
    https://github.com/google/gofuzz
  119. tavor: A generic fuzzing and delta-debugging framework
    https://github.com/zimmski/tavor
  120. hypothesis na GitHubu
    https://github.com/Hypothe­sisWorks/hypothesis
  121. Hypothesis: Test faster, fix more
    https://hypothesis.works/
  122. Hypothesis
    https://hypothesis.works/ar­ticles/intro/
  123. What is Hypothesis?
    https://hypothesis.works/articles/what-is-hypothesis/
  124. What is Property Based Testing?
    https://hypothesis.works/articles/what-is-property-based-testing/
  125. Databáze CVE
    https://www.cvedetails.com/
  126. Fuzz test Python modules with libFuzzer
    https://github.com/eerimoq/pyfuzzer
  127. Taof – The art of fuzzing
    https://sourceforge.net/pro­jects/taof/
  128. JQF + Zest: Coverage-guided semantic fuzzing for Java
    https://github.com/rohanpadhye/jqf
  129. http2fuzz
    https://github.com/c0nrad/http2fuzz
  130. Demystifying hypothesis testing with simple Python examples
    https://towardsdatascience­.com/demystifying-hypothesis-testing-with-simple-python-examples-4997ad3c5294
  131. Testování
    http://voho.eu/wiki/testovani/
  132. Unit testing (Wikipedia.en)
    https://en.wikipedia.org/wi­ki/Unit_testing
  133. Unit testing (Wikipedia.cz)
    https://cs.wikipedia.org/wi­ki/Unit_testing
  134. Unit Test vs Integration Test
    https://www.youtube.com/wat­ch?v=0GypdsJulKE
  135. TestDouble
    https://martinfowler.com/bli­ki/TestDouble.html
  136. Test Double
    http://xunitpatterns.com/Tes­t%20Double.html
  137. Test-driven development (Wikipedia)
    https://en.wikipedia.org/wiki/Test-driven_development
  138. Acceptance test–driven development
    https://en.wikipedia.org/wi­ki/Acceptance_test%E2%80%93dri­ven_development
  139. Gauge
    https://gauge.org/
  140. Gauge (software)
    https://en.wikipedia.org/wi­ki/Gauge_(software)
  141. PYPL PopularitY of Programming Language
    https://pypl.github.io/PYPL.html
  142. Testing is Good. Pyramids are Bad. Ice Cream Cones are the Worst
    https://medium.com/@fistsOf­Reason/testing-is-good-pyramids-are-bad-ice-cream-cones-are-the-worst-ad94b9b2f05f
  143. Články a zprávičky věnující se Pythonu
    https://www.root.cz/n/python/
  144. PythonTestingToolsTaxonomy
    https://wiki.python.org/mo­in/PythonTestingToolsTaxo­nomy
  145. Top 6 BEST Python Testing Frameworks [Updated 2020 List]
    https://www.softwaretestin­ghelp.com/python-testing-frameworks/
  146. pytest-print 0.1.3
    https://pypi.org/project/pytest-print/
  147. pytest fixtures: explicit, modular, scalable
    https://docs.pytest.org/en/la­test/fixture.html
  148. PyTest Tutorial: What is, Install, Fixture, Assertions
    https://www.guru99.com/pytest-tutorial.html
  149. Pytest – Fixtures
    https://www.tutorialspoin­t.com/pytest/pytest_fixtu­res.htm
  150. Marking test functions with attributes
    https://docs.pytest.org/en/la­test/mark.html
  151. pytest-print
    https://pytest-print.readthedocs.io/en/latest/
  152. Continuous integration
    https://en.wikipedia.org/wi­ki/Continuous_integration
  153. Travis CI
    https://travis-ci.org/
  154. Mutation testing
    https://en.wikipedia.org/wi­ki/Mutation_testing
  155. Články o Hypothesis
    https://news.ycombinator.com/from?si­te=hypothesis.works
  156. Testovací případ
    https://cs.wikipedia.org/wi­ki/Testovac%C3%AD_p%C5%99%C3%AD­pad
  157. Most testing is ineffective
    https://hypothesis.works/