Buscar..


Observaciones

Hay varias herramientas de prueba de unidad para Python. Este tema de documentación describe el módulo de unittest básico. Otras herramientas de prueba incluyen py.test y nosetests . Esta documentación de python sobre pruebas compara varias de estas herramientas sin profundizar.

Pruebas de excepciones

Los programas lanzan errores cuando, por ejemplo, se da una entrada incorrecta. Debido a esto, uno debe asegurarse de que se produce un error cuando se da una entrada incorrecta real. Por eso necesitamos verificar una excepción exacta, para este ejemplo usaremos la siguiente excepción:

class WrongInputException(Exception):
    pass

Esta excepción se genera cuando se proporciona una entrada incorrecta, en el siguiente contexto donde siempre esperamos un número como entrada de texto.

def convert2number(random_input):
    try:
        my_input = int(random_input)
    except ValueError:
        raise WrongInputException("Expected an integer!")
    return my_input

Para verificar si se ha generado una excepción, usamos assertRaises para verificar esa excepción. assertRaises se puede utilizar de dos maneras:

  1. Usando la llamada de función regular. El primer argumento toma el tipo de excepción, el segundo un llamable (generalmente una función) y el resto de los argumentos se pasan a este llamable.
  2. Usar una cláusula with , dando solo el tipo de excepción a la función. Esto tiene la ventaja de que se puede ejecutar más código, pero se debe usar con cuidado ya que múltiples funciones pueden usar la misma excepción, que puede ser problemática. Un ejemplo: con self.assertRaises (WrongInputException): convert2number ("no es un número")

Este primero se ha implementado en el siguiente caso de prueba:

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()

También puede ser necesario verificar si hay una excepción que no debería haberse lanzado. Sin embargo, una prueba fallará automáticamente cuando se lance una excepción y, por lo tanto, puede que no sea necesaria en absoluto. Solo para mostrar las opciones, el segundo método de prueba muestra un caso sobre cómo se puede verificar que no se produzca una excepción. Básicamente, esto es capturar la excepción y luego fallar la prueba usando el método de fail .

Funciones de simulación con unittest.mock.create_autospec

Una forma de simular una función es utilizar la función create_autospec , que create_autospec un objeto de acuerdo con sus especificaciones. Con las funciones, podemos usar esto para asegurarnos de que se llamen adecuadamente.

Con una función multiply en custom_math.py :

def multiply(a, b):
    return a * b

Y una función multiples_of en 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

Podemos probar multiples_of solo burlándose de multiply . El siguiente ejemplo utiliza la prueba de unidad de la biblioteca estándar de Python, pero esto también se puede usar con otros marcos de prueba, como pytest o 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

Configuración de prueba y desmontaje dentro de un unittest.TestCase

A veces queremos preparar un contexto para cada prueba que se ejecutará. El método de setUp se ejecuta antes de cada prueba en la clase. tearDown se ejecuta al final de cada prueba. Estos métodos son opcionales. Recuerde que los TestCases a menudo se usan en herencia múltiple cooperativa, por lo que debe tener cuidado de llamar siempre super en estos métodos para que también se tearDown métodos setUp y tearDown la clase base. La implementación básica de TestCase proporciona TestCase vacíos de setUp y tearDown para que puedan llamarse sin generar excepciones:

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()

Tenga en cuenta que en python2.7 +, también existe el método addCleanup que registra las funciones que deben llamarse después de ejecutar la prueba. A diferencia de tearDown que solo se llama si setUp tiene éxito, las funciones registradas a través de addCleanup se addCleanup incluso en el caso de una excepción no controlada en setUp . Como ejemplo concreto, con frecuencia se puede ver este método eliminando varios simulacros que se registraron mientras se ejecutaba la prueba:

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)

Otra ventaja de registrar las limpiezas de esta manera es que le permite al programador colocar el código de limpieza al lado del código de configuración y lo protege en caso de que un subclase olvide llamar super en tearDown .

Afirmación de excepciones

Puede probar que una función produce una excepción con la prueba de unidad incorporada a través de dos métodos diferentes.

Usando un administrador de contexto

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)

Esto ejecutará el código dentro del administrador de contexto y, si tiene éxito, fallará la prueba porque no se generó la excepción. Si el código genera una excepción del tipo correcto, la prueba continuará.

También puede obtener el contenido de la excepción generada si desea ejecutar aserciones adicionales en su contra.

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

Al proporcionar una función de llamada

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)

La excepción para verificar debe ser el primer parámetro, y una función que se puede llamar debe pasarse como el segundo parámetro. Cualquier otro parámetro especificado se pasará directamente a la función que se está llamando, lo que le permite especificar los parámetros que activan la excepción.

Eligiendo aserciones dentro de unitests

Mientras Python tiene una assert comunicado , el marco de la unidad de pruebas Python tiene mejores afirmaciones especializados para las pruebas: son más informativos en los fracasos, y no dependen del modo de depuración de la ejecución.

Quizás la aserción más simple es assertTrue , que se puede usar así:

import unittest

class SimplisticTest(unittest.TestCase):
    def test_basic(self):
        self.assertTrue(1 + 1 == 2)

Esto funcionará bien, pero reemplazando la línea anterior con

        self.assertTrue(1 + 1 == 3)

fallará.

La afirmación assertTrue es bastante probable que sea la afirmación más general, ya que cualquier cosa probada puede assertTrue como una condición booleana, pero a menudo hay mejores alternativas. Cuando se prueba la igualdad, como arriba, es mejor escribir

        self.assertEqual(1 + 1, 3)

Cuando el primero falla, el mensaje es

======================================================================

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

pero cuando este último falla, el mensaje es

======================================================================

FAIL: test (__main__.TruthTest)

----------------------------------------------------------------------

Traceback (most recent call last):

  File "stuff.py", line 6, in test

    self.assertEqual(1 + 1, 3)
AssertionError: 2 != 3

que es más informativo (en realidad evaluó el resultado del lado izquierdo).

Puede encontrar la lista de afirmaciones en la documentación estándar . En general, es una buena idea elegir la afirmación que se ajuste más específicamente a la condición. Por lo tanto, como se muestra arriba, para afirmar que 1 + 1 == 2 es mejor usar assertEqual que assertTrue . De manera similar, para afirmar que a is None , es mejor usar assertIsNone que assertEqual .

Tenga en cuenta también que las afirmaciones tienen formas negativas. Por assertEqual tanto, assertEqual tiene su contraparte negativa assertNotEqual , y assertIsNone tiene su contraparte negativa assertIsNotNone . Una vez más, usar las contrapartes negativas cuando sea apropiado, dará lugar a mensajes de error más claros.

Pruebas unitarias con pytest

instalando pytest:

pip install pytest

preparando las pruebas:

mkdir tests
touch tests/test_docker.py

Funciones para probar en 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)

La prueba importa 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

burlándose de un archivo como objeto en 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

Parches de mono con pytest en 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

Ejemplos de pruebas, deben comenzar con el prefijo test_ en el archivo 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])

ejecutando las pruebas una a la vez:

py.test -k test_docker_install tests
py.test -k test_copy_file_to_docker tests
py.test -k test_docker_exec_something tests

ejecutando todas las pruebas en la carpeta de tests :

py.test -k test_ tests


Modified text is an extract of the original Stack Overflow Documentation
Licenciado bajo CC BY-SA 3.0
No afiliado a Stack Overflow