Je veux écrire en Python! (3) Utiliser des simulacres

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.

L'histoire à présenter cette fois

Il s'agit d'écrire un test. Utilisez le patch.

Créer un module de test

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)

Écrivez un test immédiatement

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.

Patcher la fonction

C'est une image à coller d'en haut.

Patch des fonctions lentes

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

Fonctions de patch avec des valeurs de retour indéterminées

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'>

À propos de la chaîne de caractères spécifiée pour le patch

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'

Méthodes de patch pour l'accès externe

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.

Conseils de patch

Voici quelques conseils pour la correction.

Si vous souhaitez patcher plusieurs méthodes

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.

Je souhaite renvoyer une exception

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

Vérifiez si le simulacre a été exécuté

É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')

la prochaine fois

Il est indécis jusqu'à la fin des vacances d'Obon!

Recommended Posts

Je veux écrire en Python! (3) Utiliser des simulacres
Je veux afficher la progression en Python!
Je veux écrire en Python! (1) Vérification du format de code
Je veux écrire en Python! (2) Écrivons un test
Je veux utiliser le jeu de données R avec python
Je ne voulais pas écrire la clé AWS dans le programme
J'ai écrit le code pour écrire le code Brainf * ck en python
Je veux faire le test de Dunnett en Python
Je veux créer une fenêtre avec Python
Conseils pour rédiger un aplatissement concis en python
Je veux écrire dans un fichier avec Python
Je veux convertir par lots le résultat de "chaîne de caractères" .split () en Python
Je veux expliquer en détail la classe abstraite (ABCmeta) de Python
J'ai essayé de représenter graphiquement les packages installés en Python
Je veux facilement implémenter le délai d'expiration en python
Même avec JavaScript, je veux voir Python `range ()`!
Je veux échantillonner au hasard un fichier avec Python
Je veux hériter de l'arrière avec la classe de données python
Je veux travailler avec un robot en python.
Je veux faire quelque chose avec Python à la fin
Je veux manipuler des chaînes dans Kotlin comme Python!
Je souhaite utiliser Python dans l'environnement de pyenv + pipenv sous Windows 10
Je veux obtenir le nom du fichier, le numéro de ligne et le nom de la fonction dans Python 3.4
Dans la commande python, python pointe vers python3.8
J'ai écrit la file d'attente en Python
Je veux déboguer avec Python
J'ai écrit la pile en Python
Je veux initialiser si la valeur est vide (python)
maya Python Je veux réparer à nouveau l'animation cuite.
Je veux faire quelque chose comme sort uniq en Python
[Python] Je souhaite utiliser l'option -h avec argparse
J'ai essayé d'implémenter la fonction d'envoi de courrier en Python
Je veux connaître la nature de Python et pip
Je veux rendre le type de dictionnaire dans la liste unique
Je veux aligner les nombres valides dans le tableau Numpy
Je veux pouvoir exécuter Python avec VS Code
Je veux ajouter un joli complément à input () en python
Je veux juste trouver l'intervalle de confiance à 95% de la différence de ratio de population en Python
Je veux épingler Spyder à la barre des tâches
J'ai essayé d'implémenter PLSA en Python
Je veux sortir froidement sur la console
Je veux connaître la météo avec LINE bot avec Heroku + Python
J'ai essayé d'implémenter la permutation en Python
Je veux imprimer dans la notation d'inclusion
[Linux] Je souhaite connaître la date à laquelle l'utilisateur s'est connecté
Je veux gérer la rime part1
Ecrire le test dans la docstring python
Je veux résoudre APG4b avec Python (seulement 4.01 et 4.04 au chapitre 4)
Le 15e comment écrire un problème de référence en temps réel hors ligne en Python
Je veux gérer la rime part3
Je veux sortir le début du mois prochain avec Python
Je veux utiliser jar de python
Je veux créer un environnement Python
Je veux exécuter l'interface graphique Python au démarrage de Raspberry Pi
Je veux analyser les journaux avec Python
Développement LINEbot, je souhaite vérifier le fonctionnement dans l'environnement local
Je veux jouer avec aws avec python
[Couches Python / AWS Lambda] Je souhaite réutiliser uniquement le module dans AWS Lambda Layers
J'ai essayé d'implémenter ADALINE en Python