() translation by (you can also view the original English article)
Las pruebas son la base del sólido desarrollo de software. Hay muchos tipos de pruebas, pero el tipo más importante es la prueba unitaria. La prueba unitaria te da mucha confianza de que puedes usar piezas bien probadas como primitivas y confiar en ellas cuando las compongas para crear tu programa. Aumentan tu inventario de código confiable más allá de tu lenguaje y la biblioteca estándar. Además, Python proporciona un gran soporte para escribir pruebas unitarias.
Ejemplo de ejecución
Antes de sumergirte en todos los principios, heurísticas y directrices, veamos una prueba unitaria representativa en acción. La clase SelfDrivingCar
es una implementación parcial de la lógica de conducción de un automóvil autodirigido. Se trata principalmente de controlar la velocidad del coche. Es consciente de los objetos frente a él, el límite de velocidad, y si llegó o no a su destino.
1 |
class SelfDrivingCar(object): |
2 |
|
3 |
def __init__(self): |
4 |
|
5 |
self.speed = 0 |
6 |
|
7 |
self.destination = None |
8 |
|
9 |
|
10 |
|
11 |
def _accelerate(self): |
12 |
|
13 |
self.speed += 1 |
14 |
|
15 |
|
16 |
|
17 |
def _decelerate(self): |
18 |
|
19 |
if self.speed > 0: |
20 |
|
21 |
self.speed -= 1 |
22 |
|
23 |
|
24 |
|
25 |
def _advance_to_destination(self): |
26 |
|
27 |
distance = self._calculate_distance_to_object_in_front() |
28 |
|
29 |
if distance < 10: |
30 |
|
31 |
self.stop() |
32 |
|
33 |
|
34 |
|
35 |
elif distance < self.speed / 2: |
36 |
|
37 |
self._decelerate() |
38 |
|
39 |
elif self.speed < self._get_speed_limit(): |
40 |
|
41 |
self._accelerate() |
42 |
|
43 |
|
44 |
|
45 |
def _has_arrived(self): |
46 |
|
47 |
pass
|
48 |
|
49 |
|
50 |
|
51 |
def _calculate_distance_to_object_in_front(self): |
52 |
|
53 |
pass
|
54 |
|
55 |
|
56 |
|
57 |
def _get_speed_limit(self): |
58 |
|
59 |
pass
|
60 |
|
61 |
|
62 |
|
63 |
def stop(self): |
64 |
|
65 |
self.speed = 0 |
66 |
|
67 |
|
68 |
|
69 |
def drive(self, destination): |
70 |
|
71 |
self.destination = destination |
72 |
|
73 |
while not self._has_arrived(): |
74 |
|
75 |
self._advance_to_destination() |
76 |
|
77 |
|
78 |
self.stop() |
79 |
|
80 |
def __init__(self): |
81 |
|
82 |
self.speed = 0 |
83 |
|
84 |
self.destination = None |
85 |
|
86 |
|
87 |
|
88 |
def _accelerate(self): |
89 |
|
90 |
self.speed += 1 |
91 |
|
92 |
|
93 |
|
94 |
def _decelerate(self): |
95 |
|
96 |
if self.speed > 0: |
97 |
|
98 |
self.speed -= 1 |
99 |
|
100 |
|
101 |
|
102 |
def _advance_to_destination(self): |
103 |
|
104 |
distance = self._calculate_distance_to_object_in_front() |
105 |
|
106 |
if distance < 10: |
107 |
|
108 |
self.stop() |
109 |
|
110 |
|
111 |
|
112 |
elif distance < self.speed / 2: |
113 |
|
114 |
self._decelerate() |
115 |
|
116 |
elif self.speed < self._get_speed_limit(): |
117 |
|
118 |
self._accelerate() |
119 |
|
120 |
|
121 |
|
122 |
def _has_arrived(self): |
123 |
|
124 |
pass
|
125 |
|
126 |
|
127 |
|
128 |
def _calculate_distance_to_object_in_front(self): |
129 |
|
130 |
pass
|
131 |
|
132 |
|
133 |
|
134 |
def _get_speed_limit(self): |
135 |
|
136 |
pass
|
137 |
|
138 |
|
139 |
|
140 |
def stop(self): |
141 |
|
142 |
self.speed = 0 |
143 |
|
144 |
|
145 |
|
146 |
def drive(self, destination): |
147 |
|
148 |
self.destination = destination |
149 |
|
150 |
while not self._has_arrived(): |
151 |
|
152 |
self._advance_to_destination() |
153 |
|
154 |
self.stop() |
155 |
Aquí hay una prueba unitaria para el método stop()
y así abrirte el apetito. Entraré en los detalles más tarde.
1 |
from unittest import TestCase |
2 |
|
3 |
|
4 |
|
5 |
class SelfDrivingCarTest(TestCase): |
6 |
|
7 |
def setUp(self): |
8 |
|
9 |
self.car = SelfDrivingCar() |
10 |
|
11 |
|
12 |
|
13 |
def test_stop(self): |
14 |
|
15 |
self.car.speed = 5 |
16 |
|
17 |
self.car.stop() |
18 |
|
19 |
# Verify the speed is 0 after stopping
|
20 |
|
21 |
self.assertEqual(0, self.car.speed) |
22 |
|
23 |
|
24 |
|
25 |
# Verify it is Ok to stop again if the car is already stopped
|
26 |
|
27 |
self.car.stop() |
28 |
|
29 |
self.assertEqual(0, self.car.speed) |
Directrices para las pruebas unitarias
Debes comprometerte.
Escribir buenas pruebas unitarias es un trabajo duro. Escribir pruebas unitarias toma tiempo. Cuando haces cambios en tu código, normalmente tendrás que cambiar las pruebas también. Algunas veces tendrás errores en tu código de prueba. Eso significa que tienes que estar realmente comprometido. Los beneficios son enormes, incluso para proyectos pequeños, pero no son gratuitos.
Sé disciplinado
Debes ser disciplinado. Sé consistente. Asegúrate de que las pruebas siempre pasen. No dejes que las pruebas se rompan porque "sabes" que el código está bien.
Automatizar
Para ayudarte a ser disciplinado, debes automatizar tus pruebas unitarias. Las pruebas deben ejecutarse automáticamente en puntos significativos como pre-commit o pre-deployment. Idealmente, tu sistema de gestión de control de origen rechaza el código que no pasó todas las pruebas.
El código no probado está roto por definición
Si no lo probaste, no puedes decir que funciona. Esto significa que debes considerar que está roto. Si es un código crítico, no lo despliegues en producción.
Fondo
¿Qué es una unidad?
Una unidad para el propósito de pruebas unitarias es un archivo/módulo que contiene un conjunto de funciones relacionadas o una clase. Si tienes un archivo con varias clases, debes escribir una prueba unitaria para cada clase.
A TDD o no a TDD
El desarrollo basado en pruebas es una práctica en la que se escriben las pruebas antes de escribir el código. Hay varias ventajas de este enfoque, pero recomiendo evitarlo en caso de que tengas la disciplina para escribir pruebas adecuadas después.
La razón es que yo diseño con código. Escribo el código, lo miro, lo reescribo, lo miro de nuevo y lo vuelvo a escribir muy rápidamente. Escribir pruebas me limita primero y me frena.
Una vez que haya terminado con el diseño inicial, escribiré las pruebas inmediatamente, antes de integrarme con el resto del sistema. Dicho esto, es una gran manera de presentarse a las pruebas unitarias, y te aseguras que todo tu código tendrá pruebas.
El Módulo Unittest
El módulo unittest viene con la biblioteca estándar de Python. Proporciona una clase llamada TestCase
, de la que se puede derivar su clase. A continuación, puedes anular un método setUp()
para preparar un dispositivo de prueba antes de cada prueba y/o un método de clase classSetUp()
para preparar un dispositivo de prueba para todas las pruebas (no restablecer entre pruebas individuales). Existen métodos correspondientes de tearDown()
y classTearDown()
que también puedes reemplazar.
Aquí están las partes relevantes de nuestra clase SelfDrivingCarTest
. Sólo utilizo el método setUp()
. Creo una nueva instancia SelfDrivingCar
y la almaceno en self.car
para que esté disponible en cada prueba.
1 |
from unittest import TestCase |
2 |
|
3 |
|
4 |
|
5 |
class SelfDrivingCarTest(TestCase): |
6 |
|
7 |
def setUp(self): |
8 |
|
9 |
self.car = SelfDrivingCar() |
El siguiente paso es escribir métodos de prueba específicos para probar el código bajo prueba—la clase SelfDrivingCar
en este caso—está haciendo lo que se supone que debe hacer. La estructura de un método de prueba es bastante estándar:
- Preparar el entorno (opcional).
- Prepara el resultado esperado.
- Llama el código bajo prueba.
- Asegúrate que el resultado real coincida con el resultado esperado.
Ten en cuenta que el resultado no tiene que ser la salida de un método. Puede ser un cambio de estado de una clase, un efecto secundario como añadir una nueva fila en una base de datos, escribir un archivo o enviar un correo electrónico.
Por ejemplo, el método stop()
de la clase SelfDrivingCar
no devuelve nada, pero cambia el estado interno estableciendo la velocidad en 0. El método assertEqual()
proporcionado por la clase base TestCase
se utiliza aquí para verificar que el parámetro llamado stop()
funcionó como se esperaba.
1 |
def test_stop(self): |
2 |
|
3 |
self.car.speed = 5 |
4 |
|
5 |
self.car.stop() |
6 |
|
7 |
# Verify the speed is 0 after stopping
|
8 |
|
9 |
self.assertEqual(0, self.car.speed) |
10 |
|
11 |
|
12 |
|
13 |
# Verify it is Ok to stop again if the car is already stopped
|
14 |
|
15 |
self.car.stop() |
16 |
|
17 |
self.assertEqual(0, self.car.speed) |
En realidad, hay dos pruebas aquí. La primera prueba es asegurarse de que si la velocidad del coche es 5 y si se llama el método stop()
, entonces la velocidad se convierta en 0. Luego, la otra prueba es asegurar que nada va mal si se llama a stop()
de nuevo cuando el coche ya está detenido.
Más adelante, presentaré varias pruebas más para una funcionalidad adicional.
El módulo Doctest
El módulo doctest es bastante interesante. Te permite utilizar muestras de código interactivo en tu docstring y verificar los resultados, incluidas las excepciones generadas.
No utilizo o recomiendo doctest para sistemas a gran escala. Las pruebas unitarias apropiadas requieren mucho trabajo. El código de prueba es típicamente mucho más grande que el código bajo prueba. Los Docstrings no son el medio adecuado para escribir pruebas exhaustivas. Sin embargo, son geniales. Esto es lo que se parece a una función factorial
con pruebas de doc:
1 |
import math |
2 |
|
3 |
|
4 |
|
5 |
def factorial(n): |
6 |
|
7 |
"""Return the factorial of n, an exact integer >= 0.
|
8 |
|
9 |
|
10 |
|
11 |
If the result is small enough to fit in an int, return an int.
|
12 |
|
13 |
Else return a long.
|
14 |
|
15 |
|
16 |
|
17 |
>>> [factorial(n) for n in range(6)]
|
18 |
|
19 |
[1, 1, 2, 6, 24, 120]
|
20 |
|
21 |
>>> [factorial(long(n)) for n in range(6)]
|
22 |
|
23 |
[1, 1, 2, 6, 24, 120]
|
24 |
|
25 |
>>> factorial(30)
|
26 |
|
27 |
265252859812191058636308480000000L
|
28 |
|
29 |
>>> factorial(30L)
|
30 |
|
31 |
265252859812191058636308480000000L
|
32 |
|
33 |
>>> factorial(-1)
|
34 |
|
35 |
Traceback (most recent call last):
|
36 |
|
37 |
...
|
38 |
|
39 |
ValueError: n must be >= 0
|
40 |
|
41 |
|
42 |
|
43 |
Factorials of floats are OK, but the float must be an exact integer:
|
44 |
|
45 |
>>> factorial(30.1)
|
46 |
|
47 |
Traceback (most recent call last):
|
48 |
|
49 |
...
|
50 |
|
51 |
ValueError: n must be exact integer
|
52 |
|
53 |
>>> factorial(30.0)
|
54 |
|
55 |
265252859812191058636308480000000L
|
56 |
|
57 |
|
58 |
|
59 |
It must also not be ridiculously large:
|
60 |
|
61 |
>>> factorial(1e100)
|
62 |
|
63 |
Traceback (most recent call last):
|
64 |
|
65 |
...
|
66 |
|
67 |
OverflowError: n too large
|
68 |
|
69 |
"""
|
70 |
|
71 |
if not n >= 0: |
72 |
|
73 |
raise ValueError("n must be >= 0") |
74 |
|
75 |
if math.floor(n) != n: |
76 |
|
77 |
raise ValueError("n must be exact integer") |
78 |
|
79 |
if n+1 == n: # catch a value like 1e300 |
80 |
|
81 |
raise OverflowError("n too large") |
82 |
|
83 |
result = 1 |
84 |
|
85 |
factor = 2 |
86 |
|
87 |
while factor <= n: |
88 |
|
89 |
result *= factor |
90 |
|
91 |
factor += 1 |
92 |
|
93 |
return result |
94 |
|
95 |
|
96 |
|
97 |
|
98 |
|
99 |
if __name__ == "__main__": |
100 |
|
101 |
import doctest |
102 |
|
103 |
doctest.testmod() |
Como puedes ver, la docstring es mucho más grande que el código de función. No promueve la legibilidad.
Pruebas en curso
Y bien. Escribiste tu prueba unitaria. Para un sistema grande, tendrás decenas/cientos/miles de módulos y clases en directorios posiblemente múltiples. ¿Cómo se hacen todas estas pruebas?
El módulo unittest proporciona varias facilidades para agrupar pruebas y ejecutarlas de forma programática. Echa un vistazo a las pruebas de carga y ejecución. Pero la forma más fácil es el descubrimiento de pruebas. Esta opción se agregó sólo en Python 2.7. Pre-2.7 podrías utilizar nose para descubrir y ejecutar pruebas. Nose tiene algunas otras ventajas como ejecutar funciones de prueba sin tener que crear una clase para sus casos de prueba. Pero para el propósito de este artículo, vamos a seguir con unittest.
Para descubrir y ejecutar tus pruebas basadas en pruebas unitarias, simplemente escribe en la línea de comandos:
python -m unittest discover
Unittest analizará todos los archivos y subdirectorios, ejecutará las pruebas que encuentre y proporcionará un informe agradable así como el tiempo de ejecución. Si deseas ver qué pruebas está ejecutando, puedes agregar el indicador -v:
python -m unittest discover -v
Hay varios indicadores que controlan la operación:
1 |
python -m unittest -h |
2 |
|
3 |
Usage: python -m unittest [options] [tests] |
4 |
|
5 |
|
6 |
|
7 |
Options: |
8 |
|
9 |
-h, --help Show this message |
10 |
|
11 |
-v, --verbose Verbose output |
12 |
|
13 |
-q, --quiet Minimal output |
14 |
|
15 |
-f, --failfast Stop on first failure |
16 |
|
17 |
-c, --catch Catch control-C and display results |
18 |
|
19 |
-b, --buffer Buffer stdout and stderr during test runs |
20 |
|
21 |
|
22 |
|
23 |
Examples: |
24 |
|
25 |
python -m unittest test_module - run tests from test_module |
26 |
|
27 |
python -m unittest module.TestClass - run tests from module.TestClass |
28 |
|
29 |
python -m unittest module.Class.test_method - run specified test method |
30 |
|
31 |
|
32 |
|
33 |
[tests] can be a list of any number of test modules, classes and test |
34 |
|
35 |
methods. |
36 |
|
37 |
|
38 |
|
39 |
Alternative Usage: python -m unittest discover [options] |
40 |
|
41 |
|
42 |
|
43 |
Options: |
44 |
|
45 |
-v, --verbose Verbose output |
46 |
|
47 |
-f, --failfast Stop on first failure |
48 |
|
49 |
-c, --catch Catch control-C and display results |
50 |
|
51 |
-b, --buffer Buffer stdout and stderr during test runs |
52 |
|
53 |
-s directory Directory to start discovery ('.' default) |
54 |
|
55 |
-p pattern Pattern to match test files ('test*.py' default) |
56 |
|
57 |
-t directory Top level directory of project (default to |
58 |
|
59 |
start directory) |
60 |
|
61 |
|
62 |
|
63 |
For test discovery all test modules must be importable from the top |
64 |
|
65 |
level directory of the project. |
Cobertura de la prueba
La cobertura de la prueba es un campo a menudo descuidado. Cobertura significa cuánto de tu código es realmente probado por tus pruebas. Por ejemplo, si tienes una función con una sentencia if-else
y pruebas sólo la rama if
, entonces no sabe si la otra rama else
funciona o no. En el ejemplo de código siguiente, la función add()
comprueba el tipo de sus argumentos. Si ambos son enteros, simplemente los agrega.
Si ambos son cadenas, intenta convertirlos en enteros y los agrega. De lo contrario, plantea una excepción. La función test_add()
prueba la función add()
con argumentos que son enteros y con argumentos que son flotantes y verifica el comportamiento correcto en cada caso. Pero la cobertura de la prueba está incompleta. El caso de argumentos de cadena no se probó. Como resultado, la prueba pasa con éxito, pero el error tipográfico en la rama en la que los argumentos son ambas cadenas no fue descubierto (¿miras el 'intg' allí?).
1 |
import unittest |
2 |
|
3 |
|
4 |
|
5 |
def add(a, b): |
6 |
|
7 |
"""This function adds two numbers a, b and returns their sum
|
8 |
|
9 |
|
10 |
|
11 |
a and b may integers
|
12 |
|
13 |
"""
|
14 |
|
15 |
if isinstance(a, int) and isinstance(b, int): |
16 |
|
17 |
return a + b |
18 |
|
19 |
elseif isinstance(a, str) and isinstance(b, str): |
20 |
|
21 |
return int(a) + intg(b) |
22 |
|
23 |
else: |
24 |
|
25 |
raise Exception('Invalid arguments') |
26 |
|
27 |
|
28 |
|
29 |
class Test(unittest.TestCase): |
30 |
|
31 |
def test_add(self): |
32 |
|
33 |
self.assertEqual(5, add(2, 3)) |
34 |
|
35 |
self.assertEqual(15, add(-6, 21)) |
36 |
|
37 |
self.assertRaises(Exception, add, 4.0, 5.0) |
38 |
|
39 |
|
40 |
|
41 |
unittest.main() |
Aquí está la salida:
1 |
---------------------------------------------------------------------- |
2 |
|
3 |
Ran 1 test in 0.000s |
4 |
|
5 |
|
6 |
|
7 |
OK |
8 |
|
9 |
|
10 |
|
11 |
Process finished with exit code 0 |
Pruebas unitarias
Escribir pruebas unitarias de fuerza industrial no es fácil ni simple. Hay varias cosas a considerar y compensaciones que se harán.
Diseño para Testabilidad
Si tu código es lo que se llama formalmente código espagueti o una gran bola de barro donde diferentes niveles de abstracción se mezclan y cada pieza de código depende de cada otro pedazo de código, tendrás un tiempo difícil para probarlo. Además, siempre que cambies algo, tendrás que actualizar un montón de pruebas también.
La buena noticia es que el diseño de software adecuado de propósito general es exactamente lo que necesitas para la testabilidad. En particular, el código modular bien factorizado, en el que cada componente tiene una responsabilidad clara e interactúa con otros componentes a través de interfaces bien definidas, hará que escribir buenas pruebas unitarias sea un placer.
Por ejemplo, nuestra clase SelfDrivingCar
es responsable del funcionamiento del alto nivel del coche: ir, detenerse, navegar. Tiene un método calculate_distance_to_object_in_front ()
que aún no se ha implementado. Esta funcionalidad probablemente debería ser implementada por un subsistema totalmente separado. Puedes incluir la lectura de datos de varios sensores, la interacción con otros automóviles de conducción propia, una pila de toda la visión de la máquina para analizar las imágenes de múltiples cámaras.
Veamos cómo funciona esto en la práctica. El SelfDrivingCar
aceptará un argumento llamado object_detector
que tiene un método llamado calculate_distance_to_object_in_front ()
, y delegará esta funcionalidad a este objeto. Ahora, no hay necesidad de probar esta unidad porque el objeto_detector
es responsable (y debe ser probado) para ello. Todavía quieres para la prueba unitaria el hecho de que estás utilizando object_detector
correctamente.
1 |
class SelfDrivingCar(object): |
2 |
|
3 |
def __init__(self, object_detector): |
4 |
|
5 |
self.object_detector |
6 |
|
7 |
self.speed = 0 |
8 |
|
9 |
self.destination = None |
10 |
|
11 |
|
12 |
|
13 |
def _calculate_distance_to_object_in_front(self): |
14 |
|
15 |
return self.object_detector.calculate_distance_to_object_in_front() |
Costo/Beneficio
La cantidad de esfuerzo que pones en las pruebas debe estar correlacionada con el costo del fracaso, la estabilidad del código y la facilidad con que se arreglan si se detectan problemas en la línea.
Por ejemplo, nuestra clase self-driving (autopilotado) es muy crítica. Si el método stop()
no funciona correctamente, nuestro coche autodirigido puede matar a personas, destruir bienes y descarrilar todo el mercado automotor. Si desarrollas un auto autopilotado, sospecho que tus pruebas unitarias para el método stop()
serán un poco más rigurosas que las mías.
Por otro lado, si un botón de tu aplicación web que está tres niveles debajo de tu página principal parpadea un poco cuando alguien hace clic en él, puedes arreglarlo, pero probablemente no agregarás una prueba unitaria dedicada para este caso. La economía simplemente no lo justifica.
Prueba Mindset
La prueba mindset es muy importante. Un principio que utilizo es que cada pieza del código tiene por lo menos dos usuarios: el otro código que se está usando y la prueba que se está testeando. Esta regla simple ayuda mucho con el diseño y las dependencias. Si recuerdas que tienes que escribir una prueba para tu código, no agregarás muchas dependencias que sean difíciles de reconstruir durante las pruebas.
Por ejemplo, supongamos que tu código necesita calcular algo. Para ello, necesitas cargar algunos datos de una base de datos, leer un archivo de configuración y consultar dinámicamente algunas API REST para obtener información actualizada. Todo esto puede ser necesario por varias razones, pero poniendo todo eso en una sola función hará que sea bastante difícil la prueba unitaria. Todavía es posible con burlas, pero es mucho mejor estructurar tu código correctamente.
Funciones Puras
El código más fácil de probar son las funciones puras. Las funciones puras son funciones que acceden sólo a los valores de sus parámetros, no tienen efectos secundarios y devuelven el mismo resultado cuando se les llama con los mismos argumentos. No cambian el estado del programa, no acceden al sistema de archivos o a la red. Sus beneficios son demasiados para contarlos aquí.
¿Por qué son fáciles de probar? Porque no hay necesidad de establecer un entorno especial para probar. Sólo pasas argumentos y pruebas el resultado. También sabes que mientras el código bajo prueba no cambie, tu prueba no tiene que cambiar.
Compara esto con una función que lee un archivo de configuración XML. Tu prueba tendrá que crear un archivo XML y pasar su nombre de archivo al código bajo prueba. No es gran cosa. Pero supongamos que alguien decidió que XML es abominable y que todos los archivos de configuración deben estar en JSON. Ellos van sobre su negocio y a convertir todos los archivos de configuración a JSON. ¡Ejecutan todas las pruebas incluyendo sus pruebas y todas pasan!
¿Por qué? Porque el código no cambió. Todavía esperas un archivo de configuración XML, y tu prueba todavía construye un archivo XML para eso. Pero en la producción, tu código obtendrá un archivo JSON, que no podrás analizar.
Prueba de manejo de errores
El manejo de errores es otra cosa que es crítica para probar. También es parte del diseño. ¿Quién es responsable de la corrección de la entrada? Cada función y método debe ser claro al respecto. Si es la responsabilidad de la función, debes verificar su entrada, pero si es la responsabilidad de la persona que llama, entonces la función sólo puede ir sobre su negocio y asumir que la entrada es correcta. La corrección general del sistema se garantizará mediante la realización de pruebas para que el llamante verifique que sólo transmite la entrada correcta a su función.
Normalmente, deseas verificar la entrada en la interfaz pública a tu código porque no necesariamente sabes quién va a llamar a tu código. Echemos un vistazo al método drive()
del automóvil autodirigido. Este método espera un parámetro 'destino'. El parámetro 'destino' se utilizará más adelante en la navegación, pero el método de unidad no hace nada para verificar que es correcto.
Supongamos que se supone que el destino es una tupla de latitud y longitud. Hay todo tipo de pruebas que se pueden hacer para verificar que es válida (por ejemplo, es el destino en el centro del mar). Para nuestros propósitos, asegúrate de que es una tupla de flotadores en el rango de 0.0 a 90.0 para la latitud y -180.0 a 180.0 para la longitud.
Aquí está la clase SelfDrivingCar
actualizada. Implementé trivialmente algunos de los métodos no implementados porque el método drive()
llama a algunos de estos métodos directa o indirectamente.
1 |
class SelfDrivingCar(object): |
2 |
|
3 |
def __init__(self, object_detector): |
4 |
|
5 |
self.object_detector = object_detector |
6 |
|
7 |
self.speed = 0 |
8 |
|
9 |
self.destination = None |
10 |
|
11 |
|
12 |
|
13 |
def _accelerate(self): |
14 |
|
15 |
self.speed += 1 |
16 |
|
17 |
|
18 |
|
19 |
def _decelerate(self): |
20 |
|
21 |
if self.speed > 0: |
22 |
|
23 |
self.speed -= 1 |
24 |
|
25 |
|
26 |
|
27 |
def _advance_to_destination(self): |
28 |
|
29 |
distance = self._calculate_distance_to_object_in_front() |
30 |
|
31 |
if distance < 10: |
32 |
|
33 |
self.stop() |
34 |
|
35 |
|
36 |
|
37 |
elif distance < self.speed / 2: |
38 |
|
39 |
self._decelerate() |
40 |
|
41 |
elif self.speed < self._get_speed_limit(): |
42 |
|
43 |
self._accelerate() |
44 |
|
45 |
|
46 |
|
47 |
def _has_arrived(self): |
48 |
|
49 |
return True |
50 |
|
51 |
|
52 |
|
53 |
def _calculate_distance_to_object_in_front(self): |
54 |
|
55 |
return self.object_detector.calculate_distance_to_object_in_front() |
56 |
|
57 |
|
58 |
|
59 |
def _get_speed_limit(self): |
60 |
|
61 |
return 65 |
62 |
|
63 |
|
64 |
|
65 |
def stop(self): |
66 |
|
67 |
self.speed = 0 |
68 |
|
69 |
|
70 |
|
71 |
def drive(self, destination): |
72 |
|
73 |
self.destination = destination |
74 |
|
75 |
while not self._has_arrived(): |
76 |
|
77 |
self._advance_to_destination() |
78 |
|
79 |
self.stop() |
Para probar el manejo de errores en la prueba, pasaré argumentos no válidos y verificaré que sean rechazados correctamente. Puedes hacerlo utilizando el método self.assertRaises()
método de unittest.TestCase
. Este método tiene éxito si el código bajo prueba en realidad plantea una excepción.
Vamos a verlo en acción. El método test_drive()
pasa latitud y longitud fuera del intervalo válido y espera que el método drive()
genere una excepción.
1 |
from unittest import TestCase |
2 |
|
3 |
from self_driving_car import SelfDrivingCar |
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
class MockObjectDetector(object): |
10 |
|
11 |
def calculate_distance_to_object_in_front(self): |
12 |
|
13 |
return 20 |
14 |
|
15 |
|
16 |
|
17 |
|
18 |
|
19 |
class SelfDrivingCarTest(TestCase): |
20 |
|
21 |
def setUp(self): |
22 |
|
23 |
self.car = SelfDrivingCar(MockObjectDetector()) |
24 |
|
25 |
|
26 |
|
27 |
def test_stop(self): |
28 |
|
29 |
self.car.speed = 5 |
30 |
|
31 |
self.car.stop() |
32 |
|
33 |
# Verify the speed is 0 after stopping
|
34 |
|
35 |
self.assertEqual(0, self.car.speed) |
36 |
|
37 |
|
38 |
|
39 |
# Verify it is Ok to stop again if the car is already stopped
|
40 |
|
41 |
self.car.stop() |
42 |
|
43 |
self.assertEqual(0, self.car.speed) |
44 |
|
45 |
|
46 |
|
47 |
def test_drive(self): |
48 |
|
49 |
# Valid destination
|
50 |
|
51 |
self.car.drive((55.0, 66.0)) |
52 |
|
53 |
|
54 |
|
55 |
# Invalid destination wrong range
|
56 |
|
57 |
self.assertRaises(Exception, self.car.drive, (-55.0, 200.0)) |
La prueba falla, porque el método drive()
no comprueba sus argumentos de validez y no genera una excepción. Obtienes un informe agradable con información completa sobre lo que falló, dónde y por qué.
1 |
python -m unittest discover -v |
2 |
|
3 |
test_drive (untitled.test_self_driving_car.SelfDrivingCarTest) ... FAIL |
4 |
|
5 |
test_stop (untitled.test_self_driving_car.SelfDrivingCarTest) ... ok |
6 |
|
7 |
|
8 |
|
9 |
====================================================================== |
10 |
|
11 |
FAIL: test_drive (untitled.test_self_driving_car.SelfDrivingCarTest) |
12 |
|
13 |
---------------------------------------------------------------------- |
14 |
|
15 |
Traceback (most recent call last): |
16 |
|
17 |
File "/Users/gigi/PycharmProjects/untitled/test_self_driving_car.py", line 29, in test_drive |
18 |
|
19 |
self.assertRaises(Exception, self.car.drive, (-55.0, 200.0)) |
20 |
|
21 |
AssertionError: Exception not raised |
22 |
|
23 |
|
24 |
|
25 |
---------------------------------------------------------------------- |
26 |
|
27 |
Ran 2 tests in 0.000s |
28 |
|
29 |
|
30 |
|
31 |
FAILED (failures=1) |
Para corregirlo, actualizamos el método drive()
para comprobar realmente el rango de sus argumentos:
1 |
def drive(self, destination): |
2 |
|
3 |
lat, lon = destination |
4 |
|
5 |
if not (0.0 <= lat <= 90.0): |
6 |
|
7 |
raise Exception('Latitude out of range') |
8 |
|
9 |
if not (-180.0 <= lon <= 180.0): |
10 |
|
11 |
raise Exception('Latitude out of range') |
12 |
|
13 |
|
14 |
|
15 |
self.destination = destination |
16 |
|
17 |
while not self._has_arrived(): |
18 |
|
19 |
self._advance_to_destination() |
20 |
|
21 |
self.stop() |
Ahora, todas las pruebas pasan.
1 |
python -m unittest discover -v |
2 |
|
3 |
test_drive (untitled.test_self_driving_car.SelfDrivingCarTest) ... ok |
4 |
|
5 |
test_stop (untitled.test_self_driving_car.SelfDrivingCarTest) ... ok |
6 |
|
7 |
|
8 |
|
9 |
---------------------------------------------------------------------- |
10 |
|
11 |
Ran 2 tests in 0.000s |
12 |
|
13 |
|
14 |
|
15 |
OK |
16 |
Prueba de Métodos Privados
¿Deberías probar cada función y método? En particular, ¿deberías probar métodos privados llamados sólo por tu código? La respuesta típicamente insatisfactoria es: "Depende".
Intentaré ser útil aquí y decirte de qué depende. Sabes exactamente quién llama a tu método privado: es tu propio código. Si las pruebas para los métodos públicos que llaman a tu método privado son completas, entonces ya prueba tus métodos privados exhaustivamente. Pero si un método privado es muy complicado, es posible que desees probarlo de forma independiente. Utiliza tu juicio.
Cómo organizar tus pruebas unitarias
En un sistema grande, no siempre está claro cómo organizar las pruebas. ¿Deberías tener un archivo grande con todas las pruebas de un paquete, o un archivo de prueba para cada clase? ¿Deben las pruebas estar en el mismo archivo que el código bajo prueba, o en el mismo directorio?
Aquí está el sistema que uso. Las pruebas deben estar totalmente separadas del código bajo prueba (de ahí que no use doctest). Idealmente, tu código debe estar en un paquete. Las pruebas para cada paquete deben estar en un directorio hermano de tu paquete. En el directorio de pruebas, debe haber un archivo para cada módulo de tu paquete denominado test_<nombre del módulo>
.
Por ejemplo, si tienes tres módulos en tu paquete: module_1.py
, module_2.py
y module_3.py
, debes tener tres archivos de prueba: test_module_1.py
, test_module_2.py
y test_module_3.py
en el directorio de pruebas.
Esta convención tiene varias ventajas. Se deja claro sólo por la navegación de directorios que no te olvides de probar algunos módulos por completo. También ayuda a organizar las pruebas en trozos de tamaño razonable. Suponiendo que tus módulos tengan un tamaño razonable, entonces el código de prueba para cada módulo estará en tu propio archivo, que puede ser un poco más grande que el módulo bajo prueba, pero todavía es algo que se ajusta cómodamente en un archivo.
Conclusión
Las pruebas unitarias son la base del código sólido. En este tutorial, exploré algunos principios y directrices para las pruebas de unidad y explicó el razonamiento detrás de varias prácticas recomendadas. Cuanto más grande es el sistema que estás construyendo, las pruebas unitarias se convierten en algo más importante. Pero las pruebas unitarias no son suficientes. También se necesitan otros tipos de pruebas para sistemas a gran escala: pruebas de integración, pruebas de rendimiento, pruebas de carga, pruebas de penetración, pruebas de aceptación, etc.