¿Es más tolerable el código duplicado en las pruebas unitarias?

113

Arruiné varias pruebas unitarias hace algún tiempo cuando las revisé y las refactoricé para hacerlas más SECAS: la intención de cada prueba ya no estaba clara. Parece que hay una compensación entre la legibilidad y la mantenibilidad de las pruebas. Si dejo un código duplicado en las pruebas unitarias, son más legibles, pero luego, si cambio el SUT , tendré que rastrear y cambiar cada copia del código duplicado.

¿Está de acuerdo en que existe esta compensación? Si es así, ¿prefiere que sus pruebas sean legibles o fáciles de mantener?

Daryl Spitzer
fuente

Respuestas:

68

El código duplicado es un olor en el código de prueba unitario tanto como en otro código. Si tiene código duplicado en las pruebas, será más difícil refactorizar el código de implementación porque tiene una cantidad desproporcionada de pruebas para actualizar. Las pruebas deberían ayudarlo a refactorizar con confianza, en lugar de ser una gran carga que obstaculice su trabajo en el código que se está probando.

Si la duplicación está en una configuración fija, considere hacer más uso del setUpmétodo o proporcionar más (o más flexibles) métodos de creación .

Si la duplicación está en el código que manipula el SSP, entonces pregúntese por qué varias pruebas de las llamadas "unidades" ejercen exactamente la misma funcionalidad.

Si la duplicación está en las afirmaciones, quizás necesite algunas afirmaciones personalizadas . Por ejemplo, si varias pruebas tienen una serie de afirmaciones como:

assertEqual('Joe', person.getFirstName())
assertEqual('Bloggs', person.getLastName())
assertEqual(23, person.getAge())

Entonces quizás necesite un solo assertPersonEqualmétodo para poder escribir assertPersonEqual(Person('Joe', 'Bloggs', 23), person). (O quizás simplemente necesita sobrecargar el operador de igualdad en Person).

Como mencionas, es importante que el código de prueba sea legible. En particular, es importante que la intención de una prueba sea clara. Encuentro que si muchas pruebas se ven casi iguales (por ejemplo, tres cuartos de las líneas iguales o prácticamente iguales) es difícil detectar y reconocer las diferencias significativas sin leerlas y compararlas cuidadosamente. Entonces, encuentro que la refactorización para eliminar la duplicación ayuda a la legibilidad, porque cada línea de cada método de prueba es directamente relevante para el propósito de la prueba. Eso es mucho más útil para el lector que una combinación aleatoria de líneas que son directamente relevantes y líneas que son simplemente repetitivas.

Dicho esto, a veces las pruebas están ejercitando situaciones complejas que son similares pero aún significativamente diferentes, y es difícil encontrar una buena manera de reducir la duplicación. Use el sentido común: si cree que las pruebas son legibles y deja clara su intención, y se siente cómodo con tal vez la necesidad de actualizar más que un número teórico mínimo de pruebas al refactorizar el código invocado por las pruebas, entonces acepte la imperfección y muévase en algo más productivo. Siempre puede volver y refactorizar las pruebas más tarde, cuando llegue la inspiración.

estraperlista
fuente
30
"El código duplicado es un olor en el código de prueba unitaria tanto como en otro código". No. "Si tiene código duplicado en las pruebas, es más difícil refactorizar el código de implementación porque tiene una cantidad desproporcionada de pruebas para actualizar". Esto sucede porque está probando la API privada en lugar de la API pública.
15
Pero para evitar la duplicación de código en las pruebas unitarias, normalmente es necesario introducir una nueva lógica. No creo que las pruebas unitarias deban contener lógica porque entonces necesitarías pruebas unitarias de pruebas unitarias.
Petr Peller
@ user11617 defina "API privada" y "API pública". Según tengo entendido, la API pública es la API que es visible para el mundo externo / consumidores de terceros y tiene una versión explícita a través de SemVer o similar, cualquier otra cosa es privada. Con esta definición, casi todas las pruebas unitarias están probando "API privadas" y, por lo tanto, son más sensibles a la duplicación de código, lo que creo que es cierto.
KolA
@KolA "Public" no significa consumidores de terceros, esto no es una API web. La API pública de una clase se refiere a los métodos que están destinados a ser utilizados por el código del cliente (que normalmente no cambia / no debería cambiar tanto), generalmente los métodos "públicos". La API privada se refiere a la lógica y los métodos que se utilizan internamente. No se debe acceder a estos desde fuera de la clase. Esta es una de las razones por las que es importante encapsular correctamente la lógica en una clase utilizando modificadores de acceso o la convención en el lenguaje que se está utilizando.
Nathan
@Nathan cualquier paquete library / dll / nuget tiene consumidores de terceros, no tiene que ser una API web. A lo que me referí es que es muy común declarar clases públicas y miembros que se supone que no deben ser usados ​​directamente por los consumidores de la biblioteca (o en el mejor de los casos, hacerlos internos y anotar el ensamblaje con InternalsVisibleToAttribute) solo para permitir que las pruebas unitarias lleguen a ellos directamente. Conduce a un montón de pruebas junto con la implementación y las convierte en una carga más que una ventaja
KolA
186

La legibilidad es más importante para las pruebas. Si una prueba falla, quiere que el problema sea obvio. El desarrollador no debería tener que atravesar una gran cantidad de código de prueba fuertemente factorizado para determinar exactamente qué falló. No desea que su código de prueba se vuelva tan complejo que necesite escribir pruebas unitarias.

Sin embargo, eliminar la duplicación suele ser algo bueno, siempre que no oscurezca nada, y eliminar la duplicación en sus pruebas puede conducir a una mejor API. Solo asegúrese de no pasar del punto de rendimientos decrecientes.

Kristopher Johnson
fuente
xUnit y otros contienen un argumento de 'mensaje' en las llamadas de aserción. Es una buena idea poner frases significativas para permitir que los desarrolladores encuentren rápidamente los resultados de las pruebas fallidas.
Salida el
1
@seand Puede intentar explicar qué está comprobando su aserción, pero cuando falla y contiene un código algo oscurecido, el desarrollador deberá ir y desenrollarlo de todos modos. En mi opinión, es más importante tener un código que se autodescriba allí.
IgorK
1
@Kristopher,? ¿Por qué esta publicación es una wiki de la comunidad?
Pacerier
@Pacerier, no lo sé. Solía ​​haber reglas complicadas sobre las cosas que se convierten automáticamente en wiki de la comunidad.
Kristopher Johnson
Porque la legibilidad de los informes es más importante que las pruebas, especialmente cuando se realiza una integración o una prueba de extremo a extremo, los escenarios pueden ser lo suficientemente complejos como para evitar navegar un poquito, está bien encontrar la falla, pero nuevamente para mí, la falla en los informes debería Explique el problema suficientemente bien.
Anirudh
47

El código de implementación y las pruebas son animales diferentes y las reglas de factorización se aplican de manera diferente a ellos.

El código o la estructura duplicados son siempre un olor en el código de implementación. Cuando comience a tener un texto estándar en la implementación, debe revisar sus abstracciones.

Por otro lado, el código de prueba debe mantener un nivel de duplicación. La duplicación en el código de prueba logra dos objetivos:

  • Mantener las pruebas desacopladas. El acoplamiento de pruebas excesivo puede dificultar el cambio de una sola prueba fallida que necesita actualización porque el contrato ha cambiado.
  • Mantener las pruebas significativas de forma aislada. Cuando una sola prueba falla, debe ser razonablemente sencillo averiguar exactamente qué está probando.

Tiendo a ignorar la duplicación trivial en el código de prueba siempre que cada método de prueba sea más corto que aproximadamente 20 líneas. Me gusta cuando el ritmo de configuración-ejecución-verificación es evidente en los métodos de prueba.

Cuando la duplicación aumenta en la parte de "verificación" de las pruebas, a menudo es beneficioso definir métodos de afirmación personalizados. Por supuesto, esos métodos aún deben probar una relación claramente identificada que puede hacerse evidente en el nombre del método: assertPegFitsInHole-> bueno, assertPegIsGood-> malo.

Cuando los métodos de prueba se vuelven largos y repetitivos, a veces me resulta útil definir plantillas de prueba para completar los espacios en blanco que toman algunos parámetros. Luego, los métodos de prueba reales se reducen a una llamada al método de plantilla con los parámetros adecuados.

En cuanto a muchas cosas relacionadas con la programación y las pruebas, no hay una respuesta clara. Necesita desarrollar un gusto y la mejor manera de hacerlo es cometer errores.

ddaa
fuente
8

Estoy de acuerdo. La compensación existe pero es diferente en diferentes lugares.

Es más probable que refactorice el código duplicado para configurar el estado. Pero es menos probable que refactorice la parte de la prueba que realmente ejercita el código. Dicho esto, si ejercitar el código siempre requiere varias líneas de código, entonces podría pensar que es un olor y refactorizar el código real bajo prueba. Y eso mejorará la legibilidad y el mantenimiento tanto del código como de las pruebas.

Stucampbell
fuente
Creo que es una buena idea. Si tiene mucha duplicación, vea si puede refactorizar para crear un "dispositivo de prueba" común bajo el cual se puedan ejecutar muchas pruebas. Esto eliminará el código de instalación / desmontaje duplicado.
Programador fuera de la ley
8

Puede reducir la repetición utilizando varios tipos diferentes de métodos de utilidad de prueba .

Soy más tolerante con la repetición en el código de prueba que en el código de producción, pero a veces me ha frustrado. Cuando cambia el diseño de una clase y tiene que volver atrás y ajustar 10 métodos de prueba diferentes que realizan los mismos pasos de configuración, es frustrante.

Don Kirkby
fuente
6

Jay Los campos que acuñó la frase de que "DSL debe estar húmedo, no seco", donde DAMP medios descriptivo y frases significativas . Creo que lo mismo se aplica también a las pruebas. Obviamente, demasiada duplicación es mala. Pero eliminar la duplicación a toda costa es aún peor. Las pruebas deben actuar como especificaciones que revelen la intención. Si, por ejemplo, especifica la misma característica desde varios ángulos diferentes, es de esperar una cierta cantidad de duplicación.

Jörg W Mittag
fuente
3

AMO rspec por esto:

Tiene 2 cosas para ayudar:

  • grupos de ejemplo compartidos para probar el comportamiento común.
    puede definir un conjunto de pruebas, luego 'incluir' ese conjunto en sus pruebas reales.

  • contextos anidados.
    Básicamente, puede tener un método de 'configuración' y 'desmontaje' para un subconjunto específico de sus pruebas, no solo para todas las de la clase.

Cuanto antes .NET / Java / otros marcos de prueba adopten estos métodos, mejor (o podría usar IronRuby o JRuby para escribir sus pruebas, que personalmente creo que es la mejor opción)

Orion Edwards
fuente
3

Creo que el código de prueba requiere un nivel similar de ingeniería que normalmente se aplicaría al código de producción. Ciertamente puede haber argumentos a favor de la legibilidad y estoy de acuerdo en que es importante.

Sin embargo, en mi experiencia, encuentro que las pruebas bien factorizadas son más fáciles de leer y comprender. Si hay 5 pruebas en las que cada una tiene el mismo aspecto, excepto por una variable que ha cambiado y la afirmación al final, puede ser muy difícil encontrar cuál es ese único elemento diferente. De manera similar, si se factoriza de modo que solo la variable que está cambiando sea visible y la aserción, entonces es fácil averiguar qué está haciendo la prueba de inmediato.

Encontrar el nivel adecuado de abstracción cuando se realizan pruebas puede ser difícil y creo que vale la pena hacerlo.

Kevin Londres
fuente
2

No creo que haya una relación entre un código más duplicado y legible. Creo que su código de prueba debería ser tan bueno como su otro código. El código no repetitivo es más legible que el código duplicado cuando se hace bien.

Paco
fuente
2

Idealmente, las pruebas unitarias no deberían cambiar mucho una vez que están escritas, por lo que me inclinaría hacia la legibilidad.

Hacer que las pruebas unitarias sean lo más discretas posible también ayuda a mantener las pruebas enfocadas en la funcionalidad específica a la que se dirigen.

Dicho esto, tiendo a intentar reutilizar ciertos fragmentos de código que termino usando una y otra vez, como el código de configuración que es exactamente el mismo en un conjunto de pruebas.

17 de 26
fuente
2

"los refactorizó para hacerlos más SECOS - la intención de cada prueba ya no estaba clara"

Parece que tuvo problemas para hacer la refactorización. Solo estoy adivinando, pero si resultó menos claro, ¿no significa eso que todavía tienes más trabajo por hacer para tener pruebas razonablemente elegantes que son perfectamente claras?

Es por eso que las pruebas son una subclase de UnitTest, para que pueda diseñar buenos conjuntos de pruebas que sean correctos, fáciles de validar y claros.

En la antigüedad teníamos herramientas de prueba que usaban diferentes lenguajes de programación. Fue difícil (o imposible) diseñar pruebas agradables y fáciles de trabajar.

Tienes todo el poder de, sea cual sea el lenguaje que estés usando, Python, Java, C #, así que usa bien ese lenguaje. Puede lograr un código de prueba atractivo, claro y no demasiado redundante. No hay compensación.

S.Lott
fuente