unittest — Marco de prueba automatizado¶
Propósito: | Marco de prueba automatizado |
---|
El módulo unittest
de Python se basa en el diseño del marco XUnit de Kent
Beck y Erich Gamma. El mismo patrón se repite en muchos otros lenguajes,
incluidos C, Perl, Java y Smalltalk. El marco implementado por unittest
admite accesorios (fixtures), conjuntos de pruebas y un corredor de pruebas
para permitir las pruebas automatizadas.
Estructura básica de las pruebas¶
Las pruebas, según lo definido por unittest
, tienen dos partes: código para
administrar las dependencias de prueba (llamadas fixtures) y la prueba en sí.
Las pruebas individuales se crean subclasificando TestCase
y anulando o
agregando los métodos apropiados. En el siguiente ejemplo, el
SimplisticTest
tiene un solo método test()
, que fallaría si a
es
diferente de b
.
import unittest
class SimplisticTest(unittest.TestCase):
def test(self):
a = 'a'
b = 'a'
self.assertEqual(a, b)
Ejecutar las pruebas¶
La forma más fácil de ejecutar pruebas de unittest es usar el descubrimiento automático disponible a través de la interfaz de línea de comandos.
$ python3 -m unittest unittest_simple.py
.
----------------------------------------------------------------
Ran 1 test in 0.000s
OK
Esta salida abreviada incluye la cantidad de tiempo que tomaron las pruebas,
junto con un indicador de estado para cada prueba (el «.» En la primera línea
de salida significa que pasó una prueba). Para obtener resultados de prueba
más detallados, incluye la opción -v
.
$ python3 -m unittest -v unittest_simple.py
test (unittest_simple.SimplisticTest) ... ok
----------------------------------------------------------------
Ran 1 test in 0.000s
OK
Resultados de las pruebas¶
Las pruebas tienen 3 resultados posibles, descritos en: tabla: Resultados del caso de prueba.
Resultado | Descripción |
---|---|
ok | La prueba pasa. |
FAIL | La prueba no pasa y genera una excepción AssertionError. |
ERROR | La prueba genera cualquier excepción que no es AssertionError. |
No hay una forma explícita de hacer que una prueba «pase», por lo que el estado de una prueba depende de la presencia (o ausencia) de una excepción.
import unittest
class OutcomesTest(unittest.TestCase):
def testPass(self):
return
def testFail(self):
self.assertFalse(True)
def testError(self):
raise RuntimeError('Test error!')
Cuando una prueba falla o genera un error, el rastreo se incluye en la salida.
$ python3 -m unittest unittest_outcomes.py
EF.
================================================================
ERROR: testError (unittest_outcomes.OutcomesTest)
----------------------------------------------------------------
Traceback (most recent call last):
File ".../unittest_outcomes.py", line 18, in testError
raise RuntimeError('Test error!')
RuntimeError: Test error!
================================================================
FAIL: testFail (unittest_outcomes.OutcomesTest)
----------------------------------------------------------------
Traceback (most recent call last):
File ".../unittest_outcomes.py", line 15, in testFail
self.assertFalse(True)
AssertionError: True is not false
----------------------------------------------------------------
Ran 3 tests in 0.001s
FAILED (failures=1, errors=1)
En el ejemplo anterior, testFail()
falla y el rastreo muestra la línea con
el código de falla. Sin embargo, depende de la persona que lee el resultado de
la prueba mirar el código para descubrir el significado de la prueba fallida.
import unittest
class FailureMessageTest(unittest.TestCase):
def testFail(self):
self.assertFalse(True, 'failure message goes here')
Para facilitar la comprensión de la naturaleza de una falla en la prueba, los
métodos fail*()
y assert*()
aceptan un argumento msg
, que se puede
usar para generar mensaje de error detallado.
$ python3 -m unittest -v unittest_failwithmessage.py
testFail (unittest_failwithmessage.FailureMessageTest) ... FAIL
================================================================
FAIL: testFail (unittest_failwithmessage.FailureMessageTest)
----------------------------------------------------------------
Traceback (most recent call last):
File ".../unittest_failwithmessage.py", line 12, in testFail
self.assertFalse(True, 'failure message goes here')
AssertionError: True is not false : failure message goes here
----------------------------------------------------------------
Ran 1 test in 0.000s
FAILED (failures=1)
Probar la verdad¶
La mayoría de las pruebas afirman la verdad de alguna condición. Hay dos formas diferentes de escribir pruebas de verificación de la verdad, según la perspectiva del autor de la prueba y el resultado deseado del código que se está probando.
import unittest
class TruthTest(unittest.TestCase):
def testAssertTrue(self):
self.assertTrue(True)
def testAssertFalse(self):
self.assertFalse(False)
Si el código produce un valor que se puede evaluar como verdadero, se debe usar
el método assertTrue()
. Si el código produce un valor falso, el método
assertFalse()
tiene más sentido.
$ python3 -m unittest -v unittest_truth.py
testAssertFalse (unittest_truth.TruthTest) ... ok
testAssertTrue (unittest_truth.TruthTest) ... ok
----------------------------------------------------------------
Ran 2 tests in 0.000s
OK
Probar la igualdad¶
Como caso especial, unittest
incluye métodos para probar la igualdad de dos
valores.
import unittest
class EqualityTest(unittest.TestCase):
def testExpectEqual(self):
self.assertEqual(1, 3 - 2)
def testExpectEqualFails(self):
self.assertEqual(2, 3 - 2)
def testExpectNotEqual(self):
self.assertNotEqual(2, 3 - 2)
def testExpectNotEqualFails(self):
self.assertNotEqual(1, 3 - 2)
Cuando fallan, estos métodos de prueba especiales producen mensajes de error que incluyen los valores que se comparan.
$ python3 -m unittest -v unittest_equality.py
testExpectEqual (unittest_equality.EqualityTest) ... ok
testExpectEqualFails (unittest_equality.EqualityTest) ... FAIL
testExpectNotEqual (unittest_equality.EqualityTest) ... ok
testExpectNotEqualFails (unittest_equality.EqualityTest) ...
FAIL
================================================================
FAIL: testExpectEqualFails (unittest_equality.EqualityTest)
----------------------------------------------------------------
Traceback (most recent call last):
File ".../unittest_equality.py", line 15, in
testExpectEqualFails
self.assertEqual(2, 3 - 2)
AssertionError: 2 != 1
================================================================
FAIL: testExpectNotEqualFails (unittest_equality.EqualityTest)
----------------------------------------------------------------
Traceback (most recent call last):
File ".../unittest_equality.py", line 21, in
testExpectNotEqualFails
self.assertNotEqual(1, 3 - 2)
AssertionError: 1 == 1
----------------------------------------------------------------
Ran 4 tests in 0.001s
FAILED (failures=2)
¿Casi igual?¶
Además de la igualdad estricta, es posible probar la casi igualdad de los
números de coma flotante usando assertAlmostEqual()
y
assertNotAlmostEqual()
.
import unittest
class AlmostEqualTest(unittest.TestCase):
def testEqual(self):
self.assertEqual(1.1, 3.3 - 2.2)
def testAlmostEqual(self):
self.assertAlmostEqual(1.1, 3.3 - 2.2, places=1)
def testNotAlmostEqual(self):
self.assertNotAlmostEqual(1.1, 3.3 - 2.0, places=1)
Los argumentos son los valores a comparar y el número de decimales que se utilizarán para la prueba.
$ python3 -m unittest unittest_almostequal.py
.F.
================================================================
FAIL: testEqual (unittest_almostequal.AlmostEqualTest)
----------------------------------------------------------------
Traceback (most recent call last):
File ".../unittest_almostequal.py", line 12, in testEqual
self.assertEqual(1.1, 3.3 - 2.2)
AssertionError: 1.1 != 1.0999999999999996
----------------------------------------------------------------
Ran 3 tests in 0.001s
FAILED (failures=1)
Contenedores¶
Además del genérico assertEqual()
y assertNotEqual()
, hay métodos
especiales para comparar contenedores como objetos list
, dict
y
set
.
import textwrap
import unittest
class ContainerEqualityTest(unittest.TestCase):
def testCount(self):
self.assertCountEqual(
[1, 2, 3, 2],
[1, 3, 2, 3],
)
def testDict(self):
self.assertDictEqual(
{'a': 1, 'b': 2},
{'a': 1, 'b': 3},
)
def testList(self):
self.assertListEqual(
[1, 2, 3],
[1, 3, 2],
)
def testMultiLineString(self):
self.assertMultiLineEqual(
textwrap.dedent("""
This string
has more than one
line.
"""),
textwrap.dedent("""
This string has
more than two
lines.
"""),
)
def testSequence(self):
self.assertSequenceEqual(
[1, 2, 3],
[1, 3, 2],
)
def testSet(self):
self.assertSetEqual(
set([1, 2, 3]),
set([1, 3, 2, 4]),
)
def testTuple(self):
self.assertTupleEqual(
(1, 'a'),
(1, 'b'),
)
Cada método informa la desigualdad utilizando un formato que es significativo para el tipo de entrada, lo que hace que las fallas de prueba sean más fáciles de entender y corregir.
$ python3 -m unittest unittest_equality_container.py
FFFFFFF
================================================================
FAIL: testCount
(unittest_equality_container.ContainerEqualityTest)
----------------------------------------------------------------
Traceback (most recent call last):
File ".../unittest_equality_container.py", line 15, in
testCount
[1, 3, 2, 3],
AssertionError: Element counts were not equal:
First has 2, Second has 1: 2
First has 1, Second has 2: 3
================================================================
FAIL: testDict
(unittest_equality_container.ContainerEqualityTest)
----------------------------------------------------------------
Traceback (most recent call last):
File ".../unittest_equality_container.py", line 21, in
testDict
{'a': 1, 'b': 3},
AssertionError: {'a': 1, 'b': 2} != {'a': 1, 'b': 3}
- {'a': 1, 'b': 2}
? ^
+ {'a': 1, 'b': 3}
? ^
================================================================
FAIL: testList
(unittest_equality_container.ContainerEqualityTest)
----------------------------------------------------------------
Traceback (most recent call last):
File ".../unittest_equality_container.py", line 27, in
testList
[1, 3, 2],
AssertionError: Lists differ: [1, 2, 3] != [1, 3, 2]
First differing element 1:
2
3
- [1, 2, 3]
+ [1, 3, 2]
================================================================
FAIL: testMultiLineString
(unittest_equality_container.ContainerEqualityTest)
----------------------------------------------------------------
Traceback (most recent call last):
File ".../unittest_equality_container.py", line 41, in
testMultiLineString
"""),
AssertionError: '\nThis string\nhas more than one\nline.\n' !=
'\nThis string has\nmore than two\nlines.\n'
- This string
+ This string has
? ++++
- has more than one
? ---- --
+ more than two
? ++
- line.
+ lines.
? +
================================================================
FAIL: testSequence
(unittest_equality_container.ContainerEqualityTest)
----------------------------------------------------------------
Traceback (most recent call last):
File ".../unittest_equality_container.py", line 47, in
testSequence
[1, 3, 2],
AssertionError: Sequences differ: [1, 2, 3] != [1, 3, 2]
First differing element 1:
2
3
- [1, 2, 3]
+ [1, 3, 2]
================================================================
FAIL: testSet
(unittest_equality_container.ContainerEqualityTest)
----------------------------------------------------------------
Traceback (most recent call last):
File ".../unittest_equality_container.py", line 53, in testSet
set([1, 3, 2, 4]),
AssertionError: Items in the second set but not the first:
4
================================================================
FAIL: testTuple
(unittest_equality_container.ContainerEqualityTest)
----------------------------------------------------------------
Traceback (most recent call last):
File ".../unittest_equality_container.py", line 59, in
testTuple
(1, 'b'),
AssertionError: Tuples differ: (1, 'a') != (1, 'b')
First differing element 1:
'a'
'b'
- (1, 'a')
? ^
+ (1, 'b')
? ^
----------------------------------------------------------------
Ran 7 tests in 0.005s
FAILED (failures=7)
Use assertIn()
para probar la membresía del contenedor.
import unittest
class ContainerMembershipTest(unittest.TestCase):
def testDict(self):
self.assertIn(4, {1: 'a', 2: 'b', 3: 'c'})
def testList(self):
self.assertIn(4, [1, 2, 3])
def testSet(self):
self.assertIn(4, set([1, 2, 3]))
Cualquier objeto que admita el operador in
o la interfaz de programación
del contenedor se puede usar con assertIn()
.
$ python3 -m unittest unittest_in.py
FFF
================================================================
FAIL: testDict (unittest_in.ContainerMembershipTest)
----------------------------------------------------------------
Traceback (most recent call last):
File ".../unittest_in.py", line 12, in testDict
self.assertIn(4, {1: 'a', 2: 'b', 3: 'c'})
AssertionError: 4 not found in {1: 'a', 2: 'b', 3: 'c'}
================================================================
FAIL: testList (unittest_in.ContainerMembershipTest)
----------------------------------------------------------------
Traceback (most recent call last):
File ".../unittest_in.py", line 15, in testList
self.assertIn(4, [1, 2, 3])
AssertionError: 4 not found in [1, 2, 3]
================================================================
FAIL: testSet (unittest_in.ContainerMembershipTest)
----------------------------------------------------------------
Traceback (most recent call last):
File ".../unittest_in.py", line 18, in testSet
self.assertIn(4, set([1, 2, 3]))
AssertionError: 4 not found in {1, 2, 3}
----------------------------------------------------------------
Ran 3 tests in 0.001s
FAILED (failures=3)
Probar las excepciones¶
Como se mencionó anteriormente, si una prueba genera una excepción distinta de
AssertionError
, se trata como un error. Esto es muy útil para descubrir
errores al modificar el código que tiene cobertura de prueba existente. Sin
embargo, hay circunstancias en las que la prueba debe verificar que algún
código produce una excepción. Por ejemplo, si se da un valor no válido a un
atributo de un objeto. En tales casos, assertRaises()
hace que el código
sea más claro que atrapar la excepción en la prueba. Compara estas dos
pruebas:
import unittest
def raises_error(*args, **kwds):
raise ValueError('Invalid value: ' + str(args) + str(kwds))
class ExceptionTest(unittest.TestCase):
def testTrapLocally(self):
try:
raises_error('a', b='c')
except ValueError:
pass
else:
self.fail('Did not see ValueError')
def testAssertRaises(self):
self.assertRaises(
ValueError,
raises_error,
'a',
b='c',
)
Los resultados para ambos son los mismos, pero la segunda prueba que usa
assertRaises()
es más sucinta.
$ python3 -m unittest -v unittest_exception.py
testAssertRaises (unittest_exception.ExceptionTest) ... ok
testTrapLocally (unittest_exception.ExceptionTest) ... ok
----------------------------------------------------------------
Ran 2 tests in 0.000s
OK
Accesorios de las pruebas¶
Los accesorios (fixtures) son recursos externos necesarios para una prueba. Por ejemplo, las pruebas para una clase pueden necesitar una instancia de otra clase que proporcione ajustes de configuración u otro recurso compartido. Otros accesorios de prueba incluyen conexiones de bases de datos y archivos temporales (muchas personas argumentan que el uso de recursos externos hace que tales pruebas no sean pruebas «unitarias», pero siguen siendo pruebas y aún son útiles).
unittest
incluye ganchos especiales para configurar y limpiar cualquier
accesorio necesario para las pruebas. Para establecer dispositivos para cada
caso de prueba individual, anula setUp()
en el TestCase
. Para
limpiarlos, anula tearDown()
. Para administrar un conjunto de dispositivos
para todas las instancias de una clase de prueba, anula los métodos de clase
setUpClass()
y tearDownClass()
para el TestCase
. Y para manejar
operaciones de configuración especialmente costosas para todas las pruebas
dentro de un módulo, usa las funciones de nivel de módulo setUpModule()
y
tearDownModule()
.
import random
import unittest
def setUpModule():
print('In setUpModule()')
def tearDownModule():
print('In tearDownModule()')
class FixturesTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
print('In setUpClass()')
cls.good_range = range(1, 10)
@classmethod
def tearDownClass(cls):
print('In tearDownClass()')
del cls.good_range
def setUp(self):
super().setUp()
print('\nIn setUp()')
# Pick a number sure to be in the range. The range is
# defined as not including the "stop" value, so make
# sure it is not included in the set of allowed values
# for our choice.
self.value = random.randint(
self.good_range.start,
self.good_range.stop - 1,
)
def tearDown(self):
print('In tearDown()')
del self.value
super().tearDown()
def test1(self):
print('In test1()')
self.assertIn(self.value, self.good_range)
def test2(self):
print('In test2()')
self.assertIn(self.value, self.good_range)
Cuando se ejecuta esta prueba de muestra, el orden de ejecución del dispositivo y los métodos de prueba son evidentes.
$ python3 -u -m unittest -v unittest_fixtures.py
In setUpModule()
In setUpClass()
test1 (unittest_fixtures.FixturesTest) ...
In setUp()
In test1()
In tearDown()
ok
test2 (unittest_fixtures.FixturesTest) ...
In setUp()
In test2()
In tearDown()
ok
In tearDownClass()
In tearDownModule()
----------------------------------------------------------------
Ran 2 tests in 0.000s
OK
Es posible que no se invoquen todos los métodos tearDown
si hay errores en
el proceso de limpieza de los accesorios. Para asegurarte de que un
dispositivo se libere siempre correctamente, usa addCleanup()
.
import random
import shutil
import tempfile
import unittest
def remove_tmpdir(dirname):
print('In remove_tmpdir()')
shutil.rmtree(dirname)
class FixturesTest(unittest.TestCase):
def setUp(self):
super().setUp()
self.tmpdir = tempfile.mkdtemp()
self.addCleanup(remove_tmpdir, self.tmpdir)
def test1(self):
print('\nIn test1()')
def test2(self):
print('\nIn test2()')
Esta prueba de ejemplo crea un directorio temporal y luego usa shutil
para limpiarlo cuando se realiza la prueba.
$ python3 -u -m unittest -v unittest_addcleanup.py
test1 (unittest_addcleanup.FixturesTest) ...
In test1()
In remove_tmpdir()
ok
test2 (unittest_addcleanup.FixturesTest) ...
In test2()
In remove_tmpdir()
ok
----------------------------------------------------------------
Ran 2 tests in 0.002s
OK
Repetir pruebas con diferentes entradas¶
Con frecuencia es útil ejecutar la misma lógica de prueba con diferentes
entradas. En lugar de definir un método de prueba separado para cada caso
pequeño, una forma común de hacerlo es usar un método de prueba que contenga
varias afirmaciones relacionadas. El problema con este enfoque es que tan
pronto como una afirmación falla, el resto se omite. Una mejor solución es
usar subTest()
para crear un contexto para una prueba dentro de un método
de prueba. Si la prueba falla, se informa la falla y las pruebas restantes
continúan.
import unittest
class SubTest(unittest.TestCase):
def test_combined(self):
self.assertRegex('abc', 'a')
self.assertRegex('abc', 'B')
# The next assertions are not verified!
self.assertRegex('abc', 'c')
self.assertRegex('abc', 'd')
def test_with_subtest(self):
for pat in ['a', 'B', 'c', 'd']:
with self.subTest(pattern=pat):
self.assertRegex('abc', pat)
En este ejemplo, el método test_combined()
nunca ejecuta las afirmaciones
para los patrones 'c'
y 'd'
. El método test_with_subtest()
lo
hace, e informa correctamente la falla adicional. Ten en cuenta que el
corredor de prueba todavía considera que solo hay dos casos de prueba, a pesar
de que hay tres fallas informadas.
$ python3 -m unittest -v unittest_subtest.py
test_combined (unittest_subtest.SubTest) ... FAIL
test_with_subtest (unittest_subtest.SubTest) ...
================================================================
FAIL: test_combined (unittest_subtest.SubTest)
----------------------------------------------------------------
Traceback (most recent call last):
File ".../unittest_subtest.py", line 13, in test_combined
self.assertRegex('abc', 'B')
AssertionError: Regex didn't match: 'B' not found in 'abc'
================================================================
FAIL: test_with_subtest (unittest_subtest.SubTest) (pattern='B')
----------------------------------------------------------------
Traceback (most recent call last):
File ".../unittest_subtest.py", line 21, in test_with_subtest
self.assertRegex('abc', pat)
AssertionError: Regex didn't match: 'B' not found in 'abc'
================================================================
FAIL: test_with_subtest (unittest_subtest.SubTest) (pattern='d')
----------------------------------------------------------------
Traceback (most recent call last):
File ".../unittest_subtest.py", line 21, in test_with_subtest
self.assertRegex('abc', pat)
AssertionError: Regex didn't match: 'd' not found in 'abc'
----------------------------------------------------------------
Ran 2 tests in 0.001s
FAILED (failures=3)
Omitir pruebas¶
Con frecuencia es útil poder omitir una prueba si no se cumple alguna condición
externa. Por ejemplo, al escribir pruebas para verificar el comportamiento de
una biblioteca en una versión específica de Python, no hay razón para ejecutar
esas pruebas en otras versiones de Python. Las clases y métodos de prueba se
pueden decorar con skip()
para omitir siempre las pruebas. Los decoradores
skipIf()
y skipUnless()
se pueden usar para verificar una condición
antes de omitir.
import sys
import unittest
class SkippingTest(unittest.TestCase):
@unittest.skip('always skipped')
def test(self):
self.assertTrue(False)
@unittest.skipIf(sys.version_info[0] > 2,
'only runs on python 2')
def test_python2_only(self):
self.assertTrue(False)
@unittest.skipUnless(sys.platform == 'Darwin',
'only runs on macOS')
def test_macos_only(self):
self.assertTrue(True)
def test_raise_skiptest(self):
raise unittest.SkipTest('skipping via exception')
Para condiciones complejas que son difíciles de expresar en una sola expresión
para pasar a skipIf()
o skipUnless()
, un caso de prueba puede generar
SkipTest
directamente para hacer que la prueba se omita .
$ python3 -m unittest -v unittest_skip.py
test (unittest_skip.SkippingTest) ... skipped 'always skipped'
test_macos_only (unittest_skip.SkippingTest) ... skipped 'only
runs on macOS'
test_python2_only (unittest_skip.SkippingTest) ... skipped 'only
runs on python 2'
test_raise_skiptest (unittest_skip.SkippingTest) ... skipped
'skipping via exception'
----------------------------------------------------------------
Ran 4 tests in 0.000s
OK (skipped=4)
Ignorar las pruebas fallidas¶
En lugar de eliminar las pruebas que se rompen de manera persistente, se pueden
marcar con el decorador expectedFailure()
para que se ignore la falla.
import unittest
class Test(unittest.TestCase):
@unittest.expectedFailure
def test_never_passes(self):
self.assertTrue(False)
@unittest.expectedFailure
def test_always_passes(self):
self.assertTrue(True)
Si de hecho se supera una prueba que se espera que falle, esa condición se trata como un tipo especial de falla y se informa como un «éxito inesperado».
$ python3 -m unittest -v unittest_expectedfailure.py
test_always_passes (unittest_expectedfailure.Test) ...
unexpected success
test_never_passes (unittest_expectedfailure.Test) ... expected
failure
----------------------------------------------------------------
Ran 2 tests in 0.001s
FAILED (expected failures=1, unexpected successes=1)
Ver también
- Documentación de la biblioteca estándar para unittest
doctest
– Un medio alternativo para ejecutar pruebas incrustadas en cadenas de documentos o archivos de documentación externos.- nose – Corredor de pruebas de terceros con funciones de descubrimiento sofisticadas.
- pytest – Un popular corredor de pruebas de terceros con soporte para ejecución distribuida y un sistema de administración de accesorios alternativo.
- testrepository – Corredor de prueba de terceros utilizado por el proyecto OpenStack, con soporte para ejecución paralela y seguimiento fallas de seguimiento.