Pruebas unitarias en Django

12

Realmente estoy luchando por escribir pruebas unitarias efectivas para un gran proyecto de Django. Tengo una cobertura de prueba razonablemente buena, pero me he dado cuenta de que las pruebas que he estado escribiendo son definitivamente pruebas de integración / aceptación, no pruebas unitarias en absoluto, y tengo partes críticas de mi aplicación que no se están probando de manera efectiva. Quiero arreglar esto lo antes posible.

Aquí está mi problema. Mi esquema es profundamente relacional y está muy orientado al tiempo, lo que le da a mi objeto modelo un alto acoplamiento interno y mucho estado. Muchos de mis métodos modelo consultan en función de los intervalos de tiempo, y tengo muchas actividades auto_now_adden campos con marca de tiempo. Entonces, tome un método que se vea así, por ejemplo:

def summary(self, startTime=None, endTime=None):
    # ... logic to assign a proper start and end time 
    # if none was provided, probably using datetime.now()

    objects = self.related_model_set.manager_method.filter(...)

    return sum(object.key_method(startTime, endTime) for object in objects)

¿Cómo se aborda probar algo como esto?

Aquí es donde estoy hasta ahora. Se me ocurre que el objetivo de la prueba de unidad debe tener un comportamiento simulado by key_methoden sus argumentos, ¿se está summaryfiltrando / agregando correctamente para producir un resultado correcto?

Burlarse de datetime.now () es bastante sencillo, pero ¿cómo puedo burlarme del resto del comportamiento?

  • Podría usar accesorios, pero he escuchado los pros y los contras de usar accesorios para construir mis datos (la falta de mantenimiento es una estafa que me afecta).
  • También podría configurar mis datos a través del ORM, pero eso puede ser limitante, porque entonces también tengo que crear objetos relacionados. Y el ORM no le permite jugar con los auto_now_addcampos manualmente.
  • Burlarse del ORM es otra opción, pero no solo es complicado burlarse de los métodos ORM profundamente anidados, sino que la lógica en el código ORM se burla de la prueba, y la burla parece hacer que la prueba realmente dependa de las partes internas y las dependencias del función bajo prueba.

Las tuercas más difíciles de descifrar parecen ser las funciones como esta, que se encuentran en algunas capas de modelos y funciones de nivel inferior y dependen mucho del tiempo, aunque estas funciones no sean muy complicadas. Mi problema general es que no importa cómo parezca dividirlo, mis pruebas se ven mucho más complejas que las funciones que están probando.

acjay
fuente
Debe escribir pruebas unitarias a partir de ahora, una que le ayude a detectar problemas de capacidad de prueba en su diseño antes de escribir el código de producción real.
Chedy2149
2
Eso es útil, pero en realidad no aborda el problema de cómo probar mejor las aplicaciones intrínsecamente con estado y ORM.
acjay
Tienes que abstraer la capa de persistencia
Chedy2149
1
Suena genial hipotéticamente, pero cuando se trata del mantenimiento del proyecto, creo que hay un costo no trivial para insertar una capa de persistencia personalizada entre la lógica de negocios y el Django ORM extremadamente bien documentado. De repente, las clases se llenan de un montón de pequeños métodos intermedios que deben ser refactorizados con el tiempo. Pero quizás esto esté justificado en lugares donde la capacidad de prueba es crítica.
acjay
Mira esto: vimeo.com/43612849 y esto: vimeo.com/15007792
Chedy2149

Respuestas:

6

Voy a seguir adelante y registrar una respuesta para lo que se me ocurrió hasta ahora.

Mi hipótesis es que para una función con acoplamiento y estado profundos, la realidad es que simplemente se necesitarán muchas líneas para controlar su contexto externo.

Así es aproximadamente mi caso de prueba, confiando en la biblioteca Mock estándar:

  1. Uso el ORM estándar para configurar la secuencia de eventos.
  2. Creo mi propio inicio datetimey subvierto los auto_now_addtiempos para que se ajusten a una línea de tiempo fija de mi diseño. Pensé que el ORM no permitía esto, pero funciona bien.
  3. Me aseguro de que la función bajo prueba se usa from datetime import datetimepara poder parchear datetime.now()solo esa función (si me burlo de toda la datetimeclase, el ORM se adapta).
  4. Creo mi propio reemplazo para object.key_method(), con una funcionalidad simple pero bien definida que depende de los argumentos. Quiero que dependa de los argumentos, porque de lo contrario podría no saber si la lógica de la función bajo prueba está funcionando. En mi caso, simplemente devuelve el número de segundos entre startTimey endTime. Lo parcheo envolviéndolo en una lambda y parcheando directamente para object.key_method()usar el new_callablekwarg de patch.
  5. Finalmente, ejecuto una serie de afirmaciones en varias llamadas summarycon diferentes argumentos para verificar la igualdad con los resultados calculados a mano esperados que representan el comportamiento dado del simulacrokey_method

No hace falta decir que esto es significativamente más largo y más complicado que la función en sí. Depende de la base de datos, y realmente no se siente como una prueba unitaria. Pero también está bastante desacoplado de los elementos internos de la función, solo su firma y dependencias. Así que creo que aún podría ser una prueba unitaria.

En mi aplicación, la función es bastante fundamental y está sujeta a refactorización para optimizar su rendimiento. Así que creo que el problema lo vale, a pesar de la complejidad. Pero todavía estoy abierto a mejores ideas sobre cómo abordar esto. Todo es parte de mi largo viaje hacia un estilo de desarrollo más basado en pruebas ...

acjay
fuente