proyectos ejemplos python unit-testing testing python-unittest static-code-analysis

python - ejemplos - Detectando métodos de afirmación incorrectos



django (4)

Durante una de las últimas revisiones de código, me encontré con el problema que no era inmediatamente fácil de detectar: ​​se assertTrue() lugar de assertEqual() que básicamente resultó en una prueba que no probaba nada . Aquí hay un ejemplo simplificado:

from unittest import TestCase class MyTestCase(TestCase): def test_two_things_equal(self): self.assertTrue("a", "b")

El problema aquí es que la prueba pasaría; y técnicamente, el código es válido, ya que assertTrue tiene este argumento de assertTrue opcional (que obtiene el valor "b" en este caso).

¿Podemos hacerlo mejor que confiar en la persona que revisa el código para detectar este tipo de problemas? ¿Hay alguna forma de autodetectarlo usando análisis de código estático con flake8 o pylint ?


Hace varios años, presenté un enfoque / metodología general para asegurar la calidad de las pruebas. La especificación de una prueba se puede reducir a dos cláusulas:

  1. Debe pasar para la implementación correcta de la característica que se prueba, y
  2. Debe fallar para la implementación incorrecta / incorrecta de la característica que se prueba

Según mi leal saber y entender, aunque se está ejerciendo el requisito 1. habitualmente, se presta poca atención al requisito 2.

Típicamente

  • se crea un conjunto de pruebas,
  • el código se ejecuta contra él,
  • cualquier falla (debido a errores en el código o en las pruebas) se corrige
  • y llegamos a una situación en la que creemos que nuestro código y nuestras pruebas son buenos.

La situación real puede ser que (algunas de) las pruebas contengan errores que (podrían) evitar que atrapen errores en el código. Por lo tanto, ver pasar las pruebas no debería sugerir mucha tranquilidad a la persona que se preocupa por la calidad del sistema, hasta que estén seguros de que las pruebas realmente pueden detectar los problemas contra los que fueron diseñados 1 . ¡Y una manera simple de hacerlo es introducir realmente tales problemas y verificar que las pruebas no los pasen desapercibidos!

En TDD (desarrollo basado en pruebas), esta idea se sigue solo parcialmente: la recomendación es agregar la prueba antes que el código, ver que falla (debería, ya que todavía no hay código) y luego corregirlo escribiendo el código. Pero el fracaso de una prueba debido a la falta de código no significa automáticamente que también fallará en el caso de un código defectuoso (¡esto parece ser cierto para su caso)!

De modo que la calidad de un conjunto de pruebas se puede medir como un porcentaje de errores que sería capaz de detectar. Cualquier error razonable 2 que escapa de un banco de pruebas sugiere un nuevo caso de prueba que cubre ese escenario (o, si el banco de pruebas debe haber detectado ese error, se descubre un error en el conjunto de pruebas). Esto también significa que cada prueba del conjunto debe ser capaz de detectar al menos un error (de lo contrario, esa prueba es completamente inútil).

Estaba pensando en implementar un sistema de software que facilitara la adopción de esta metodología (es decir, permite inyectar y mantener fallas artificiales en la base de códigos y verifica cómo las pruebas responden a ellas). Esta pregunta actuó como un disparador que voy a empezar a trabajar en ello de inmediato. Esperando armar algo en una semana. ¡Manténganse al tanto!

EDITAR

Una versión prototipo de la herramienta ahora está disponible en https://bitbucket.org/leon_manukyan/trit . Recomiendo clonar el repositorio y ejecutar el flujo de demostración.

1 Una versión más generalizada de esta afirmación es cierta para una gama más amplia de sistemas / situaciones (todas las cuales típicamente tienen que ver con la seguridad / seguridad):

Un sistema diseñado contra ciertos eventos debe ser probado rutinariamente contra tales eventos, de lo contrario es propenso a la degradación hasta la completa incapacidad de reaccionar contra los eventos de interés.

Solo un ejemplo: ¿tiene un sistema de alarma contra incendios en casa? ¿Cuándo presenció que funcionaba la última vez? ¿Qué pasa si también permanece en silencio durante el incendio? ¡Haz humo en la habitación ahora mismo!

2 Dentro del alcance de esta metodología, una falla de puerta trasera (por ejemplo, cuando la característica se comporta mal solo si la URL pasada es igual a https://www.formatmyharddrive.com/?confirm=yesofcourse ) no es razonable


Python ahora tiene un sistema de sugerencias tipo que hace análisis de código estático. Usando este sistema puede requerir que el primer argumento de una función como assertTrue sea ​​siempre booleano. El problema es que assertTrue no está definido por usted sino por el paquete unittest. Desafortunadamente, el paquete unittest no agregó sugerencias de tipo. Sin embargo, hay una forma razonablemente simple de hacerlo: simplemente defina su propio contenedor.

from unittest import TestCase class TestCaseWrapper(TestCase): def assertTrue(self, expr: bool, msg=None): #The ": bool" requires that the expr parameter is boolean. TestCase.assertTrue(self, expr, msg) class MyTestCase(TestCaseWrapper): def test_two_things_equal(self): self.assertTrue("a", "b") #Would give a warning about the type of "a".

Luego puede ejecutar el verificador de tipos así:

python -m mypy my_test_case.py

Esto debería entonces darle una advertencia sobre cómo "a" es una cadena, no un booleano. Lo bueno de esto es que se puede ejecutar automáticamente en un marco de prueba automatizado. Además, PyCharm comprobará los tipos en su código si los proporciona y resalta cualquier error.


Una solución para este tipo de problema es usar "pruebas de mutación" . Esta idea es generar automáticamente "mutantes" de su código, introduciendo pequeños cambios en él. Luego, su suite de prueba se ejecuta contra estos mutantes y, si es buena, la mayoría de ellos debe ser eliminada, lo que significa que su suite de pruebas detecta la mutación y las pruebas fallan.

Las pruebas de mutaciones realmente evalúan la calidad de sus pruebas. En su ejemplo, no se matarían mutantes y se detectaría fácilmente que hay algún problema con la prueba.

En python, hay varios marcos de mutación disponibles:


Una solución rápida sería proporcionar un Mixin que verifique la corrección:

import unittest class Mixin(object): def assertTrue(self, *args, **kwargs): if len(args) > 1: # TypeError is just an example, it could also do some warning/logging # stuff in here. raise TypeError(''msg should be given as keyword parameter.'') super().assertTrue(*args, **kwargs) class TestMixin(Mixin, unittest.TestCase): # Mixin before other parent classes def test_two_things_equal(self): self.assertTrue("a", "b")

Mixin también podría verificar si la expresión pasada es booleana:

class Mixin(object): def assertTrue(self, *args, **kwargs): if type(args[0]) is bool: raise TypeError(''expression should be a boolean'') if len(args) > 1: raise TypeError(''msg should be given as keyword parameter.'') super().assertTrue(*args, **kwargs)

Sin embargo, esto no es estático y requiere modificar manualmente las clases de prueba (agregar Mixin) y ejecutar las pruebas. También arrojará una gran cantidad de falsos positivos porque pasar el mensaje como argumento de palabra clave no es realmente común (al menos no donde lo he visto) y en muchos casos quiere verificar la veracidad implícita de la expresión en lugar del bool explícito. Me gusta comprobar si no hay vacío cuando a es una list , dict , etc.

También podría usar algún código de setUp , teardown que assertTrue método assertTrue para la clase en particular:

import unittest def decorator(func): def wrapper(*args, **kwargs): if len(args) > 1: raise TypeError() return func(*args, **kwargs) return wrapper class TestMixin(unittest.TestCase): def setUp(self): self._old = self.assertTrue self.assertTrue = decorator(self.assertTrue) def tearDown(self): self.assertTrue = self._old def test_two_things_equal(self): self.assertTrue("a", "b")

Pero una palabra de precaución antes de aplicar cualquiera de estos enfoques: tenga siempre cuidado antes de modificar las pruebas existentes. Desafortunadamente, las pruebas a veces están mal documentadas, por lo que no siempre es obvio qué es lo que evalúan y cómo lo evalúan. En algún momento, una prueba no tiene sentido y es seguro modificarla, pero a veces prueba una característica particular de una manera extraña y cuando la cambias, cambias lo que se está probando. Por lo menos, asegúrese de que no haya cambios en la cobertura cuando cambie el caso de prueba. Si es necesario, asegúrese de aclarar el propósito de la prueba actualizando el nombre del método, la documentación del método o los comentarios en línea.