Python Language
Test d'unité
Recherche…
Remarques
Il existe plusieurs outils de test unitaire pour Python. Cette rubrique de documentation décrit le module unittest
base. Les autres outils de test incluent py.test
et nosetests
. Cette documentation python sur les tests compare plusieurs de ces outils sans entrer dans le détail.
Tester les exceptions
Les programmes lancent des erreurs lorsque, par exemple, des entrées incorrectes sont données. Pour cette raison, il faut s'assurer qu'une erreur est générée lorsque de fausses entrées sont fournies. Pour cette raison, nous devons vérifier l'existence d'une exception exacte. Dans cet exemple, nous utiliserons l'exception suivante:
class WrongInputException(Exception):
pass
Cette exception est déclenchée lorsque des entrées incorrectes sont données, dans le contexte suivant, où nous attendons toujours un nombre comme entrée de texte.
def convert2number(random_input):
try:
my_input = int(random_input)
except ValueError:
raise WrongInputException("Expected an integer!")
return my_input
Pour vérifier si une exception a été assertRaises
, nous utilisons assertRaises
pour vérifier cette exception. assertRaises
peut être utilisé de deux manières:
- Utilisation de l'appel de la fonction régulière. Le premier argument prend le type d'exception, le second un appelable (généralement une fonction) et le reste des arguments est transmis à cet appelable.
- En utilisant une clause
with
, en ne donnant que le type d'exception à la fonction. Cela a pour avantage que plus de code peut être exécuté, mais doit être utilisé avec précaution car plusieurs fonctions peuvent utiliser la même exception, ce qui peut être problématique. Un exemple: avec self.assertRaises (WrongInputException): convert2number ("pas un nombre")
Ce premier a été implémenté dans le cas de test suivant:
import unittest
class ExceptionTestCase(unittest.TestCase):
def test_wrong_input_string(self):
self.assertRaises(WrongInputException, convert2number, "not a number")
def test_correct_input(self):
try:
result = convert2number("56")
self.assertIsInstance(result, int)
except WrongInputException:
self.fail()
Il peut également être nécessaire de vérifier une exception qui n'aurait pas dû être lancée. Cependant, un test échouera automatiquement lorsqu'une exception est lancée et peut ne pas être nécessaire du tout. Juste pour montrer les options, la seconde méthode de test montre comment vérifier qu'une exception ne peut pas être lancée. Fondamentalement, cela intercepte l'exception et échoue ensuite le test en utilisant la méthode fail
.
Fonctions moqueuses avec unittest.mock.create_autospec
Une façon de simuler une fonction consiste à utiliser la fonction create_autospec
, qui va create_autospec
un objet en fonction de ses spécifications. Avec les fonctions, nous pouvons l'utiliser pour nous assurer qu'elles sont correctement appelées.
Avec une fonction multiply
dans custom_math.py
:
def multiply(a, b):
return a * b
Et une fonction multiples_of
dans process_math.py
:
from custom_math import multiply
def multiples_of(integer, *args, num_multiples=0, **kwargs):
"""
:rtype: list
"""
multiples = []
for x in range(1, num_multiples + 1):
"""
Passing in args and kwargs here will only raise TypeError if values were
passed to multiples_of function, otherwise they are ignored. This way we can
test that multiples_of is used correctly. This is here for an illustration
of how create_autospec works. Not recommended for production code.
"""
multiple = multiply(integer,x, *args, **kwargs)
multiples.append(multiple)
return multiples
On peut tester les multiples_of
seul en se moquant de la multiply
. L'exemple ci-dessous utilise la bibliothèque standard Python unittest, mais elle peut également être utilisée avec d'autres frameworks de test, comme pytest ou nose:
from unittest.mock import create_autospec
import unittest
# we import the entire module so we can mock out multiply
import custom_math
custom_math.multiply = create_autospec(custom_math.multiply)
from process_math import multiples_of
class TestCustomMath(unittest.TestCase):
def test_multiples_of(self):
multiples = multiples_of(3, num_multiples=1)
custom_math.multiply.assert_called_with(3, 1)
def test_multiples_of_with_bad_inputs(self):
with self.assertRaises(TypeError) as e:
multiples_of(1, "extra arg", num_multiples=1) # this should raise a TypeError
Tester la configuration et le démontage dans un fichier unestest.TestCase
Parfois, nous voulons préparer un contexte pour chaque test à exécuter. La méthode setUp
est exécutée avant chaque test de la classe. tearDown
est exécuté à la fin de chaque test. Ces méthodes sont facultatives. Rappelez-vous que les TestCases sont souvent utilisés dans l'héritage multiple coopératif, vous devez donc toujours faire appel à super
dans ces méthodes pour que les méthodes setUp
et tearDown
la classe de base setUp
également appelées. L'implémentation de base de TestCase
fournit des méthodes setUp
et tearDown
vides afin qu'elles puissent être appelées sans générer d'exceptions:
import unittest
class SomeTest(unittest.TestCase):
def setUp(self):
super(SomeTest, self).setUp()
self.mock_data = [1,2,3,4,5]
def test(self):
self.assertEqual(len(self.mock_data), 5)
def tearDown(self):
super(SomeTest, self).tearDown()
self.mock_data = []
if __name__ == '__main__':
unittest.main()
Notez que dans python2.7 +, il existe également la méthode addCleanup
qui enregistre les fonctions à appeler après l'exécution du test. Contrairement à tearDown
qui n'est appelé que si setUp
réussit, les fonctions enregistrées via addCleanup
seront appelées même en cas d'exception non setUp
dans setUp
. À titre d’exemple concret, cette méthode peut souvent être vue en train de supprimer divers objets enregistrés lors de l’exécution du test:
import unittest
import some_module
class SomeOtherTest(unittest.TestCase):
def setUp(self):
super(SomeOtherTest, self).setUp()
# Replace `some_module.method` with a `mock.Mock`
my_patch = mock.patch.object(some_module, 'method')
my_patch.start()
# When the test finishes running, put the original method back.
self.addCleanup(my_patch.stop)
Un autre avantage de l'enregistrement des nettoyages de cette manière est qu'il permet au programmeur de placer le code de nettoyage à côté du code de configuration et qu'il vous protège si un sous-programme oublie d'appeler super
in tearDown
.
Affirmer des exceptions
Vous pouvez tester qu'une fonction émet une exception avec le Unittest intégré via deux méthodes différentes.
Utiliser un gestionnaire de contexte
def division_function(dividend, divisor):
return dividend / divisor
class MyTestCase(unittest.TestCase):
def test_using_context_manager(self):
with self.assertRaises(ZeroDivisionError):
x = division_function(1, 0)
Cela exécutera le code à l'intérieur du gestionnaire de contexte et, s'il réussit, le test échouera car l'exception n'a pas été déclenchée. Si le code déclenche une exception du type correct, le test continuera.
Vous pouvez également obtenir le contenu de l'exception déclenchée si vous souhaitez exécuter des assertions supplémentaires.
class MyTestCase(unittest.TestCase):
def test_using_context_manager(self):
with self.assertRaises(ZeroDivisionError) as ex:
x = division_function(1, 0)
self.assertEqual(ex.message, 'integer division or modulo by zero')
En fournissant une fonction appelable
def division_function(dividend, divisor):
"""
Dividing two numbers.
:type dividend: int
:type divisor: int
:raises: ZeroDivisionError if divisor is zero (0).
:rtype: int
"""
return dividend / divisor
class MyTestCase(unittest.TestCase):
def test_passing_function(self):
self.assertRaises(ZeroDivisionError, division_function, 1, 0)
L'exception à vérifier doit être le premier paramètre et une fonction appelable doit être transmise en tant que second paramètre. Tous les autres paramètres spécifiés seront transmis directement à la fonction appelée, vous permettant de spécifier les paramètres qui déclenchent l'exception.
Choisir des assertions au sein des inattaquables
Bien que Python ait une déclaration assert
, l'infrastructure de test unitaire Python possède de meilleures assertions spécialisées pour les tests: ils sont plus informatifs sur les échecs et ne dépendent pas du mode de débogage de l'exécution.
L'assertion la plus simple est peut-être assertTrue
, qui peut être utilisée comme ceci:
import unittest
class SimplisticTest(unittest.TestCase):
def test_basic(self):
self.assertTrue(1 + 1 == 2)
Cela se passera bien, mais en remplaçant la ligne ci-dessus par
self.assertTrue(1 + 1 == 3)
va échouer.
L'assertion assertTrue
est probablement l'assertion la plus générale, car tout élément testé peut être considéré comme une condition booléenne, mais il existe souvent de meilleures alternatives. En testant l'égalité, comme ci-dessus, il est préférable d'écrire
self.assertEqual(1 + 1, 3)
Lorsque le premier échoue, le message est
======================================================================
FAIL: test (__main__.TruthTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "stuff.py", line 6, in test
self.assertTrue(1 + 1 == 3)
AssertionError: False is not true
mais lorsque ce dernier échoue, le message est
======================================================================
FAIL: test (__main__.TruthTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "stuff.py", line 6, in test
self.assertEqual(1 + 1, 3)
AssertionError: 2 != 3
ce qui est plus informatif (il a en fait évalué le résultat du côté gauche).
Vous pouvez trouver la liste des assertions dans la documentation standard . En général, il est judicieux de choisir l'assertion la plus adaptée à la situation. Ainsi, comme indiqué ci-dessus, pour affirmer que 1 + 1 == 2
il est préférable d'utiliser assertEqual
que assertTrue
. De même, pour affirmer que a is None
, il est préférable d'utiliser assertIsNone
que assertEqual
.
Notez également que les assertions ont des formes négatives. Ainsi assertEqual
a son homologue négatif assertNotEqual
et assertIsNone
a son homologue négatif assertIsNotNone
. Encore une fois, en utilisant les contreparties négatives, le cas échéant, cela conduira à des messages d'erreur plus clairs.
Tests unitaires avec le pytest
installation de pytest:
pip install pytest
préparer les tests:
mkdir tests
touch tests/test_docker.py
Fonctions à tester dans docker_something/helpers.py
:
from subprocess import Popen, PIPE
# this Popen is monkeypatched with the fixture `all_popens`
def copy_file_to_docker(src, dest):
try:
result = Popen(['docker','cp', src, 'something_cont:{}'.format(dest)], stdout=PIPE, stderr=PIPE)
err = result.stderr.read()
if err:
raise Exception(err)
except Exception as e:
print(e)
return result
def docker_exec_something(something_file_string):
fl = Popen(["docker", "exec", "-i", "something_cont", "something"], stdin=PIPE, stdout=PIPE, stderr=PIPE)
fl.stdin.write(something_file_string)
fl.stdin.close()
err = fl.stderr.read()
fl.stderr.close()
if err:
print(err)
exit()
result = fl.stdout.read()
print(result)
Le test importe test_docker.py
:
import os
from tempfile import NamedTemporaryFile
import pytest
from subprocess import Popen, PIPE
from docker_something import helpers
copy_file_to_docker = helpers.copy_file_to_docker
docker_exec_something = helpers.docker_exec_something
se moquer d'un fichier comme objet dans test_docker.py
:
class MockBytes():
'''Used to collect bytes
'''
all_read = []
all_write = []
all_close = []
def read(self, *args, **kwargs):
# print('read', args, kwargs, dir(self))
self.all_read.append((self, args, kwargs))
def write(self, *args, **kwargs):
# print('wrote', args, kwargs)
self.all_write.append((self, args, kwargs))
def close(self, *args, **kwargs):
# print('closed', self, args, kwargs)
self.all_close.append((self, args, kwargs))
def get_all_mock_bytes(self):
return self.all_read, self.all_write, self.all_close
Patch de singe avec pytest dans test_docker.py
:
@pytest.fixture
def all_popens(monkeypatch):
'''This fixture overrides / mocks the builtin Popen
and replaces stdin, stdout, stderr with a MockBytes object
note: monkeypatch is magically imported
'''
all_popens = []
class MockPopen(object):
def __init__(self, args, stdout=None, stdin=None, stderr=None):
all_popens.append(self)
self.args = args
self.byte_collection = MockBytes()
self.stdin = self.byte_collection
self.stdout = self.byte_collection
self.stderr = self.byte_collection
pass
monkeypatch.setattr(helpers, 'Popen', MockPopen)
return all_popens
Les exemples de tests doivent commencer par le préfixe test_
dans le fichier test_docker.py
:
def test_docker_install():
p = Popen(['which', 'docker'], stdout=PIPE, stderr=PIPE)
result = p.stdout.read()
assert 'bin/docker' in result
def test_copy_file_to_docker(all_popens):
result = copy_file_to_docker('asdf', 'asdf')
collected_popen = all_popens.pop()
mock_read, mock_write, mock_close = collected_popen.byte_collection.get_all_mock_bytes()
assert mock_read
assert result.args == ['docker', 'cp', 'asdf', 'something_cont:asdf']
def test_docker_exec_something(all_popens):
docker_exec_something(something_file_string)
collected_popen = all_popens.pop()
mock_read, mock_write, mock_close = collected_popen.byte_collection.get_all_mock_bytes()
assert len(mock_read) == 3
something_template_stdin = mock_write[0][1][0]
these = [os.environ['USER'], os.environ['password_prod'], 'table_name_here', 'test_vdm', 'col_a', 'col_b', '/tmp/test.tsv']
assert all([x in something_template_stdin for x in these])
exécuter les tests un par un:
py.test -k test_docker_install tests
py.test -k test_copy_file_to_docker tests
py.test -k test_docker_exec_something tests
exécuter tous les tests dans le dossier de tests
:
py.test -k test_ tests