¿Es normal pasar tanto tiempo, si no más, escribiendo pruebas que el código real?

211

Encuentro las pruebas mucho más difíciles y difíciles de escribir que el código real que están probando. No es inusual que pase más tiempo escribiendo la prueba que el código que está probando.

¿Es normal o estoy haciendo algo mal?

Las preguntas “ ¿Merecen la pena las pruebas unitarias o el desarrollo basado en pruebas? "," Pasamos más tiempo implementando pruebas funcionales que implementando el sistema en sí, ¿es esto normal? ”Y sus respuestas son más sobre si las pruebas valen la pena (como en" ¿deberíamos omitir las pruebas de escritura por completo? "). Si bien estoy convencido de que las pruebas son importantes, me pregunto si pasar más tiempo en las pruebas que el código real es normal o si solo soy yo.

A juzgar por la cantidad de puntos de vista, respuestas y votos positivos que recibió mi pregunta, solo puedo asumir que es una preocupación legítima que no se aborda en ninguna otra pregunta en el sitio web.

cargado de resortes
fuente
20
Anecdótico, pero descubro que paso casi tanto tiempo escribiendo pruebas como escribiendo código cuando hago TDD. Es cuando me deslizo y escribo pruebas después del hecho de que paso más tiempo en las pruebas que en el código.
RubberDuck
10
También pasa más tiempo leyendo que escribiendo su código.
Thorbjørn Ravn Andersen
27
Además, las pruebas son código real. Simplemente no envía esa parte a los clientes.
Thorbjørn Ravn Andersen
55
Idealmente, también pasarás más tiempo corriendo que escribiendo tu código. (De lo contrario, simplemente haría la tarea a mano.)
Joshua Taylor
55
@RubberDuck: experiencia opuesta aquí. A veces, cuando escribo pruebas después del hecho, el código y el diseño ya están bastante ordenados, por lo que no necesito reescribir demasiado el código y las pruebas. Por lo tanto, lleva menos tiempo escribir las pruebas. No es una regla, pero me sucede con bastante frecuencia.
Giorgio

Respuestas:

205

Recuerdo de un curso de ingeniería de software, que uno gasta ~ 10% del tiempo de desarrollo escribiendo código nuevo, y el otro 90% es depuración, prueba y documentación.

Dado que las pruebas unitarias capturan la depuración y el esfuerzo de prueba en el código (potencialmente automatizable), tendría sentido que se dedique más esfuerzo a ellos; el tiempo real empleado no debería ser mucho más que la depuración y las pruebas que se realizarían sin escribir las pruebas.

Finalmente, las pruebas también deberían funcionar como documentación. Uno debe escribir pruebas unitarias en la forma en que se pretende usar el código; es decir, las pruebas (y el uso) deben ser simples, poner las cosas complicadas en la implementación.

Si sus pruebas son difíciles de escribir, ¡el código que prueban probablemente sea difícil de usar!

esoterik
fuente
44
Puede ser un buen momento para analizar por qué el código es tan difícil de probar :) Intente "desenrollar" líneas complejas de múltiples funciones que no son estrictamente necesarias, como operadores binarios / ternarios anidados masivamente ... Realmente odio los binarios innecesarios / operador ternario que también tiene un operador binario / ternario como una de las rutas ...
Nelson
53
Ruego no estar de acuerdo con la última parte. Si está buscando una cobertura de prueba de unidad muy alta, debe cubrir los casos de uso que son raros y, a veces, directamente contra el uso previsto de su código. Escribir pruebas para esos casos de esquina puede ser la parte más lenta de toda la tarea.
otto
He dicho esto en otra parte, pero las pruebas unitarias tienden a durar mucho más, porque la mayoría del código tiende a seguir una especie de patrón de "Principio de Pareto": puede cubrir aproximadamente el 80% de su lógica con aproximadamente el 20% del código necesario para cubrir el 100% de su lógica (es decir, cubrir todos los casos límite requiere aproximadamente cinco veces más código de prueba de unidad). Por supuesto, dependiendo del marco, puede inicializar el entorno para múltiples pruebas, reduciendo el código general necesario, pero incluso eso requiere una planificación adicional. Acercarse al 100% de confianza requiere mucho más tiempo que simplemente probar las rutas principales.
phyrfox
2
@phyrfox Creo que es demasiado cauteloso, es más como "el otro 99% del código son casos extremos". Lo que significa que el otro 99% de las pruebas son para esos casos límite.
Más
@ Nelson Estoy de acuerdo en que los operadores ternarios anidados son difíciles de leer, pero no creo que hagan que las pruebas sean particularmente difíciles (una buena herramienta de cobertura le dirá si se ha perdido una de las combinaciones posibles). En mi opinión, el software es difícil de probar cuando está demasiado acoplado, o depende de datos cableados o datos que no se pasan como parámetro (por ejemplo, cuando una condición depende de la hora actual, y esto no se pasa como parámetro). Esto no está directamente relacionado con cuán "legible" es el código, aunque, por supuesto, todo lo demás es igual, ¡el código legible es mejor!
Andres F.
96

Está.

Incluso si solo realiza pruebas unitarias, no es inusual tener más código dentro de las pruebas que el código realmente probado. No tiene nada de malo.

Considere un código simple:

public void SayHello(string personName)
{
    if (personName == null) throw new NullArgumentException("personName");

    Console.WriteLine("Hello, {0}!", personName);
}

¿Cuáles serían las pruebas? Hay al menos cuatro casos simples para probar aquí:

  1. El nombre de la persona es null. ¿Se produce realmente una excepción? Eso es al menos tres líneas de código de prueba para escribir.

  2. El nombre de la persona es "Jeff". ¿Recibimos "Hello, Jeff!"respuesta? Eso son cuatro líneas de código de prueba.

  3. El nombre de la persona es una cadena vacía. ¿Qué salida esperamos? ¿Cuál es el resultado real? Pregunta secundaria: ¿coincide con los requisitos funcionales? Eso significa otras cuatro líneas de código para la prueba unitaria.

  4. El nombre de la persona es lo suficientemente corto para una cadena, pero demasiado largo para combinarlo con "Hello, "el signo de exclamación. ¿Qué pasa?

Esto requiere una gran cantidad de código de prueba. Además, las piezas de código más elementales a menudo requieren un código de configuración que inicializa los objetos necesarios para el código que se está probando, lo que a menudo también lleva a escribir trozos y simulaciones, etc.

Si la relación es muy grande, en cuyo caso puede verificar algunas cosas:

  • ¿Hay duplicación de código en las pruebas? El hecho de que sea un código de prueba no significa que el código deba duplicarse (copiado-pegado) entre pruebas similares: dicha duplicación dificultará el mantenimiento de esas pruebas.

  • ¿Hay pruebas redundantes? Como regla general, si elimina una prueba unitaria, la cobertura de la rama debería disminuir. Si no es así, puede indicar que la prueba no es necesaria, ya que las rutas ya están cubiertas por otras pruebas.

  • ¿Está probando solo el código que debe probar? No se espera que pruebe el marco subyacente de las bibliotecas de terceros, sino exclusivamente el código del proyecto en sí.

Con las pruebas de humo, las pruebas de sistema e integración, las pruebas funcionales y de aceptación y las pruebas de estrés y carga, agrega aún más código de prueba, por lo que tener cuatro o cinco LOC de pruebas para cada LOC del código real no es algo de lo que deba preocuparse.

Una nota sobre TDD

Si le preocupa el tiempo que lleva probar su código, es posible que lo esté haciendo mal, es decir, primero el código, luego las pruebas. En este caso, TDD puede ayudarlo al alentarlo a trabajar en iteraciones de 15 a 45 segundos, cambiando entre código y pruebas. De acuerdo con los defensores de TDD, acelera el proceso de desarrollo al reducir tanto la cantidad de pruebas que debe hacer como, y lo que es más importante, la cantidad de código comercial para escribir y especialmente reescribir para pruebas.


¹ Let n sea la longitud máxima de una cadena . Podemos llamar SayHelloy pasar por referencia una cadena de longitud n - 1 que debería funcionar bien. Ahora, en el Console.WriteLinepaso, el formato debe terminar con una cadena de longitud n + 8, lo que dará como resultado una excepción. Posiblemente, debido a los límites de memoria, incluso una cadena que contiene n / 2 caracteres dará lugar a una excepción. La pregunta que uno debe hacerse es si esta cuarta prueba es una prueba unitaria (parece una, pero puede tener un impacto mucho mayor en términos de recursos en comparación con las pruebas unitarias promedio) y si prueba el código real o el marco subyacente.

Arseni Mourzenko
fuente
55
No olvides que una persona también puede tener el nombre nulo. stackoverflow.com/questions/4456438/…
psatek
1
@JacobRaihle Supongo que @MainMa significa el valor de los personNameajustes en a string, pero el valor de personNamemás los valores concatenados se desbordan string.
woz
@JacobRaihle: Edité mi respuesta para explicar este punto. Ver la nota al pie.
Arseni Mourzenko
44
As a rule of thumb, if you remove a unit test, the branch coverage should decrease.Si escribo las cuatro pruebas que mencionó anteriormente y luego elimino la tercera prueba, ¿disminuirá la cobertura?
Vivek
3
"lo suficientemente largo" → "demasiado largo" (en el punto 4)?
Paŭlo Ebermann
59

Creo que es importante distinguir entre dos tipos de estrategias de prueba: pruebas unitarias y pruebas de integración / aceptación.

Aunque la prueba de la unidad es necesaria en algunos casos, con frecuencia se realiza sin remedio. Esto se ve exacerbado por las métricas sin sentido forzadas en los desarrolladores, como "100% de cobertura". http://www.rbcs-us.com/documents/Why-Most-Unit-Testing-is-Waste.pdf proporciona un argumento convincente para esto. Considere los siguientes problemas con las pruebas unitarias agresivas:

  • Una gran cantidad de pruebas sin sentido que no documentan un valor comercial, sino que simplemente existen para acercarse a esa cobertura del 100%. Donde trabajo, tenemos que escribir pruebas unitarias para fábricas que no hacen nada más que crear nuevas instancias de clases. No agrega valor. O esos largos métodos .equals () generados por Eclipse: no es necesario probarlos.
  • Para facilitar las pruebas, los desarrolladores subdividirían algoritmos complejos en unidades comprobables más pequeñas. Suena como una victoria, ¿verdad? No, si necesita tener 12 clases abiertas para seguir una ruta de código común. En estos casos, las pruebas unitarias pueden disminuir la legibilidad del código. Otro problema con esto es que si corta su código en partes demasiado pequeñas, terminará con una magnitud de clases (o partes de código) que parecen no tener justificación más allá de ser un subconjunto de otra parte del código.
  • La refactorización de código altamente cubierto puede ser difícil, ya que también necesita mantener la abundancia de pruebas unitarias que dependen de que funcione correctamente . Esto se ve exacerbado por las pruebas de unidad de comportamiento, donde parte de su prueba también verifica la interacción de los colaboradores de una clase (generalmente se burlan).

Las pruebas de integración / aceptación, por otro lado, son una parte muy importante de la calidad del software y, en mi experiencia, debería dedicar una cantidad significativa de tiempo a hacerlas bien.

Muchas tiendas han bebido el TDD kool-aid, pero como lo muestra el enlace anterior, varios estudios muestran que su beneficio no es concluyente.

firtydank
fuente
10
+1 para el punto de refactorización. Solía ​​trabajar en un producto heredado que había sido parcheado y pegado durante más de una década. Solo tratar de determinar las dependencias para un método en particular podría llevar la mayor parte del día, y luego tratar de descubrir cómo burlarse de ellos podría tomar aún más tiempo. No era inusual que un cambio de cinco líneas requiriera más de 200 líneas de código de prueba y tomara la mayor parte de una semana.
TMN
3
Esta. En la respuesta de MainMa, la prueba 4 no debe hacerse (fuera de un contexto académico), porque piense cómo ocurriría eso en la práctica ... si el nombre de una persona está cerca del tamaño máximo de una cadena, algo salió mal. No pruebe, en la mayoría de los casos no tiene una ruta de código para detectarlo. La respuesta adecuada es dejar que el marco arroje la excepción subyacente de memoria insuficiente, porque ese es el problema real.
Más
3
Te estoy animando hasta que "no necesites probar esos largos .equals()métodos generados por Eclipse". Escribí un instrumento de prueba para equals()y compareTo() github.com/GlenKPeterson/TestUtils~~V~~singular~~1st Casi todas las implementaciones que he probado era deficiente. ¿Cómo se usan las colecciones si equals()y hashCode()no funcionan juntas de manera correcta y eficiente? Estoy animando nuevamente por el resto de su respuesta y la he votado. Incluso reconozco que algunos métodos equals () generados automáticamente pueden no necesitar pruebas, pero he tenido tantos errores con implementaciones deficientes que me pone nervioso.
GlenPeterson
1
@GlenPeterson De acuerdo. Un colega mío escribió EqualsVerifier para este propósito. Ver github.com/jqno/equalsverifier
Tohnmeister
@ Ӎσᶎ no, todavía tiene que probar una entrada inaceptable, así es como las personas encuentran las vulnerabilidades de seguridad.
gbjbaanb
11

No se puede generalizar.

Si necesito implementar una fórmula o un algoritmo a partir del renderizado basado en la física, es muy posible que pase 10 horas en pruebas unitarias paranoides, ya que sé que el más mínimo error o falta de precisión puede dar lugar a errores que son casi imposibles de diagnosticar, meses después. .

Si solo quiero agrupar lógicamente algunas líneas de código y darle un nombre, solo usado en el alcance del archivo, es posible que no lo pruebe en absoluto (si insiste en escribir pruebas para cada función, sin excepción, los programadores podrían retroceder) para escribir la menor cantidad de funciones posible).

phresnel
fuente
Este es un punto de vista realmente valioso. La pregunta necesita más contexto para ser respondida completamente.
CLF
3

Sí, es normal si estás hablando de TDDing. Cuando tiene pruebas automatizadas, asegura el comportamiento deseado de su código. Cuando escribe sus pruebas primero, determina si el código existente ya tiene el comportamiento que desea.

Esto significa que:

  • Si escribe una prueba que falla, entonces corregir el código con la cosa más simple que funciona es más corto que escribir la prueba.
  • Si escribe una prueba que pasa, entonces no tiene código adicional para escribir, de hecho, pasa más tiempo para escribir la prueba que el código.

(Esto no tiene en cuenta la refactorización de código, que tiene como objetivo pasar menos tiempo escribiendo código posterior. Se equilibra con la refactorización de prueba, que tiene como objetivo pasar menos tiempo escribiendo pruebas posteriores).

Sí, también, si estás hablando de escribir exámenes después del hecho, pasarás más tiempo:

  • Determinando el comportamiento deseado.
  • Determinar formas de probar el comportamiento deseado.
  • Cumplimiento de dependencias de código para poder escribir las pruebas.
  • Código de corrección para pruebas que fallan.

De lo que gastarás en realidad escribiendo código.

Entonces sí, es una medida esperada.

Laurent LA RIZZA
fuente
3

Creo que es la parte más importante.

La prueba de unidad no siempre se trata de "ver si funciona bien", sino de aprender. Una vez que prueba algo lo suficiente, se "codifica" en su cerebro y finalmente reduce el tiempo de prueba de su unidad y puede encontrarse escribiendo clases y métodos completos sin probar nada hasta que termine.

Es por eso que una de las otras respuestas en esta página menciona que en un "curso" hicieron un 90% de pruebas, porque todos necesitaban aprender las advertencias de su objetivo.

La prueba de unidad no solo es un uso muy valioso de su tiempo, sino que literalmente mejora sus habilidades, es una buena manera de revisar su propio código nuevamente y encontrar un error lógico en el camino.

Jesse
fuente
2

Puede ser para muchas personas, pero depende.

Si está escribiendo pruebas primero (TDD), es posible que experimente cierta superposición en el tiempo dedicado a escribir la prueba que es realmente útil para escribir el código. Considerar:

  • Determinación de resultados e insumos (parámetros)
  • Convenciones de nombres
  • Estructura: dónde colocar las cosas.
  • Pensamiento viejo y llano

Al escribir pruebas después de escribir el código, puede descubrir que su código no es fácilmente comprobable, por lo que escribir pruebas es más difícil / lleva más tiempo.

La mayoría de los programadores han estado escribiendo código mucho más tiempo que las pruebas, por lo que espero que la mayoría de ellos no sean tan fluidos. Además, puede agregar el tiempo necesario para comprender y utilizar su marco de prueba.

Creo que tenemos que cambiar nuestra mentalidad sobre cuánto tiempo lleva codificar y cómo están involucradas las pruebas unitarias. Nunca lo mire a corto plazo y nunca compare el tiempo total para ofrecer una función particular porque debe considerar no solo escribir un código mejor / menos defectuoso, sino un código que sea más fácil de cambiar y aún así hacerlo mejor / menos con errores.

En algún momento, todos somos capaces de escribir código que es tan bueno, por lo que algunas de las herramientas y técnicas solo pueden ofrecer mucho para mejorar nuestras habilidades. No es como si pudiera construir una casa si solo tuviera una sierra guiada por láser.

JeffO
fuente
2

¿Es normal pasar tanto tiempo, si no más, escribiendo pruebas que el código real?

Sí lo es. Con algunas advertencias.

En primer lugar, es "normal" en el sentido de que la mayoría de las grandes tiendas funcionan de esta manera, por lo que incluso si esta forma fue completamente equivocada y tonta, el hecho de que la mayoría de las grandes tiendas trabajen de esta manera lo hace "normal".

Con esto no quiero decir que probarlo sea incorrecto. He trabajado en entornos sin pruebas y en entornos con pruebas obsesivo-compulsivas, y aún puedo decirles que incluso las pruebas obsesivo-compulsivas fueron mejores que ninguna prueba.

Y todavía no hago TDD (quién sabe, podría hacerlo en el futuro), pero hago la gran mayoría de mis ciclos de edición-ejecución-depuración ejecutando las pruebas, no la aplicación real, así que, naturalmente, trabajo mucho en mis pruebas, para evitar todo lo posible tener que ejecutar la aplicación real.

Sin embargo, tenga en cuenta que existen peligros en las pruebas excesivas, y específicamente en la cantidad de tiempo que se dedica a mantener las pruebas. (Estoy escribiendo principalmente esta respuesta para señalar específicamente esto).

En el prefacio de The Art of Unit Testing (Manning, 2009) de Roy Osherove , el autor admite haber participado en un proyecto que fracasó en gran parte debido a la tremenda carga de desarrollo impuesta por las pruebas unitarias mal diseñadas que tuvieron que mantenerse durante todo el proceso. duración del esfuerzo de desarrollo. Entonces, si te encuentras pasando demasiado tiempo haciendo nada más que mantener tus pruebas, esto no significa necesariamente que estás en el camino correcto, porque es "normal". Su esfuerzo de desarrollo podría haber entrado en un modo poco saludable, donde podría ser necesario un replanteamiento radical de su metodología de prueba para guardar el proyecto.

Mike Nakis
fuente
0

¿Es normal pasar tanto tiempo, si no más, escribiendo pruebas que el código real?

  • Sí, para escribir pruebas unitarias (Probar un módulo de forma aislada) si el código está altamente acoplado (heredado, no hay suficiente separación de preocupaciones , falta la inyección de dependencia , no se ha desarrollado tdd )
  • Sí, para escribir pruebas de integración / aceptación si la lógica que se probará solo es accesible a través del código gui
  • No para escribir pruebas de integración / aceptación siempre y cuando el código gui y la lógica de negocios estén separados (la prueba no necesita interactuar con la gui)
  • No para escribir pruebas unitarias si hay una separación de preocupaciones, inyección de dependencia, código desarrollado por prueba desarrollado (tdd)
k3b
fuente