Python Language
Examen de la unidad
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:
- 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.
- 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