Pruebas vs No te repitas (SECO)

11

¿Por qué es tan recomendable que te repitas escribiendo pruebas?

Parece que las pruebas básicamente expresan lo mismo que el código, y por lo tanto es un duplicado (en concepto, no implementación) del código. ¿El objetivo final de DRY no incluiría la eliminación de todo el código de prueba?

John Tseng
fuente

Respuestas:

24

Creo que esta es una idea falsa de cualquier manera que se me ocurra.

El código de prueba que prueba el código de producción no es en absoluto similar. Lo demostraré en python:

def multiply(a, b):
    """Multiply ``a`` by ``b``"""
    return a*b

Entonces una prueba simple sería:

def test_multiply():
    assert multiply(4, 5) == 20

Ambas funciones tienen una definición similar, pero ambas hacen cosas muy diferentes. No hay código duplicado aquí. ;-)

También ocurre que las personas escriben pruebas duplicadas que esencialmente tienen una afirmación por función de prueba. Esto es una locura y he visto gente haciendo esto. Esta es una mala práctica.

def test_multiply_1_and_3():
    """Assert that a multiplication of 1 and 3 is 3."""
    assert multiply(1, 3) == 3

def test_multiply_1_and_7():
    """Assert that a multiplication of 1 and 7 is 7."""
    assert multiply(1, 7) == 7

def test_multiply_3_and_4():
    """Assert that a multiplication of 3 and 4 is 12."""
    assert multiply(3, 4) == 12

Imagine hacer esto para más de 1000 líneas de código efectivas. En su lugar, realiza una prueba por "característica":

def test_multiply_positive():
    """Assert that positive numbers can be multiplied."""
    assert multiply(1, 3) == 3
    assert multiply(1, 7) == 7
    assert multiply(3, 4) == 12

def test_multiply_negative():
    """Assert that negative numbers can be multiplied."""
    assert multiply(1, -3) == -3
    assert multiply(-1, -7) == 7
    assert multiply(-3, 4) == -12

Ahora, cuando se agregan / eliminan funciones, solo tengo que considerar agregar / eliminar una función de prueba.

Es posible que haya notado que no he aplicado forbucles. Esto se debe a que repetir algunas cosas es bueno. Cuando hubiera aplicado bucles, el código sería mucho más corto. Pero cuando falla una aserción, podría ofuscar la salida que muestra un mensaje ambiguo. Si esto ocurre entonces sus pruebas serán menos útiles y que se necesita un depurador para inspeccionar donde las cosas van mal.

siebz0r
fuente
8
Se recomienda técnicamente una afirmación por prueba porque significa que múltiples problemas no se mostrarán como una sola falla. Sin embargo, en la práctica, creo que la agregación cuidadosa de aserciones reduce la cantidad de código repetido y casi nunca me apego a una aserción por guía de prueba.
Rob Church
@ pink-diamond-square Veo que NUnit no deja de probar después de que una afirmación falla (lo cual creo que es extraño). En ese caso específico, es mejor tener una afirmación por prueba. Si un marco de pruebas unitarias detiene la prueba después de una afirmación fallida, las aserciones múltiples son mejores.
siebz0r
3
NUnit no detiene todo el conjunto de pruebas, pero esa prueba sí se detiene a menos que tome medidas para evitarla (puede detectar la excepción que arroja, lo que ocasionalmente es útil). El punto que creo que están haciendo es que si escribe pruebas que incluyen más de una afirmación, no obtendrá toda la información que necesita para corregir el problema. Para seguir su ejemplo, imagine que a esta función de multiplicar no le gusta el número 3. En este caso, assert multiply(1,3)fallaría, pero tampoco obtendría el informe de prueba fallido assert multiply(3,4).
Rob Church
Solo pensé en plantearlo porque una sola afirmación por prueba es, por lo que he leído en el mundo .net, la "buena práctica" y las afirmaciones múltiples son "uso pragmático". Se ve un poco diferente en la documentación de Python donde el ejemplo def test_shufflerealiza dos afirmaciones.
Rob Church
Estoy de acuerdo y en desacuerdo: D Aquí hay claramente una repetición: assert multiply(*, *) == *por lo que podría definir una assert_multiplyfunción. En el escenario actual, no importa el recuento de filas y la legibilidad, pero con pruebas más largas puede reutilizar aserciones complicadas, accesorios, código generador de accesorios, etc. No sé si esta es una práctica recomendada, pero generalmente lo hago. esta.
inf3rno
10

Parece que las pruebas básicamente expresan lo mismo que el código, y por lo tanto es un duplicado

No, esto no es verdad.

Las pruebas tienen un propósito diferente al de su implementación:

  • Las pruebas aseguran que su implementación funcione.
  • Sirven como documentación: al observar las pruebas, verá los contratos que debe cumplir su código, es decir, qué entrada devuelve qué salida, cuáles son los casos especiales, etc.
  • Además, sus pruebas garantizan que a medida que agrega nuevas funciones, su funcionalidad existente no se rompe.
Uooo
fuente
4

No. DRY se trata de escribir código solo una vez para realizar una tarea en particular, las pruebas son validación de que la tarea se está realizando correctamente. Es algo parecido a un algoritmo de votación, donde obviamente usar el mismo código sería inútil.

jmoreno
fuente
2

¿El objetivo final de DRY no incluiría la eliminación de todo el código de prueba?

No, el objetivo final de DRY en realidad significaría la eliminación de todo el código de producción .

Si nuestras pruebas pudieran ser especificaciones perfectas de lo que queremos que haga el sistema, solo tendríamos que generar el código de producción correspondiente (o binarios) automáticamente, eliminando efectivamente la base del código de producción per se.

Esto es en realidad lo que los enfoques como la arquitectura basada en modelos afirman lograr: una única fuente de verdad diseñada por humanos de la cual todo se deriva por computación.

No creo que lo contrario (deshacerse de todas las pruebas) sea deseable porque:

  • Debe resolver el desajuste de impedancia entre la implementación y la especificación. El código de producción puede transmitir la intención hasta cierto punto, pero nunca será tan fácil razonar sobre pruebas tan bien expresadas. Los seres humanos necesitamos esa visión más elevada de por qué estamos construyendo cosas. Incluso si no realiza pruebas debido a DRY, las especificaciones probablemente tendrán que escribirse en los documentos de todos modos, lo cual es una bestia definitivamente más peligrosa en términos de desajuste de impedancia y desincronización de código si me pregunta.
  • Mientras que el código de producción es fácilmente derivable de las especificaciones ejecutables correctas (suponiendo tiempo suficiente), un conjunto de pruebas es mucho más difícil de reconstituir a partir del código final de un programa. Las especificaciones no aparecen claramente solo mirando el código, porque las interacciones entre unidades de código en tiempo de ejecución son difíciles de distinguir. Es por eso que nos cuesta tanto lidiar con aplicaciones heredadas sin prueba. En otras palabras: si desea que su aplicación sobreviva más de unos pocos meses, probablemente sea mejor que pierda el disco duro que aloja su base de código de producción que el que tiene su conjunto de pruebas.
  • Es mucho más fácil introducir un error por accidente en el código de producción que en el código de prueba. Y dado que el código de producción no se autoverifica (aunque esto puede abordarse con Diseño por contrato o sistemas de tipo más rico), aún necesitamos algún programa externo para probarlo y advertirnos si ocurre una regresión.
guillaume31
fuente
1

Porque a veces repetirlo está bien. Ninguno de estos principios está destinado a ser tomado en todas las circunstancias sin preguntas o contexto. A veces he escrito pruebas contra una versión ingenua (y lenta) de un algoritmo, que es una violación bastante clara de DRY, pero definitivamente beneficiosa.

U2EF1
fuente
1

Dado que las pruebas unitarias consisten en dificultar los cambios involuntarios , a veces también pueden dificultar los cambios intencionales . Este hecho está relacionado con el principio DRY.

Por ejemplo, si tiene una función MyFunctionque se llama en el código de producción en un solo lugar y escribe 20 pruebas unitarias para ella, puede terminar fácilmente teniendo 21 lugares en su código donde se llama esa función. Ahora, cuando tiene que cambiar la firma de MyFunction, o la semántica, o ambos (porque algunos requisitos cambian), tiene 21 lugares para cambiar en lugar de solo uno. Y la razón es una violación del principio DRY: repitió (al menos) la misma llamada de función MyFunction21 veces.

El enfoque correcto para tal caso es aplicar el principio DRY a su código de prueba también: al escribir 20 pruebas unitarias, encapsule las llamadas MyFunctionen sus pruebas unitarias en solo unas pocas funciones auxiliares (idealmente solo una), que son utilizadas por el 20 pruebas unitarias. Idealmente, terminas con solo dos lugares en tu código de llamada MyFunction: uno de tu código de producción y otro de tus pruebas unitarias. Entonces, cuando tenga que cambiar la firma de MyFunctionmás adelante, tendrá solo unos pocos lugares para cambiar en sus pruebas.

"Algunos lugares" siguen siendo más que "un lugar" (lo que se obtiene sin pruebas unitarias), pero las ventajas de tener pruebas unitarias deberían ser mayores que la ventaja de tener menos código para cambiar (de lo contrario, se realizan pruebas unitarias completamente incorrecto).

Doc Brown
fuente
0

Uno de los mayores desafíos para crear software es capturar los requisitos; eso es para responder la pregunta, "¿qué debe hacer este software?" El software necesita requisitos exactos para definir con precisión lo que el sistema necesita hacer, pero quienes definen las necesidades de sistemas y proyectos de software a menudo incluyen personas que no tienen un fondo de software o formal (matemático). La falta de rigor en la definición de requisitos obligó al desarrollo de software a encontrar una manera de validar el software a los requisitos.

El equipo de desarrollo se encontró traduciendo la descripción coloquial de un proyecto en requisitos más rigurosos. La disciplina de prueba se ha unido como el punto de control para el desarrollo de software, para cerrar la brecha entre lo que un cliente dice que quiere y qué software entiende que quiere. Tanto los desarrolladores de software como el equipo de calidad / pruebas forman la comprensión de la especificación (informal), y cada uno (independientemente) escribe software o pruebas para garantizar que su comprensión se ajuste. Agregar a otra persona para comprender los requisitos (imprecisos) agregó preguntas y una perspectiva diferente para perfeccionar aún más la precisión de los requisitos.

Como siempre ha habido pruebas de aceptación, era natural ampliar el rol de prueba para escribir pruebas automatizadas y unitarias. El problema era que eso significaba contratar programadores para hacer pruebas y, por lo tanto, redujo la perspectiva del aseguramiento de la calidad a los programadores que realizan pruebas.

Dicho todo esto, probablemente estás haciendo una prueba incorrecta si tus pruebas difieren poco de los programas reales. La sugerencia de Msdy sería centrarse más en qué en las pruebas y menos en cómo.

La ironía es que, en lugar de capturar una especificación formal de los requisitos de la descripción coloquial, la industria ha optado por implementar pruebas puntuales como código para automatizar las pruebas. En lugar de producir requisitos formales para los cuales el software podría construirse para responder, el enfoque adoptado ha sido probar algunos puntos, en lugar de abordar el software de construcción utilizando la lógica formal. Esto es un compromiso, pero ha sido bastante efectivo y relativamente exitoso.

ChuckCottrill
fuente
0

Si cree que su código de prueba es demasiado similar a su código de implementación, esto puede ser una indicación de que está utilizando en exceso un marco burlón. Las pruebas simuladas a un nivel demasiado bajo pueden terminar con la configuración de prueba muy parecida al método que se está probando. Intente escribir pruebas de nivel superior que sean menos propensas a romperse si cambia su implementación (sé que esto puede ser difícil, pero si puede administrarlo tendrá un conjunto de pruebas más útil como resultado).

Jules
fuente
0

Las pruebas unitarias no deben incluir una duplicación del código bajo prueba, como ya se ha señalado.

Sin embargo, agregaría que las pruebas unitarias generalmente no son tan SECAS como el código de "producción", porque la configuración tiende a ser similar (pero no idéntica) en todas las pruebas ... especialmente si tiene un número significativo de dependencias de las que se está burlando / fingiendo.
Por supuesto, es posible refactorizar este tipo de cosas en un método de configuración común (o conjunto de métodos de configuración) ... pero he descubierto que esos métodos de configuración tienden a tener largas listas de parámetros y son bastante frágiles.

Así que sé pragmático. Si puede consolidar el código de configuración sin comprometer la mantenibilidad, hágalo por todos los medios. Pero si la alternativa es un conjunto complejo y frágil de métodos de configuración, un poco de repetición en sus métodos de prueba está bien.

Un evangelista local de TDD / BDD lo expresa así:
"Su código de producción debe estar SECO. Pero está bien que sus pruebas estén 'húmedas'".

David
fuente
0

Parece que las pruebas básicamente expresan lo mismo que el código, y por lo tanto es un duplicado (en concepto, no implementación) del código.

Esto no es cierto, las pruebas describen los casos de uso, mientras que el código describe un algoritmo que pasa los casos de uso, por lo que es más general. Con TDD, comienza a escribir casos de uso (probablemente basados ​​en la historia del usuario) y luego implementa el código necesario para pasar estos casos de uso. Entonces escribe una pequeña prueba, una pequeña porción de código, y luego refactoriza si es necesario para deshacerse de las repeticiones. Asi es como funciona.

Por pruebas puede haber repeticiones también. Por ejemplo, puede reutilizar dispositivos, código generador de dispositivos, afirmaciones complicadas, etc. Por lo general, hago esto para evitar errores en las pruebas, pero generalmente olvido probar primero si una prueba realmente falla y realmente puede arruinar el día. , cuando está buscando el error en el código durante media hora y la prueba es incorrecta ... xD

inf3rno
fuente