Estoy tratando de familiarizarme con las pruebas unitarias.
Digamos que tenemos un dado que puede tener un número predeterminado de lados igual a 6 (pero puede ser 4, 5 lados, etc.):
import random
class Die():
def __init__(self, sides=6):
self._sides = sides
def roll(self):
return random.randint(1, self._sides)
¿Serían las siguientes pruebas unitarias válidas / útiles?
- prueba un lanzamiento en el rango 1-6 para un dado de 6 lados
- prueba un resultado de 0 para un dado de 6 lados
- prueba un resultado de 7 para un dado de 6 lados
- prueba un lanzamiento en el rango 1-3 para un dado de 3 lados
- prueba un resultado de 0 para un dado de 3 lados
- prueba un resultado de 4 para un dado de 3 lados
Solo estoy pensando que esto es una pérdida de tiempo, ya que el módulo aleatorio ha existido durante el tiempo suficiente, pero luego creo que si el módulo aleatorio se actualiza (digamos que actualizo mi versión de Python), al menos estoy cubierto.
Además, ¿necesito probar otras variaciones de tiradas de dado, por ejemplo, las 3 en este caso, o es bueno cubrir otro estado de dado inicializado?
python
unit-testing
tdd
Cybran
fuente
fuente
Respuestas:
Tiene razón, sus pruebas no deben verificar que el
random
módulo está haciendo su trabajo; una prueba de unidad solo debe probar la clase en sí, no cómo interactúa con otro código (que debe probarse por separado).Por supuesto, es completamente posible que su código use
random.randint()
incorrectamente; o estás llamandorandom.randrange(1, self._sides)
y tu dado nunca arroja el valor más alto, pero ese sería un tipo diferente de error, no uno que puedas atrapar con una prueba de unidad. En ese caso, sudie
unidad está funcionando según lo diseñado, pero el diseño en sí era defectuoso.En este caso, usaría la burla para reemplazar la
randint()
función, y solo verificaría que se haya llamado correctamente. Python 3.3 y versiones posteriores vienen con elunittest.mock
módulo para manejar este tipo de pruebas, pero puede instalar elmock
paquete externo en versiones anteriores para obtener exactamente la misma funcionalidadCon burlas, su prueba ahora es muy simple; solo hay 2 casos, de verdad. El caso predeterminado para un dado de 6 lados y el caso de lados personalizados.
Hay otras formas de reemplazar temporalmente la
randint()
función en el espacio de nombres global deDie
, pero elmock
módulo lo hace más fácil. El@mock.patch
decorador aquí se aplica a todos los métodos de prueba en el caso de prueba; a cada método de prueba se le pasa un argumento adicional, larandom.randint()
función simulada, por lo que podemos probar el simulacro para ver si realmente se ha llamado correctamente. Elreturn_value
argumento especifica lo que se devuelve del simulacro cuando se llama, por lo que podemos verificar que eldie.roll()
método nos haya devuelto el resultado 'aleatorio'.He usado otra práctica recomendada de pruebas unitarias de Python aquí: importe la clase bajo prueba como parte de la prueba. El
_make_one
método realiza la importación y la creación de instancias dentro de una prueba , de modo que el módulo de prueba aún se cargará incluso si cometió un error de sintaxis u otro error que evitará que el módulo original se importe.De esta manera, si cometió un error en el código del módulo, las pruebas aún se ejecutarán; simplemente fallarán y le informarán sobre el error en su código.
Para ser claros, las pruebas anteriores son simplistas en extremo. El objetivo aquí no es probar que
random.randint()
se ha llamado con los argumentos correctos, por ejemplo. En cambio, el objetivo es probar que la unidad está produciendo los resultados correctos dadas ciertas entradas, donde esas entradas incluyen los resultados de otras unidades que no están bajo prueba. Al burlarse delrandom.randint()
método, puede tomar el control de solo otra entrada a su código.En las pruebas del mundo real , el código real en su unidad bajo prueba será más complejo; La relación con las entradas pasadas a la API y cómo se invocan otras unidades puede ser interesante aún, y la burla le dará acceso a resultados intermedios, así como también le permitirá establecer los valores de retorno para esas llamadas.
Por ejemplo, en el código que autentica a los usuarios contra un servicio OAuth2 de terceros (una interacción de varias etapas), desea probar que su código está pasando los datos correctos a ese servicio de terceros y le permite simular diferentes respuestas de error que El servicio de terceros volvería, permitiéndole simular diferentes escenarios sin tener que construir un servidor OAuth2 completo usted mismo. Aquí es importante probar que la información de una primera respuesta se ha manejado correctamente y se ha pasado a una llamada de segunda etapa, por lo que desea ver que el servicio simulado se llama correctamente.
fuente
randint()
, no el códigoDie.roll()
.sentinel.die
por ejemplo (el objeto centinela también es deunittest.mock
) y luego verifique que sea lo que fue devuelto por su método de rollo. En realidad, esto solo permite una forma de implementar el método probado.sentinel.die
sería una excelente manera de garantizarlo.La respuesta de Martijn es cómo lo haría si realmente quisiera ejecutar una prueba que demuestre que está llamando al azar. Sin embargo, a riesgo de que me digan "eso no responde a la pregunta", creo que esto no debería probarse en absoluto. Burlarse de randint ya no es una prueba de recuadro negro: está mostrando específicamente que ciertas cosas están sucediendo en la implementación . La prueba de caja negra ni siquiera es una opción: no hay ninguna prueba que pueda ejecutar que demuestre que el resultado nunca será inferior a 1 o superior a 6.
¿Puedes burlarte
randint
? Sí tu puedes. Pero que estas probando? Que lo llamaste con argumentos 1 y lados. ¿Qué significa eso ? Estás de vuelta en la casilla uno: al final del día terminarás teniendo que demostrar, formal o informalmente, que llamarrandom.randint(1, sides)
correctamente implementa una tirada de dados.Estoy todo para pruebas unitarias. Son fantásticos controles de cordura y exponen la presencia de errores. Sin embargo, nunca pueden probar su ausencia, y hay cosas que no se pueden afirmar a través de las pruebas (por ejemplo, que una función en particular nunca arroja una excepción o siempre termina). En este caso particular, siento que hay muy poco a lo que te refieres ganancia. Para un comportamiento determinista, las pruebas unitarias tienen sentido porque realmente sabes cuál será la respuesta que esperas.
fuente
random.randint
se llama no1, sides
tiene valor si eso es algo incorrecto.random.randint()
devuelvan correctamente los valores en el rango [1, lados] (inclusive), eso depende de los desarrolladores de Python para asegurarse de que larandom
unidad funcione correctamente.random.randint()
comportarse derandom.randrange()
esa manera y así llamarlorandom.randint(1, sides + 1)
, entonces está hundido de todos modos.)Arreglar semilla aleatoria. Para los dados de 1, 2, 5 y 12 lados, confirme que unos pocos miles de tiradas arrojen resultados que incluyen 1 y N, y no incluyen 0 o N + 1. Si por casualidad parece que obtiene un conjunto de resultados aleatorios que no cubra el rango esperado, cambie a una semilla diferente.
Las herramientas de burla son geniales, pero el hecho de que te permitan hacer algo no significa que se deba hacer. YAGNI se aplica tanto a los accesorios de prueba como a las características.
Si puede probar fácilmente con dependencias no desmontadas, casi siempre debería hacerlo; de esa manera, sus pruebas se centrarán en reducir el recuento de defectos, no solo en aumentar el recuento de pruebas. El exceso de burlas corre el riesgo de crear cifras de cobertura engañosas, lo que a su vez puede llevar a posponer la prueba real a una fase posterior a la que quizás nunca tenga tiempo para llegar ...
fuente
¿Qué es un
Die
si lo piensas? - No más que una envoltura alrededorrandom
. Se encapsularandom.randint
y etiqueta de nuevo en términos de un vocabulario propio de la aplicación:Die.Roll
.No me parece relevante insertar otra capa de abstracción entre
Die
yrandom
porqueDie
ya es esta capa de indirección entre su aplicación y la plataforma.Si quieres resultados de dados enlatados, solo mofa
Die
, no te burlesrandom
.En general, no pruebo mis objetos de contenedor que se comunican con sistemas externos, escribo pruebas de integración para ellos. Podría escribir un par de esos,
Die
pero como señaló, debido a la naturaleza aleatoria del objeto subyacente, no serán significativos. Además, aquí no hay configuración o comunicación de red, por lo que no hay mucho que probar, excepto una llamada de plataforma.=> Teniendo en cuenta que
Die
solo se trata de unas pocas líneas de código triviales y agrega poca o ninguna lógica en comparación conrandom
sí mismo, me saltaría la prueba en ese ejemplo específico.fuente
Sembrar el generador de números aleatorios y verificar los resultados esperados NO es, por lo que puedo ver, una prueba válida. Hace suposiciones sobre CÓMO funciona su dado internamente, lo cual es travieso-travieso. Los desarrolladores de python podrían cambiar el generador de números aleatorios, o el dado (NOTA: "dado" es plural, "dado" es singular. A menos que su clase implemente múltiples tiradas de dado en una llamada, probablemente debería llamarse "dado") podría use un generador de números aleatorios diferente.
Del mismo modo, burlarse de la función aleatoria supone que la implementación de la clase funciona exactamente como se esperaba. ¿Por qué podría no ser este el caso? Alguien podría tomar el control del generador de números aleatorios de python predeterminado, y para evitar eso, una versión futura de su dado puede obtener varios números aleatorios, o números aleatorios más grandes, para mezclar más datos aleatorios. Los fabricantes del sistema operativo FreeBSD utilizaron un esquema similar cuando sospecharon que la NSA estaba manipulando los generadores de números aleatorios de hardware integrados en las CPU.
Si fuera yo, correría, digamos, 6000 rollos, los contaría, y me aseguraría de que cada número del 1-6 salga entre 500 y 1500 veces. También verificaría que no se devuelvan números fuera de ese rango. También podría comprobar que, para un segundo conjunto de 6000 rollos, al ordenar el [1..6] en orden de frecuencia, el resultado es diferente (¡esto fallará una vez de 720 ejecuciones, si los números son aleatorios!). Si desea ser minucioso, puede encontrar la frecuencia de los números después de un 1, después de un 2, etc. pero asegúrese de que el tamaño de su muestra sea lo suficientemente grande y que tenga suficiente variación. Los humanos esperan que los números aleatorios tengan menos patrones de los que realmente tienen.
Repita para un dado de 12 lados y 2 lados (6 es el más utilizado, por lo que es el más esperado para cualquiera que escriba este código).
Finalmente, probaría para ver qué sucede con un dado de 1 cara, un dado de 0 caras, un dado de -1 cara, un dado de 2.3 caras, un dado de [1,2,3,4,5,6] y un dado de "bla". Por supuesto, todo esto debería fallar; ¿Fallan de una manera útil? Estos probablemente deberían fallar en la creación, no en el rodamiento.
O, tal vez, también desee manejarlos de manera diferente, tal vez crear un dado con [1,2,3,4,5,6] debería ser aceptable, y quizás también "bla"; Esto podría ser un dado con 4 caras, y cada cara con una letra. Me viene a la mente el juego "Boggle", al igual que una bola mágica de ocho.
Y finalmente, es posible que desee contemplar esto: http://lh6.ggpht.com/-fAGXwbJbYRM/UJA_31ACOLI/AAAAAAAAAPg/2FxOWzo96KE/s1600-h/random%25255B3%25255D.jpg
fuente
A riesgo de nadar contra la corriente, resolví este problema exacto hace varios años utilizando un método no mencionado hasta ahora.
Mi estrategia fue simplemente burlarme del RNG con uno que produzca una corriente predecible de valores que abarque todo el espacio. Si (por ejemplo) lado = 6 y el RNG produce valores de 0 a 5 en secuencia, puedo predecir cómo debería comportarse mi clase y la prueba unitaria en consecuencia.
La razón es que esto prueba la lógica solo en esta clase, bajo el supuesto de que el RNG finalmente producirá cada uno de esos valores y sin probar el RNG en sí.
Es simple, determinista, reproducible y atrapa errores. Usaría la misma estrategia nuevamente.
La pregunta no explica cuáles deberían ser las pruebas, solo qué datos podrían usarse para las pruebas, dada la presencia de un RNG. Mi sugerencia es simplemente probar exhaustivamente burlándose del RNG. La pregunta de qué vale la pena probar depende de la información no proporcionada en la pregunta.
fuente
Las pruebas que sugiere en su pregunta no detectan un contador aritmético modular como implementación. Y no detectan errores de implementación comunes en el código relacionado con la distribución de probabilidad como
return 1 + (random.randint(1,maxint) % sides)
. O un cambio en el generador que resulta en patrones bidimensionales.Si realmente desea verificar que está generando números de distribución aleatoria uniformemente distribuidos, debe verificar una gran variedad de propiedades. Para hacer un trabajo razonablemente bueno, puede ejecutar http://www.phy.duke.edu/~rgb/General/dieharder.php en sus números generados. O escriba un conjunto de pruebas unitarias igualmente complejo.
Eso no es culpa de las pruebas unitarias o TDD, la aleatoriedad resulta ser una propiedad muy difícil de verificar. Y un tema popular para ejemplos.
fuente
La prueba más fácil de una tirada de dados es simplemente repetirlo cientos de miles de veces y validar que cada resultado posible se golpeó aproximadamente (1 / número de lados). En el caso de un dado de 6 lados, debería ver cada valor posible golpear alrededor del 16.6% del tiempo. Si alguno está apagado en más de un porcentaje, entonces tiene un problema.
Hacerlo de esta manera evita que le permita refactorizar la mecánica subyacente de generar un número aleatorio fácilmente, y lo más importante, sin cambiar la prueba.
fuente