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:

  1. 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.
  2. 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


Modified text is an extract of the original Stack Overflow Documentation
Sous licence CC BY-SA 3.0
Non affilié à Stack Overflow