Bonjour. C'est leo1109. Cette fois également, suite à la précédente, nous parlerons des tests.
Le code utilisé dans l'article a été téléchargé sur GitHub.
Il s'agit d'écrire un test. Utilisez le patch.
Nous avons amélioré le script précédent et ajouté les fonctions suivantes.
De plus, l'acquisition du numéro de Fibonacci et l'acquisition du total ont été changées en implémentation telle que définie.
# python 3.5.2
import time
from functools import wraps
def required_natural_number(func):
@wraps(func)
def wrapper(*args, **kwargs):
k = args[0]
if isinstance(k, bool):
raise TypeError
if not isinstance(k, int) or k < 1:
raise TypeError
return func(*args, **kwargs)
return wrapper
def _f(k):
if k == 1 or k == 2:
return 1
else:
return _f(k-1) + _f(k-2)
def _sum(k):
if k == 1:
return 1
else:
return k + _sum(k-1)
@required_natural_number
def fibonacci(k):
return _f(k)
@required_natural_number
def sum_all(k):
return _sum(k)
@required_natural_number
def delta_of_sum_all_and_fibonacci(k):
x = sum_all(k) - fibonacci(k)
return x if x >= 0 else -x
@required_natural_number
def surplus_time_by(k):
return int(time.time() % k)
Ajoutez un test pour sum_all_minus_fibonacci (x)
pour obtenir la différence entre la somme et le nombre de Fibonacci.
J'ai choisi la valeur du cas de test de manière appropriée.
my_math_2_test.py
# python 3.5.2
from unittest.mock import patch
import pytest
import my_math
class TestFibonacci:
def test(self):
assert my_math.fibonacci(1) == 1
assert my_math.fibonacci(5) == 5
assert my_math.fibonacci(10) == 55
assert my_math.fibonacci(20) == 6765
assert my_math.fibonacci(30) == 832040
assert my_math.fibonacci(35) == 9227465
class TestSumAll:
def test(self):
assert my_math.sum_all(1) == 1
assert my_math.sum_all(5) == 15
assert my_math.sum_all(10) == 55
assert my_math.sum_all(20) == 210
assert my_math.sum_all(30) == 465
assert my_math.sum_all(35) == 630
class TestDeltaOfSumAllAndFibonacci:
def test(self):
assert my_math.delta_of_sum_all_and_fibonacci(1) == 1 - 1
assert my_math.delta_of_sum_all_and_fibonacci(5) == 15 - 5
assert my_math.delta_of_sum_all_and_fibonacci(10) == 55 - 55
assert my_math.delta_of_sum_all_and_fibonacci(20) == -1 * (210 - 6765)
assert my_math.delta_of_sum_all_and_fibonacci(30) == -1 * (465 - 832040)
assert my_math.delta_of_sum_all_and_fibonacci(35) == -1 * (630 - 9227465)
Lançons le test.
$ pytest -v my_math_2_test.py
================================================================== test session starts ==================================================================
collected 3 items
my_math_2_test.py::TestFibonacci::test PASSED
my_math_2_test.py::TestSumAll::test PASSED
my_math_2_test.py::TestDeltaOfSumAllAndFibonacci::test PASSED
=============================================================== 3 passed in 21.01 seconds ===============================================================
Cela a pris 20 secondes. Il semble que cela prend environ 10 secondes chacun pour le test du nombre de Fibonacci et le test de différence.
La cause est que la méthode pour obtenir le numéro de Fibonacci a été réécrite de manière récursive. (Étant donné que le but de cet article est un test, je vais omettre l'explication sur le temps de calcul, mais la mise en œuvre utilisant la récurrence peut prendre un très grand temps de calcul)
Cependant, ce test implique en fait des traitements inutiles autres que des problèmes d'implémentation. En corrigeant ce point, vous pouvez accélérer le test.
C'est une image à coller d'en haut.
La raison du test lent est la méthode pour obtenir le numéro de Fibonacci, je voudrais donc éviter de l'exécuter autant que possible.
En regardant le contenu du test, vous pouvez voir que le test fibonacci (x)
et le delta_of_sum_all_and_fibonacci (x)
appellent tous deux fibonacci (x)
avec les mêmes arguments. ... c'est un peu un gaspillage.
Puisque fibonacci (x)
a été testé seul, je ne veux pas le faire lors du testdelta_of_sum_all_and_fibonacci (x)
. (Parce que ça prend du temps)
Cette fois, utilisons patch
et essayons de ne pas exécuter fibonacci (x)
.
Après avoir sauté le test existant, j'ai ajouté un test qui "patchʻed fibonacci (x)
.
class TestDeltaOfSumAllAndFibonacci:
@pytest.mark.skip
def test_slow(self):
assert my_math.delta_of_sum_all_and_fibonacci(1) == 1 - 1
assert my_math.delta_of_sum_all_and_fibonacci(5) == 15 - 5
assert my_math.delta_of_sum_all_and_fibonacci(10) == 55 - 55
assert my_math.delta_of_sum_all_and_fibonacci(20) \
== -1 * (210 - 6765)
assert my_math.delta_of_sum_all_and_fibonacci(30) \
== -1 * (465 - 832040)
assert my_math.delta_of_sum_all_and_fibonacci(35) \
== -1 * (630 - 9227465)
def test_patch(self):
with patch('my_math.fibonacci') as mock_fib:
mock_fib.return_value = 1
assert my_math.delta_of_sum_all_and_fibonacci(1) == 1 - 1
assert my_math.delta_of_sum_all_and_fibonacci(5) == 15 - 5
assert my_math.delta_of_sum_all_and_fibonacci(10) == 55 - 55
assert my_math.delta_of_sum_all_and_fibonacci(20) \
== -1 * (210 - 6765)
assert my_math.delta_of_sum_all_and_fibonacci(30) \
== -1 * (465 - 832040)
assert my_math.delta_of_sum_all_and_fibonacci(35) \
== -1 * (630 - 9227465)
patch réécrit le contenu d'exécution de la méthode du module spécifié en utilisant l'instruction with et le décorateur.
Pour confirmer l'opération, fibonacci (x)
renvoie toujours 1.
Lançons-le.
$ pytest -v my_math_2_test.py::TestDeltaOfSumAllAndFibonacci
================================================================== test session starts ==================================================================
collected 2 items
my_math_2_test.py::TestDeltaOfSumAllAndFibonacci::test_slow SKIPPED
my_math_2_test.py::TestDeltaOfSumAllAndFibonacci::test_patch FAILED
======================================================================= FAILURES ========================================================================
_______________________________________________________ TestDeltaOfSumAllAndFibonacci.test_patch ________________________________________________________
self = <my_math_2_test.TestDeltaOfSumAllAndFibonacci object at 0x103abcf28>
def test_patch(self):
with patch('my_math.fibonacci') as mock_fib:
mock_fib.return_value = 1
assert my_math.delta_of_sum_all_and_fibonacci(1) == 1 - 1
> assert my_math.delta_of_sum_all_and_fibonacci(5) == 15 - 5
E assert 14 == (15 - 5)
E + where 14 = <function delta_of_sum_all_and_fibonacci at 0x103ac0840>(5)
E + where <function delta_of_sum_all_and_fibonacci at 0x103ac0840> = my_math.delta_of_sum_all_and_fibonacci
my_math_2_test.py:45: AssertionError
========================================================== 1 failed, 1 skipped in 0.21 seconds ==========================================================
Le test échoue maintenant.
Dans le cas de test de delta_of_sum_all_and_fibonacci (5)
, fibonacci (5)
, qui devrait à l'origine obtenir 5, renvoie maintenant 1, donc le test échoue comme14 == (15 --5)
. Je vais.
Maintenant que nous avons confirmé que le correctif fonctionne comme prévu, nous allons améliorer le test pour renvoyer la valeur correcte.
def test_patch(self):
with patch('my_math.fibonacci') as mock_fib:
mock_fib.return_value = 1
assert my_math.delta_of_sum_all_and_fibonacci(1) == 1 - 1
mock_fib.return_value = 5
assert my_math.delta_of_sum_all_and_fibonacci(5) == 15 - 5
mock_fib.return_value = 55
assert my_math.delta_of_sum_all_and_fibonacci(10) == 55 - 55
mock_fib.return_value = 6765
assert my_math.delta_of_sum_all_and_fibonacci(20) \
== -1 * (210 - 6765)
mock_fib.return_value = 832040
assert my_math.delta_of_sum_all_and_fibonacci(30) \
== -1 * (465 - 832040)
mock_fib.return_value = 9227465
assert my_math.delta_of_sum_all_and_fibonacci(35) \
== -1 * (630 - 9227465)
Réessayer.
$ pytest -v my_math_2_test.py::TestDeltaOfSumAllAndFibonacci
================================================================== test session starts ==================================================================
collected 2 items
my_math_2_test.py::TestDeltaOfSumAllAndFibonacci::test_slow SKIPPED
my_math_2_test.py::TestDeltaOfSumAllAndFibonacci::test_patch PASSED
========================================================== 1 passed, 1 skipped in 0.05 seconds ==========================================================
Le test a réussi.
Le temps d'exécution est de 0,05 (s), ce qui est considérablement plus rapide qu'auparavant.
Bien sûr, si tout était exécuté, cela prendrait autant de temps que le premier fibonacci (x)
pour être exécuté, mais même ainsi, c'était 20 secondes -> 10 secondes, ce qui représentait une réduction de 50%.
Dans la section précédente, en se moquant d'une fonction lente. Nous avons réussi à rationaliser le test.
Au fait, surplus_time_by (k)
est une fonction qui récupère l'heure unix de l'heure actuelle (UTC) et renvoie le reste, mais comme l'heure actuelle change à chaque exécution, définissez une valeur fixe comme avant. Je ne peux pas faire le test que j'ai écrit.
Par exemple, le test suivant réussira si la valeur de «time.time ()» est un multiple de 5, mais pas autrement.
assert my_math.surplus_time_by(5) == 5
Dans ces cas, envisagez de patcher time.time ()
.
time est une bibliothèque Python standard, donc en supposant qu'elle soit déjà bien testée, cela fait partie du calcul du reste.
Maintenant, pour patcher le time.time ()
utilisé dans my_math
, écrivez:
class TestSurplusTimeBy:
def test(self):
with patch('my_math.time') as mock_time:
mock_time.time.return_value = 1000
assert my_math.surplus_time_by(3) == 1
assert my_math.surplus_time_by(5) == 0
mock_time.time.return_value = 1001
assert my_math.surplus_time_by(3) == 2
assert my_math.surplus_time_by(5) == 1
Il est également possible de "patcher" jusqu'à "time.time" comme indiqué ci-dessous.
def test2(self):
with patch('my_math.time.time') as mock_time:
mock_time.return_value = 1000
assert my_math.surplus_time_by(3) == 1
assert my_math.surplus_time_by(5) == 0
mock_time.return_value = 1001
assert my_math.surplus_time_by(3) == 2
assert my_math.surplus_time_by(5) == 1
La différence ci-dessus est la quantité de moquerie faite.
En regardant le résultat de l'exécution ci-dessous, vous pouvez voir que le module de temps lui-même a été réécrit en tant qu'objet fictif lorsque l'heure est corrigée.
Ainsi, par exemple, si vous utilisez autre chose que time ()
under time
, vous devez spécifier explicitement time.time
ou corriger toutes les méthodes que vous utilisez.
# time.Temps de patch
>>> with patch('my_math.time.time') as m:
... print(my_math.time)
... print(my_math.time.time)
...
<module 'time' (built-in)>
<MagicMock name='time' id='4329989904'>
#Temps de patch
>>> with patch('my_math.time') as m:
... print(my_math.time)
... print(my_math.time.time)
...
<MagicMock name='time' id='4330034680'>
<MagicMock name='time.time' id='4330019136'>
Dans cet exemple, le module «time» est importé dans «my_math», alors écrivez-le sous «my_math.time».
Par conséquent, si un autre module my_module
qui utilise my_math
apparaît, vous devez écrire my_module.my_math.time
.
Lors de l'application de correctifs à la méthode d'instance d'une classe, vous devez spécifier jusqu'à l'instance.
my_class.py
# Python 3.5.2
class MyClass:
def __init__(self, prefix='My'):
self._prefix = prefix
def my_method(self, x):
return '{} {}'.format(self._prefix, x)
my_main.py
# Python 3.5.2
from my_class import MyClass
def main(name):
c = MyClass()
return c.my_method(name)
if __name__ == '__main__':
print(main('Python'))
Après avoir exécuté ce qui précède, vous verrez Mon Python
.
Maintenant, si vous voulez patcher MyClass.my_method
, vous pouvez écrire:
my_main_test.py
# python 3.5.2
from unittest.mock import patch
import my_main
class TestFunction:
def test(self):
assert my_main.function('Python') == 'My Python'
def test_patch_return_value(self):
with patch('my_class.MyClass.my_method') as mock:
mock.return_value = 'Hi! Perl'
assert my_main.function('Python') == 'Hi! Perl'
def test_patch_side_effect(self):
with patch('my_class.MyClass.my_method') as mock:
mock.side_effect = lambda x: 'OLA! {}'.format(x)
assert my_main.function('Python') == 'OLA! Python'
Les correctifs sont également utiles lors du test de modules accessibles de manière externe.
En utilisant le module requests
, j'ai créé une méthode qui renvoie le code d'état lorsque l'URL spécifiée est GET.
Pensez à tester cela.
# Python 3.5.2
import requests
def get_status_code(url):
r = requests.get(url)
return r.status_code
J'écrirai un test.
# python 3.5.2
import pytest
import my_http
class TestGetStatusCode:
@pytest.fixture
def url(self):
return 'http://example.com'
def test(self, url):
assert my_http.get_status_code(url) == 200
Ce test passe sans aucun problème. Mais qu'en est-il lorsque vous êtes hors ligne? J'omettrai la trace détaillée de la pile, mais le test échouera.
...
> raise ConnectionError(e, request=request)
E requests.exceptions.ConnectionError: HTTPConnectionPool(host='example.com', port=80): Max retries exceeded with url: / (Caused by NewConnectionError('<urllib3.connection.HTTPConnection object at 0x1039a4cf8>: Failed to establish a new connection: [Errno 8] nodename nor servname provided, or not known',))
../../../.pyenv/versions/3.5.2/lib/python3.5/site-packages/requests/adapters.py:504: ConnectionError =============================================================== 1 failed in 1.06 seconds ================================================================
Pour que ce test réussisse hors ligne, corrigez request.get.
Puisque la valeur de retour est un objet, il est nécessaire de spécifier explicitement MagicMock ()
dans return_value.
# python 3.5.2
from unittest.mock import MagicMock, patch
import pytest
import my_http
class TestGetStatusCode:
@pytest.fixture
def url(self):
return 'http://example.com'
@pytest.mark.skip
def test_online(self, url):
assert my_http.get_status_code(url) == 200
def test_offline(self, url):
with patch('my_http.requests') as mock_requests:
mock_response = MagicMock(status_code=200)
mock_requests.get.return_value = mock_response
assert my_http.get_status_code(url) == 200
Le test ci-dessus passera même hors ligne. Si vous spécifiez un code d'état MagicMock de 400, 500, etc., vous pouvez ajouter un test de cas d'erreur tout en conservant l'URL identique. Il peut être utilisé pour simuler l'accès à la base de données dans un environnement sans base de données ni acquisition de jetons à partir d'un service externe.
Voici quelques conseils pour la correction.
Dans la série Python3, ʻExitStack () `est pratique.
>>> with ExitStack() as stack:
... x = stack.enter_context(patch('my_math.fibonacci'))
... y = stack.enter_context(patch('my_math.sum_all'))
... x.return_value = 100
... y.return_value = 200
... z = my_math.delta_of_sum_all_and_fibonacci(99999)
... print(z)
...
100
Puisque les valeurs de retour des deux fonctions ont été réécrites, my_math.delta_of_sum_all_and_fibonacci (99999)
renvoie la différence entre 200 et 100.
Lorsque vous utilisez une maquette, vous souhaiterez peut-être renvoyer une exception.
Dans ce cas, utilisez mock.side_effect
.
>>> with patch('my_math.fibonacci') as m:
... m.side_effect = ValueError
... my_math.fibonacci(1)
...
Traceback (most recent call last):
File "<stdin>", line 3, in <module>
File "/Users/sho/.pyenv/versions/3.5.2/lib/python3.5/unittest/mock.py", line 917, in __call__
return _mock_self._mock_call(*args, **kwargs)
File "/Users/sho/.pyenv/versions/3.5.2/lib/python3.5/unittest/mock.py", line 973, in _mock_call
raise effect
ValueError
Étant donné que l'objet d'appel est stocké dans l'objet fictif, il est pratique de le vérifier.
Vous n'avez pas besoin d'écrire ʻassert car les méthodes commençant par ʻassert_
lèveront une exception si la condition n'est pas remplie.
Les arguments lorsque la simulation est exécutée sont stockés dans un taple dans mock.call_args
.
Consultez la documentation officielle pour plus de détails.
Seuls les types typiques seront introduits.
--mock.called
S'il a été exécuté (booléen)
--mock.call_count
Nombre d'exécutions
--S'il a été exécuté avec l'argument mock.assert_called_with ()
def test_called(self):
with patch('my_class.MyClass.my_method') as mock:
my_main.function('Python')
assert mock.called
assert mock.call_count == 1
mock.assert_called_with('Python')
Il est indécis jusqu'à la fin des vacances d'Obon!
Recommended Posts