Obsah
1. Použití Pythonu pro tvorbu testů: použití třídy Mock z knihovny unittest.mock
2. Zjištění, zda byla mockovaná funkce zavolána
3. Problematika mockování funkce, která je volaná nepřímo
4. Mockování funkce volané nepřímo
5. Modifikace kódu v případě, že použijeme import a nikoli from X import
8. Nepřímé volání metody, kterou budeme mockovat
9. Mockování nepřímo volané metody
10. Složitější aplikace implementovaná ve větším množství modulů
11. Mockování funkcí volaných nepřímo z jiných modulů
13. Mockování funkce přímo volané z testů
15. Přímé použití konstruktoru patch() v těle testů
16. Další možnosti nabízené objekty Mock a MagicMock
17. Zjištění kolikrát a s jakými parametry byla mockovaná funkce zavolána
18. Repositář s demonstračními příklady
19. Předchozí články s tématem testování (nejenom) v Pythonu
1. Použití Pythonu pro tvorbu testů: použití třídy Mock z knihovny unittest.mock
Ve druhé části seriálu o testování s využitím programovacího jazyka Python si popíšeme některé další užitečné možnosti, které jsou programátorům nabízeny knihovnou unittest.mock. Zejména si ukážeme způsoby použití tříd nazvaných Mock a MagicMock (což je třída odvozená od třídy Mock), seznámíme se s možnostmi zjištění (otestování), zda jsou mockované funkce volány s očekávanými parametry a samozřejmě nezapomeneme ani na velmi důležitou „maličkost“ – jakým způsobem se mockování použije společně se standardním testovacím frameworkem pytest (protože právě při skutečném testování se mockování funkcí a metod provádí nejčastěji, i když oblasti použití jsou ve skutečnosti větší).
Připomeňme si, že v současnosti existuje relativně velké množství různých knihoven, které mockování funkcí a metod v Pythonu umožňují (není se ostatně čemu divit, protože samotná podstata jazyka Python tyto operace umožňuje implementovat relativně snadno). Z těchto knihoven jsme již minule jmenovali projekt Flexmock, který naleznete na adrese https://pypi.python.org/pypi/flexmock. V Pythonu 3.x se standardem v této oblasti stala knihovna nazvaná unittest.mock. V případě, že ještě z nějakého důvodu musíte používat Python 2.x, použijte namísto knihovny unittest.mock knihovnu nazvanou jednoduše mock. Tato knihovna nabízí prakticky stejné možnosti jako unittest.mock (je ostatně založena na stejném základním kódu, který pouze byl pro potřeby Pythonu 2.x upraven), ovšem lze ji použít jak v Pythonu 2.x, tak i v Pythonu 3.x, a to bez toho, abyste museli upravovat zdrojové kódy vašich testů (samozřejmě za předpokladu, že se v nich nevyskytují konstrukce, které nejsou v Pythonu 2.x podporovány).
2. Zjištění, zda byla mockovaná funkce zavolána
Mnohdy, zejména při testování složitěji strukturovaného programového kódu s mnoha podmínkami, popř. rozsáhlejším stavovým prostorem aplikace, je důležité zjistit, zda se vůbec mockovaná funkce při spuštění jednotkových testů zavolala. A právě v těchto případech nám přijde vhod objekt, který je do testovací funkce automaticky předáván, v našem případě v prvním parametru (jméno tohoto parametru si můžeme vybrat sami, důležité je pouze znát jeho pořadí/index):
@patch('application.function1', side_effect=side_effect_handler) def test3(mocked_function_object): ... ... ...
Tento objekt obsahuje mj. i vlastnost (property) pojmenovanou jednoduše called. Ve výchozím stavu je tato vlastnost nastavena na hodnotu False, ale po prvním zavolání mockované funkce se vlastnost nastaví na hodnotu True. Ostatně se o tomto chování můžeme rychle přesvědčit, a to velmi snadno – vypíšeme hodnotu vlastnosti called před vlastním voláním mockované funkce a taktéž ihned po tomto volání. Upravený jednotkový test bude vypadat následovně:
def side_effect_handler(): print("side_effect function called") return -1 @patch('application.function1', side_effect=side_effect_handler) def test3(mocked_function_object): print("mocked function called: {c}".format(c=mocked_function_object.called)) print(application.function1()) print("mocked function called: {c}".format(c=mocked_function_object.called))
Po zavolání testovací funkce test3 by se na standardní výstup měla vypsat následující sekvence zpráv:
mocked function called: False side_effect function called -1 mocked function called: True
Podobně tomu bude v případě, že kombinujeme vlastní handler se specifikací návratové hodnoty (handler je tedy zavolán, jeho volání je korektně zaregistrováno, ovšem nakonec se použije programátorem specifikovaná návratová hodnota):
def side_effect_handler_2(): print("side_effect function called") return DEFAULT @patch('application.function1', return_value=42, side_effect=side_effect_handler_2) def test5(mocked_function): print("mocked function called: {c}".format(c=mocked_function.called)) print(application.function1()) print("mocked function called: {c}".format(c=mocked_function.called))
Výsledky po spuštění:
mocked function called: False side_effect function called 42 mocked function called: True
Pro úplnost si v této kapitole opět ukážeme úplný zdrojový kód výše popsaného demonstračního příkladu. V případě potřeby ho naleznete na adrese https://github.com/tisnik/testing-in-python/tree/master/unittest_mock/mock-test3:
Soubor application.py s testovanou funkcí
"""Implementace logiky aplikace, kterou budeme testovat.""" def function1(): """Funkce, kterou v testech nahradíme mockem.""" print("function1 called") return "tested function"
Soubor test.py
"""Implementace jednotkových testů.""" from unittest.mock import * import application def test1(): """První test neprovádí prakticky žádné reálné kontroly, jen zavolá testovanou funkci.""" print(application.function1()) @patch('application.function1', return_value=42) def test2(mocked_function): """Druhý test používá fake test double - náhradu volané funkce.""" print(application.function1()) def side_effect_handler(): """Implementace handleru - stub funkce nahrazované mockem.""" print("side_effect function called") return -1 @patch('application.function1', side_effect=side_effect_handler) def test3(mocked_function): """Třetí test používá stub test double - náhradu volané funkce.""" # vytiskneme informaci o tom, zda se mockovaná funkce zavolala print("mocked function called: {c}".format(c=mocked_function.called)) print(application.function1()) # opět vytiskneme informaci o tom, zda se mockovaná funkce zavolala print("mocked function called: {c}".format(c=mocked_function.called)) @patch('application.function1', return_value=42, side_effect=side_effect_handler) def test4(mocked_function): """Čtvrtý test se snaží zkombinovat fake a stub.""" print(application.function1()) def side_effect_handler_2(): """Implementace handleru - stub funkce nahrazované mockem, který ovšem ovlivňuje chování testu.""" print("side_effect function called") return DEFAULT @patch('application.function1', return_value=42, side_effect=side_effect_handler_2) def test5(mocked_function): """Pátý test se opět snaží zkombinovat fake a stub.""" # vytiskneme informaci o tom, zda se mockovaná funkce zavolala print("mocked function called: {c}".format(c=mocked_function.called)) print(application.function1()) # opět vytiskneme informaci o tom, zda se mockovaná funkce zavolala print("mocked function called: {c}".format(c=mocked_function.called)) if __name__ == '__main__': print("*** test1 ***") test1() print() print("*** test2 ***") test2() print() print("*** test3 ***") test3() print() print("*** test4 ***") test4() print() print("*** test5 ***") test5() print()
3. Problematika mockování funkce, která je volaná nepřímo
Nyní se dostáváme k velmi důležité vlastnosti knihovny unittest.mock, kterou je užitečné správně pochopit (už jen z toho důvodu, že dokumentace tuto vlastnost podle mého názoru nepopisuje do všech podrobností, o čemž ostatně svědčí poměrně časté dotazy na fórech). Týká se to způsobu určení plného jména funkce, která má být nahrazena svojí „falešnou“ variantou, tedy mockem. Nejdříve si naschvál zkomplikujeme kód naší aplikace, kterou budeme testovat. V upravené variantě budou aplikaci tvořit dvě funkce nazvané function1 a function2, přičemž si povšimněte, že se z funkce function1 volá funkce function2 (konkrétně v rámci příkazu return, ale to v tomto konkrétním případě není tak důležité – podstatný je fakt, že k volání dojde):
"""Implementace logiky aplikace, kterou budeme testovat.""" def function1(): """První funkce, která volá funkci druhou.""" print("function1 called") return function2() def function2(): """Druhá funkce, kterou v testech nahradíme mockem.""" print("function2 called") return "function 2"
4. Mockování funkce volané nepřímo
Komplikace nastanou ve chvíli, kdy budeme potřebovat nahradit funkci function2 za její falešnou variantu, tedy za mock. Tuto funkci totiž nebudeme volat přímo z testu, ale pouze nepřímo – přes vlastní kód aplikace. A právě v tomto okamžiku se projevuje již výše zmíněná vlastnost – do anotace @patch je nutné uvést jméno funkce tak, jak ji vidí volající kód. Co to pro nás znamená? Funkce function2 je volána z funkce pojmenované function1 a přitom k tomuto volání dochází v modulu application. Plné jméno mockované funkce tedy bude v tomto konkrétním případě znít application.function2, což jen náhodou odpovídá stejnému jménu, jakoby se funkce volala přímo z testů (lze totiž zavolat i funkci z jiného balíčku atd.). Podívejme se nyní na příklady jednotkových testů.
Nejprve změníme import testovaného modulu takovým způsobem, aby nebylo nutné při volání funkcí z modulu application používat celé jméno tohoto modulu s tečkou:
from unittest.mock import * from application import *
Můžeme si otestovat, že se funkce z modulu application mohou volat přímo (jsou totiž naimportovány do aktuálního jmenného prostoru):
def test1(): print("function1 returns: {v}".format(v=function1()))
Výsledek je prozatím uspokojující:
function1 called function2 called function1 returns: function 2
Správné určení funkce, která se má mockovat, bude vypadat následovně – funkce je totiž volána nepřímo v modulu application a nikoli v modulu test (který je aktuálně nastaven), což se musí projevit v dekorátoru:
@patch('application.function2', return_value=42) def test2(mocked_function_object): print("mocked function called: {c}".format(c=mocked_function_object.called)) print("function1 returns: {v}".format(v=function1())) print("mocked function called: {c}".format(c=mocked_function_object.called))
Výsledek bude v tomto případě vypadat takto:
mocked function called: False function1 called function1 returns: 42 < zde se volá mock namísto "pravé" funkce function2 mocked function called: True
Jakmile známe správné jméno funkce, můžeme samozřejmě použít i její falešnou verzi, například následovně:
def side_effect_handler(): print("side_effect_handler function called") return -1 @patch('application.function2', side_effect=side_effect_handler) def test3(mocked_function_object): print("mocked function called: {c}".format(c=mocked_function_object.called)) print(function1()) print("mocked function called: {c}".format(c=mocked_function_object.called))
Výsledek:
mocked function called: False function1 called side_effect_handler function called -1 mocked function called: True
Podobně, jako tomu bylo ve druhé kapitole, si uvedeme úplný zdrojový kód dnešního v pořadí druhého demonstračního příkladu, jenž je opět složen ze dvou souborů – application.py a test.py (a z pomocného souboru main.py). Tento příklad naleznete na adrese https://github.com/tisnik/testing-in-python/tree/master/unittest_mock/mock-test4:
Soubor application.py s testovanou funkcí
"""Implementace logiky aplikace, kterou budeme testovat.""" def function1(): """První funkce, která volá funkci druhou.""" print("function1 called") return function2() def function2(): """Druhá funkce, kterou v testech nahradíme mockem.""" print("function2 called") return "function 2"
Soubor test.py
"""Implementace jednotkových testů.""" from unittest.mock import * from application import * def test1(): """První test neprovádí prakticky žádné reálné kontroly, jen zavolá testovanou funkci.""" print("function1 returns: {v}".format(v=function1())) @patch('application.function2', return_value=42) def test2(mocked_function): # vytiskneme informaci o tom, zda se mockovaná funkce zavolala print("mocked function called: {c}".format(c=mocked_function.called)) print("function1 returns: {v}".format(v=function1())) # opět vytiskneme informaci o tom, zda se mockovaná funkce zavolala print("mocked function called: {c}".format(c=mocked_function.called)) def side_effect_handler(): print("side_effect_handler function called") return -1 @patch('application.function2', side_effect=side_effect_handler) def test3(mocked_function): # vytiskneme informaci o tom, zda se mockovaná funkce zavolala print("mocked function called: {c}".format(c=mocked_function.called)) print(function1()) # opět vytiskneme informaci o tom, zda se mockovaná funkce zavolala print("mocked function called: {c}".format(c=mocked_function.called)) if __name__ == '__main__': test1() print() test2() print() test3() print()
5. Modifikace kódu v případě, že použijeme import a nikoli from X import
Dnešní třetí demonstrační příklad je založen na prakticky stejném kódu jako příklad předchozí, ovšem s tím podstatným rozdílem, že se modul application importuje do testů nikoli příkazem:
from application import *
ale naopak následujícím způsobem:
import application
To vede k tomu, že přímé volání funkcí z modulu application je nutné provádět s uvedením jména modulu. Samotné testy a dokonce ani obsah dekorátoru (v jiných programovacích jazycích anotace) @patch se ovšem žádným způsobem nezmění. Testy nyní budou vypadat následovně:
from unittest.mock import * import application def test1(): """První test neprovádí prakticky žádné reálné kontroly, jen zavolá testovanou funkci.""" print("function1 returns: {v}".format(v=application.function1())) @patch('application.function2', return_value=42) def test2(mocked_function_object): # vytiskneme informaci o tom, zda se mockovaná funkce zavolala print("mocked function called: {c}".format(c=mocked_function_object.called)) print("function1 returns: {v}".format(v=application.function1())) # opět vytiskneme informaci o tom, zda se mockovaná funkce zavolala print("mocked function called: {c}".format(c=mocked_function_object.called)) def side_effect_handler(): print("side_effect_handler function called") return -1 @patch('application.function2', side_effect=side_effect_handler) def test3(mocked_function_object): # vytiskneme informaci o tom, zda se mockovaná funkce zavolala print("mocked function called: {c}".format(c=mocked_function_object.called)) print("function1 returns: {v}".format(v=application.function1())) # opět vytiskneme informaci o tom, zda se mockovaná funkce zavolala print("mocked function called: {c}".format(c=mocked_function_object.called)) if __name__ == '__main__': test1() print() test2() print() test3() print()
Výsledek běhu testů již dokážeme velmi dobře předvídat:
function1 called function2 called function1 returns: function 2 mocked function called: False function1 called function1 returns: 42 mocked function called: True mocked function called: False function1 called side_effect_handler function called function1 returns: -1 mocked function called: True
6. Mockování metod
Prakticky stejným způsobem, jakým jsme vytvářeli „falešné“ varianty běžných funkcí (tedy mocky), je možné nahrazovat metody vybraných tříd. Ostatně v programovacím jazyku Python není mezi funkcemi a metodami tak velký rozdíl, jako v některých jiných programovacích jazycích, které před programátory skrývají (mj) implicitní parametr self či this. V následujících třech kapitolách si ukážeme vytvoření jednoduchého mocku nepřímo volané (nestatické a netřídní) metody.
7. Testovaná třída
Třída, kterou budeme testovat, je opět velmi jednoduchá. Kromě konstruktoru obsahuje i dvě metody nazvané method1 a method2, přičemž druhá metoda je automaticky volána z metody první (opět v příkazu return, což ovšem není podstatné):
"""Implementace logiky aplikace, kterou budeme testovat.""" class Application: def __init__(self): pass def method1(self): """První metoda, která volá metodu druhou.""" print("method1 called") return self.method2() def method2(self): """Druhá metoda, kterou v testech nahradíme mockem.""" print("method2 called") return "method 2"
Poznámka: ve skutečnosti obě metody nepřistupují k žádnému atributu objektu, takže by se z nich mohly stát statické metody pomocí anotace @staticmethod; alternativně i třídní metody. Nicméně pro jednoduchost zatím uvažujme o nestatických metodách, kterým se při jejich volání explicitně předává i parametr self.
8. Nepřímé volání metody, kterou budeme mockovat
Samotný modul s testem bude začínat přesně tak, jak jsme zvyklí, tj. importem třídy unittest.mock i třídy Application uložené v modulu application:
from unittest.mock import * from application import Application
První jednotkový test prozatím žádnou mockovanou funkci nepoužívá, pouze je v něm ukázán způsob volání metody method1 společně s výpisem návratové hodnoty této metody:
def test1(): app = Application() print("method1 returns: {v}".format(v=app.method1()))
Po spuštění tohoto testu by se na standardní výstup měly vypsat následující řádky oznamující, že se z první metody volá metoda druhá:
method1 called method2 called method1 returns: method 2
9. Mockování nepřímo volané metody
Nyní se již dostáváme k popisu mockování metod. Opět si musíme uvědomit, že je zapotřebí zadat plné jméno metody, a to podle toho, v jakém kontextu bude tato metoda volána. Vzhledem k tomu, že mockujeme metodu se jménem Application.method2 („Application“ je jméno třídy), která bude volána nepřímo (nikoli z testu, ale z první metody method1), bude plné jméno mockované metody znít „application.Application.method2“.
Zkusme si tento test napsat:
@patch('application.Application.method2', return_value=42) def test2(mocked_method): app = Application() print("mocked method called: {c}".format(c=mocked_method.called)) print("method1 returns: {v}".format(v=app.method1())) print("mocked method called: {c}".format(c=mocked_method.called))
Výsledek po spuštění testu:
mocked method called: False method1 called method1 returns: 42 mocked method called: True
Vzhledem k tomu, že vlastnosti mockovaných funkcí a metod jsou prakticky stejné, můžeme metodu nahradit jiným handlerem atd. atd.:
def side_effect_handler(): print("side_effect_handler method called") return -1 @patch('application.Application.method2', side_effect=side_effect_handler) def test3(mocked_method): app = Application() print("mocked method called: {c}".format(c=mocked_method.called)) print("method1 returns: {v}".format(v=app.method1())) print("mocked method called: {c}".format(c=mocked_method.called))
Výsledek po spuštění testu:
mocked method called: False method1 called side_effect_handler method called method1 returns: -1 mocked method called: True
10. Složitější aplikace implementovaná ve větším množství modulů
Nyní ukážeme si způsob mockování funkcí u složitěji strukturované aplikace, v níž se nachází celkem tři moduly pojmenované jednoduše module1.py, module2.py a module3.py:
module1.py module2.py module3.py
V prvním modulu module1 nalezneme funkci nazvanou function1, která pouze na standardní výstup vypíše své jméno a zavolá funkci function2 z modulu module2. Tento modul samozřejmě musíme importovat:
from module2 import * def function1(): print("function1") return "function1 " + function2()
Druhý modul pojmenovaný module2 vypadá podobně jako modul první, ovšem nachází se v něm funkce function2 volající funkci function3 ze třetího modulu:
from module3 import * def function2(): print("function2") return "function2 " + function3()
Konečně se dostáváme ke třetímu modulu, který je nejjednodušší, protože neobsahuje žádný import, ale pouze implementaci funkce nazvané function3:
def function3(): print("function3") return "function3"
Celá aplikace se spouští z modulu main.py, jehož obsah je triviální – spouští se v něm funkce z prvního zmíněného modulu:
from module1 import * if __name__ == '__main__': print(function1())
Aplikaci si můžeme vyzkoušet například v debuggeru, a to poměrně jednoduše. Nejprve debugger spustíme (použijeme přitom standardní debugger Pythonu):
$ python3 -m pdb main.py
Průběh spuštění a inicializace:
> /home/tester/temp/python/mocking-in-python/mock-test7/main.py(1)<module>() -> from module1 import *
Dále nastavíme tzv. breakpoint (bod zastavení), a to na poslední volané funkci function3. Tato funkce není v modulu main.py a tudíž ji musíme klasifikovat celým jménem:
(Pdb) break module3.function3 Breakpoint 1 at /home/tester/temp/python/mocking-in-python/mock-test7/module3.py:1
Program v debuggeru spustíme příkazem continue:
(Pdb) continue function1 function2 > /home/tester/temp/python/mocking-in-python/mock-test7/module3.py(2)function3() -> print("function3")
A po jeho zastavení se podíváme, jak vypadají zásobníkové rámce:
(Pdb) where /usr/lib/python3.4/bdb.py(431)run() -> exec(cmd, globals, locals) <string>(1)<module>() /home/tester/temp/python/mocking-in-python/mock-test7/main.py(5)<module>() -> print(function1()) /home/tester/temp/python/mocking-in-python/mock-test7/module1.py(6)function1() -> return "function1 " + function2() /home/tester/temp/python/mocking-in-python/mock-test7/module2.py(6)function2() -> return "function2 " + function3() > /home/tester/temp/python/mocking-in-python/mock-test7/module3.py(2)function3() -> print("function3")
Z předchozího výpisu je zřejmý celý řetězec volání.
11. Mockování funkcí volaných nepřímo z jiných modulů
Nyní si již můžeme ukázat, jakým způsobem lze mockovat funkce volané z jiných modulů. V našem konkrétním případě by se jednalo o tato volání:
Voláno z | Volaná funkce | Modul v němž je funkce definována |
---|---|---|
(testy).py | function1 | module1.py |
module1.py | function2 | module2.py |
module2.py | function3 | module3.py |
Připomeňme si základní pravidlo, které platí v Pythonu při použití unittest.mock – při deklaraci mocku musíme uvést plné jméno funkce v kontextu jejího volání, tj. z jakého modulu je funkce volána a nikoli v jakém modulu je definována.
Zkusme si tedy napsat testovací skript test.py. Jeho začátek je „klasický“:
from unittest.mock import * from module1 import *
První jednotkový test test1 bude jednoduše volat funkci function1 z modulu module1:
def test1(): print("*** test1 ***") value = function1() print("function1 returns: {v}".format(v=value))
Ve druhém testu voláme stejnou funkci, ale současně se pokusíme mockovat funkci function2 volanou z modulu module2 (což ale nebude mít žádný efekt):
@patch('module2.function2', return_value="*mocked*") def test2(mocked_function): print("*** test2 ***") value = function1() print("function1 returns: {v}".format(v=value))
Třetí test již provede mockování skutečně volané funkce. Stále se jedná o funkci function2, ovšem volanou z modulu module1, což je i případ naší „reálné“ aplikace:
@patch('module1.function2', return_value="*mocked*") def test3(mocked_function): print("*** test3 ***") value = function1() print("function1 returns: {v}".format(v=value))
Čtvrtý test posunuje mock o jednu úroveň dále, konkrétně na funkci function3 volanou z modulu module2:
@patch('module2.function3', return_value="*mocked*") def test4(mocked_function): print("*** test4 ***") value = function1() print("function1 returns: {v}".format(v=value))
A konečně se pokusíme taktéž mockovat funkci function3, ovšem tentokrát volanou z modulu module3, což ve skutečnosti nikdy nenastane (to však testovací framework neoznámí!):
@patch('module3.function3', return_value="*mocked*") def test5(mocked_function): print("*** test5 ***") value = function1() print("function1 returns: {v}".format(v=value))
Poslední část skriptu pouze testy spustí bez dalších triků:
if __name__ == '__main__': test1() print() test2() print() test3() print() test4() print() test5() print()
12. Výsledek spuštění testů
Podívejme se nyní na výsledky, které získáme po spuštění testů definovaných v předchozím demonstračním příkladu. Vlastní spuštění je jednoduché, protože postačuje spustit skript nazvaný test (ten spustí interpret Pythonu a předá mu skript test.py):
$ ./test
První test test1 ve skutečnosti žádné mockování nepoužívá, takže se funkce spustí v pořadí module1.function1 → module2.function2 → module3.function3:
*** test1 *** function1 function2 function3 function1 returns: function1 function2 function3
Ve druhém testu jsme se snažili mockovat funkci module2.function2, ovšem ve skutečnosti se tímto způsobem žádná funkce nevolá (voláme sice function2, ovšem v kontextu prvního modulu), takže výsledek bude stejný, jako v příkladu předchozím:
*** test2 *** function1 function2 function3 function1 returns: function1 function2 function3
Teprve ve třetím testu, v němž byl mock nastaven na „module1.function2“ se namísto funkce module2.function2 použila nastavená návratová hodnota „*mocked*“. Třetí funkce se pochopitelně vůbec nezavolá:
*** test3 *** function1 function1 returns: function1 *mocked*
Čtvrtý příklad je obdobný, ovšem s tím rozdílem, že se mock použil namísto funkce module3.function3, ovšem – to je důležité – v kontextu druhého modulu, nikoli modulu třetího:
*** test4 *** function1 function2 function1 returns: function1 function2 *mocked*
A konečně v posledním testu byla mockována funkce function3 v kontextu modulu module3, což je sice korektní, ovšem v tomto kontextu žádná funkce zavolána není, takže výsledek je následující:
*** test5 *** function1 function2 function3 function1 returns: function1 function2 function3
13. Mockování funkce přímo volané z testů
V demonstračním příkladu, s nímž jsme se seznámili v předchozích kapitolách, se modul nazvaný module1 do testovacího modulu importoval tímto způsobem:
from module1 import *
To je samozřejmě zcela legální způsob, ovšem má jednu nevýhodu – všechny funkce a třídy z modulu module1 se stanou součástí jmenného prostoru testů, takže se při tvorbě mocků těchto funkcí/tříd dostaneme do zbytečných problémů. Praktičtější bude import nepatrně změnit a použít následující řádek:
import module1
Nyní je již možné velmi jednoduše mockovat funkci function1 volanou v kontextu modulu module1, protože přesně takto k této funkci budeme muset přistupovat:
@patch('module1.function1', return_value="*mocked*") def test1(mocked_function): print("*** test1 ***") value = module1.function1() print("function1 returns: {v}".format(v=value))
Podobným způsobem je možné zabezpečit mockování dalších funkcí, tentokrát již volaných nepřímo. Povšimněte si, že všechny další anotace @patch jsou shodné s prvním demonstračním příkladem:
@patch('module2.function2', return_value="*mocked*") def test2(mocked_function): print("*** test2 ***") value = module1.function1() print("function1 returns: {v}".format(v=value)) @patch('module1.function2', return_value="*mocked*") def test3(mocked_function): print("*** test3 ***") value = module1.function1() print("function1 returns: {v}".format(v=value)) @patch('module2.function3', return_value="*mocked*") def test4(mocked_function): print("*** test4 ***") value = module1.function1() print("function1 returns: {v}".format(v=value)) @patch('module3.function3', return_value="*mocked*") def test5(mocked_function): print("*** test5 ***") value = module1.function1() print("function1 returns: {v}".format(v=value))
I spuštění testů je pochopitelně stejné (zde se nic nezměnilo):
if __name__ == '__main__': test1() print() test2() print() test3() print() test4() print() test5() print()
14. Výsledek spuštění testů
Pokud tento demonstrační příklad spustíme, zjistíme snadno, že je skutečně možné mockovat i funkci pojmenovanou function1 z modulu module1 (viz první dva řádky výpisu):
*** test1 *** function1 returns: *mocked*
Další zprávy vypisované v testech jsou již shodné s prvním příkladem:
*** test2 *** function1 function2 function3 function1 returns: function1 function2 function3 *** test3 *** function1 function1 returns: function1 *mocked* *** test4 *** function1 function2 function1 returns: function1 function2 *mocked* *** test5 *** function1 function2 function3 function1 returns: function1 function2 function3
15. Přímé použití konstruktoru patch() v těle testů
V některých případech není možné vystačit s tím, že se před testovací funkci napíše anotace @patch. Můžeme se totiž dostat do situace, kdy budeme jednou potřebovat zavolat funkci původní a jindy (v tom samém testu) mock této funkce. I toto chování je samozřejmě podporováno, protože namísto nám již známého zápisu:
@patch('module1.function1', return_value="*mocked*") def test1(mocked_function): print("*** test1 ***") value = module1.function1() print("function1 returns: {v}".format(v=value))
Je možné použít přímé volání funkce patch importované z modulu unittest. Typicky se volání funkce patch používá společně s řídicí konstrukcí with, která omezuje kontext, v němž je mock platný (dnes se pravděpodobně jedná o idiomatický způsob použití). Podívejme se na příklad:
def test1(): with patch("module1.function1", return_value="*mocked"): print("*** test1 ***") value = module1.function1() print("function1 returns: {v}".format(v=value))
Podobným způsobem lze zapsat i další testovací funkce. Výsledky testů budou shodné s výsledky předchozího příkladu:
*** test1 *** function1 returns: *mocked *** test2 *** function1 function2 function3 function1 returns: function1 function2 function3 *** test3 *** function1 function1 returns: function1 *mocked *** test4 *** function1 function2 function1 returns: function1 function2 *mocked *** test5 *** function1 function2 function3 function1 returns: function1 function2 function3
16. Další možnosti nabízené objekty Mock
Ve druhé kapitole jsme si řekli, že je velmi snadné zjistit, jestli je mockovaná funkce volána či nikoli. To lze samozřejmě provést i ve chvíli, kdy nepoužijeme anotaci @patch, ale namísto toho přímo zavoláme funkci patch v bloku with (předchozí kapitola). Co ovšem musíme doplnit je jméno proměnné obsahující referenci na mock. Tato reference je vrácena funkcí patch, ovšem kvůli jejímu volání v bloku with je syntaxe zápisu nepatrně odlišná:
with patch("jméno_modulu.jméno_funkce") as jméno_proměnné_s_referencí_na mock: ... ... ...
Ve chvíli, kdy máme referenci na mock, můžeme zjistit, zda byl volán přečtením vlastnosti called. Příklad použití pro první test:
def test1(): with patch("module1.function1") as mocked_function: mocked_function.return_value = "*mocked*" print("*** test1 ***") value = module1.function1() print("function1 returns: {v}".format(v=value)) print("mocked function called: {c}".format(c=mocked_function.called))
I zbylé testy budou vypadat podobně. Podívejme se ještě na výsledky těchto testů.
*** test1 *** function1 returns: *mocked* mocked function called: True
U druhého testu k volání mocku nedošlo (což již nepřímo víme z výsledné hodnoty vrácené funkcí function1):
*** test2 *** function1 function2 function3 function1 returns: function1 function2 function3 mocked function called: False
V dalších dvou testech se mockovaná funkce volala:
*** test3 *** function1 function1 returns: function1 *mocked* mocked function called: True *** test4 *** function1 function2 function1 returns: function1 function2 *mocked* mocked function called: True
V posledním testu však již opět ne:
*** test5 *** function1 function2 function3 function1 returns: function1 function2 function3 mocked function called: False
Tyto postupy naleznete v příkladu dostupném na adrese https://github.com/tisnik/testing-in-python/tree/master/unittest_mock/mock-test9.
17. Zjištění kolikrát a s jakými parametry byla mockovaná funkce zavolána
Další vlastnost je při testování taktéž nemálo užitečná. Zavoláním mock.assert_called_with(hodnota1, hodnota2, …) je totiž možné zjistit, zda vůbec a s jakými parametry je mock zavolán. To však není zdaleka vše, protože ve vlastnosti mock_calls je uložena sekvence všech volání. Z této sekvence zjistíme jak pořadí volání mocku, tak i hodnoty parametrů, které byly při volání použity. V dalším testu budeme mockovat tuto funkci:
def add(x, y): return add_implementation(x, y)
První test může vypadat následovně – nejdříve mock zavoláme, následně vypíšeme informaci o tom, zda byl skutečně zavolán, posléze otestujeme, jestli se použili parametry 1 a 2 (popř. při druhém volání 100 a 100) a konečně si necháme vypsat seznam všech volání dané funkce:
def test1(): print("*** test1 ***") with patch("module1.add_implementation") as mocked_function: mocked_function.return_value = 42 value = module1.add(1, 2) print("add returns: {v}".format(v=value)) print("mocked function called: {c}".format(c=mocked_function.called)) mocked_function.assert_called_with(1, 2) value = module1.add(100, 100) print("add returns: {v}".format(v=value)) print("mocked function called: {c}".format(c=mocked_function.called)) mocked_function.assert_called_with(100, 100) print("calls: ", mocked_function.mock_calls)
Naproti tomu druhý test je schválně napsán takovým způsobem, aby se při kontrole zjistilo, že mock byl sice volán, ale s neočekávanými parametry. Ve skutečnosti totiž voláme funkci s parametry 1 a 2, ale očekáváme volání s parametry 1 a 1:
def test2(): print("*** test2 ***") with patch("module1.add_implementation") as mocked_function: mocked_function.return_value = 42 value = module1.add(1, 2) print("add returns: {v}".format(v=value)) print("mocked function called: {c}".format(c=mocked_function.called)) mocked_function.assert_called_with(1, 1)
Nejzajímavější je samozřejmě výsledek spuštění testů. V prvním testu se skutečně mockovaná funkce volala, a to dokonce dvakrát – poprvé s parametry 1, 2, podruhé s parametry 100, 100:
*** test1 *** add returns: 42 mocked function called: True add returns: 42 mocked function called: True calls: [call(1, 2), call(100, 100)]
U druhého testu však dojde k pádu, protože se mock nezavolal ani jednou, což kontrola velmi rychle odhalí:
*** test2 *** add returns: 42 mocked function called: True Traceback (most recent call last): File "test.py", line 41, in test2() File "test.py", line 34, in test2 mocked_function.assert_called_with(1, 1) File "/usr/lib/python3.4/unittest/mock.py", line 771, in assert_called_with raise AssertionError(_error_message()) from cause AssertionError: Expected call: add_implementation(1, 1) Actual call: add_implementation(1, 2)
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ě deseti 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:
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:
- 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/ - 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/ - 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/ - 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/ - Validace datových struktur v Pythonu (2. část)
https://www.root.cz/clanky/validace-datovych-struktur-v-pythonu-2-cast/ - Validace datových struktur v Pythonu (dokončení)
https://www.root.cz/clanky/validace-datovych-struktur-v-pythonu-dokonceni/ - Univerzální testovací nástroj Robot Framework
https://www.root.cz/clanky/univerzalni-testovaci-nastroj-robot-framework/ - Univerzální testovací nástroj Robot Framework a BDD testy
https://www.root.cz/clanky/univerzalni-testovaci-nastroj-robot-framework-a-bdd-testy/ - Úvod do problematiky fuzzingu a fuzz testování
https://www.root.cz/clanky/uvod-do-problematiky-fuzzingu-a-fuzz-testovani/ - Ú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/ - 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/ - 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/ - Testování aplikací naprogramovaných v jazyce Go
https://www.root.cz/clanky/testovani-aplikaci-naprogramovanych-v-jazyce-go/ - 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/ - 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/ - 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/ - 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/ - 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/ - 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/ - 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/ - 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
- Awesome Python – testing
https://github.com/vinta/awesome-python#testing - Selenium (pro Python)
https://pypi.org/project/selenium/ - Getting Started With Testing in Python
https://realpython.com/python-testing/ - unittest.mock — mock object library
https://docs.python.org/3.5/library/unittest.mock.html - mock 2.0.0
https://pypi.python.org/pypi/mock - An Introduction to Mocking in Python
https://www.toptal.com/python/an-introduction-to-mocking-in-python - Mock – Mocking and Testing Library
http://mock.readthedocs.io/en/stable/ - Python Mocking 101: Fake It Before You Make It
https://blog.fugue.co/2016–02–11-python-mocking-101.html - Nauč se Python! – Testování
http://naucse.python.cz/lessons/intro/testing/ - Flexmock (dokumentace)
https://flexmock.readthedocs.io/en/latest/ - Test Fixture (Wikipedia)
https://en.wikipedia.org/wiki/Test_fixture - Mock object (Wikipedia)
https://en.wikipedia.org/wiki/Mock_object - 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 - Pip (dokumentace)
https://pip.pypa.io/en/stable/ - Tox
https://tox.readthedocs.io/en/latest/ - pytest: helps you write better programs
https://docs.pytest.org/en/latest/ - doctest — Test interactive Python examples
https://docs.python.org/dev/library/doctest.html#module-doctest - unittest — Unit testing framework
https://docs.python.org/dev/library/unittest.html - Python namespaces
https://bytebaker.com/2008/07/30/python-namespaces/ - Namespaces and Scopes
https://www.python-course.eu/namespaces.php - Stránka projektu Robot Framework
https://robotframework.org/ - GitHub repositář Robot Frameworku
https://github.com/robotframework/robotframework - Robot Framework (Wikipedia)
https://en.wikipedia.org/wiki/Robot_Framework - Tutoriál Robot Frameworku
http://www.robotframeworktutorial.com/ - Robot Framework Documentation
https://robotframework.org/robotframework/ - Robot Framework Introduction
https://blog.testproject.io/2016/11/22/robot-framework-introduction/ - robotframework 3.1.2 na PyPi
https://pypi.org/project/robotframework/ - Robot Framework demo (GitHub)
https://github.com/robotframework/RobotDemo - Robot Framework web testing demo using SeleniumLibrary
https://github.com/robotframework/WebDemo - Robot Framework for Mobile Test Automation Demo
https://www.youtube.com/watch?v=06LsU08slP8 - Gherkin
https://cucumber.io/docs/gherkin/ - Selenium
https://selenium.dev/ - SeleniumLibrary
https://robotframework.org/ - The Practical Test Pyramid
https://martinfowler.com/articles/practical-test-pyramid.html - Acceptance Tests and the Testing Pyramid
http://www.blog.acceptancetestdrivendevelopment.com/acceptance-tests-and-the-testing-pyramid/ - Tab-separated values
https://en.wikipedia.org/wiki/Tab-separated_values - A quick guide about Python implementations
https://blog.rmotr.com/a-quick-guide-about-python-implementations-aa224109f321 - radamsa
https://gitlab.com/akihe/radamsa - Fuzzing (Wikipedia)
https://en.wikipedia.org/wiki/Fuzzing - american fuzzy lop
http://lcamtuf.coredump.cx/afl/ - Fuzzing: the new unit testing
https://go-talks.appspot.com/github.com/dvyukov/go-fuzz/slides/fuzzing.slide#1 - Corpus for github.com/dvyukov/go-fuzz examples
https://github.com/dvyukov/go-fuzz-corpus - AFL – QuickStartGuide.txt
https://github.com/google/AFL/blob/master/docs/QuickStartGuide.txt - Introduction to Fuzzing in Python with AFL
https://alexgaynor.net/2015/apr/13/introduction-to-fuzzing-in-python-with-afl/ - Writing a Simple Fuzzer in Python
https://jmcph4.github.io/2018/01/19/writing-a-simple-fuzzer-in-python/ - How to Fuzz Go Code with go-fuzz (Continuously)
https://fuzzit.dev/2019/10/02/how-to-fuzz-go-code-with-go-fuzz-continuously/ - Golang Fuzzing: A go-fuzz Tutorial and Example
http://networkbit.ch/golang-fuzzing/ - Fuzzing Python Modules
https://stackoverflow.com/questions/20749026/fuzzing-python-modules - 0×3 Python Tutorial: Fuzzer
http://www.primalsecurity.net/0×3-python-tutorial-fuzzer/ - fuzzing na PyPi
https://pypi.org/project/fuzzing/ - Fuzzing 0.3.2 documentation
https://fuzzing.readthedocs.io/en/latest/ - Randomized testing for Go
https://github.com/dvyukov/go-fuzz - HTTP/2 fuzzer written in Golang
https://github.com/c0nrad/http2fuzz - 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 - Continuous Fuzzing Made Simple
https://fuzzit.dev/ - Halt and Catch Fire
https://en.wikipedia.org/wiki/Halt_and_Catch_Fire#Intel_x86 - Random testing
https://en.wikipedia.org/wiki/Random_testing - Monkey testing
https://en.wikipedia.org/wiki/Monkey_testing - Fuzzing for Software Security Testing and Quality Assurance, Second Edition
https://books.google.at/books?id=tKN5DwAAQBAJ&pg=PR15&lpg=PR15&q=%22I+settled+on+the+term+fuzz%22&redir_esc=y&hl=de#v=onepage&q=%22I%20settled%20on%20the%20term%20fuzz%22&f=false - libFuzzer – a library for coverage-guided fuzz testing
https://llvm.org/docs/LibFuzzer.html - fuzzy-swagger na PyPi
https://pypi.org/project/fuzzy-swagger/ - fuzzy-swagger na GitHubu
https://github.com/namuan/fuzzy-swagger - Fuzz testing tools for Python
https://wiki.python.org/moin/PythonTestingToolsTaxonomy#Fuzz_Testing_Tools - A curated list of awesome Go frameworks, libraries and software
https://github.com/avelino/awesome-go - gofuzz: a library for populating go objects with random values
https://github.com/google/gofuzz - tavor: A generic fuzzing and delta-debugging framework
https://github.com/zimmski/tavor - hypothesis na GitHubu
https://github.com/HypothesisWorks/hypothesis - Hypothesis: Test faster, fix more
https://hypothesis.works/ - Hypothesis
https://hypothesis.works/articles/intro/ - What is Hypothesis?
https://hypothesis.works/articles/what-is-hypothesis/ - Databáze CVE
https://www.cvedetails.com/ - Fuzz test Python modules with libFuzzer
https://github.com/eerimoq/pyfuzzer - Taof – The art of fuzzing
https://sourceforge.net/projects/taof/ - JQF + Zest: Coverage-guided semantic fuzzing for Java
https://github.com/rohanpadhye/jqf - http2fuzz
https://github.com/c0nrad/http2fuzz - Demystifying hypothesis testing with simple Python examples
https://towardsdatascience.com/demystifying-hypothesis-testing-with-simple-python-examples-4997ad3c5294 - Testování
http://voho.eu/wiki/testovani/ - Unit testing (Wikipedia.en)
https://en.wikipedia.org/wiki/Unit_testing - Unit testing (Wikipedia.cz)
https://cs.wikipedia.org/wiki/Unit_testing - Unit Test vs Integration Test
https://www.youtube.com/watch?v=0GypdsJulKE - TestDouble
https://martinfowler.com/bliki/TestDouble.html - Test Double
http://xunitpatterns.com/Test%20Double.html - Test-driven development (Wikipedia)
https://en.wikipedia.org/wiki/Test-driven_development - Acceptance test–driven development
https://en.wikipedia.org/wiki/Acceptance_test%E2%80%93driven_development - Gauge
https://gauge.org/ - Gauge (software)
https://en.wikipedia.org/wiki/Gauge_(software) - PYPL PopularitY of Programming Language
https://pypl.github.io/PYPL.html - Testing is Good. Pyramids are Bad. Ice Cream Cones are the Worst
https://medium.com/@fistsOfReason/testing-is-good-pyramids-are-bad-ice-cream-cones-are-the-worst-ad94b9b2f05f - Články a zprávičky věnující se Pythonu
https://www.root.cz/n/python/ - PythonTestingToolsTaxonomy
https://wiki.python.org/moin/PythonTestingToolsTaxonomy - Top 6 BEST Python Testing Frameworks [Updated 2020 List]
https://www.softwaretestinghelp.com/python-testing-frameworks/