¿Cómo facilitan las pruebas unitarias el diseño?

43

Nuestro colega promueve las pruebas unitarias de escritura como algo que realmente nos ayuda a refinar nuestro diseño y refactorizar las cosas, pero no veo cómo. Si estoy cargando un archivo CSV y lo analizo, ¿cómo me ayudará la prueba unitaria (validando los valores en los campos) a verificar mi diseño? Mencionó el acoplamiento y la modularidad, etc., pero para mí no tiene mucho sentido, aunque no tengo muchos antecedentes teóricos.

No es lo mismo que la pregunta que marcó como duplicado, me interesarían ejemplos reales de cómo esto ayuda, no solo la teoría que dice "ayuda". Me gusta la respuesta a continuación y el comentario, pero me gustaría obtener más información.

Usuario039402
fuente
77
Posible duplicado de ¿TDD conduce al buen diseño?
mosquito
3
La respuesta a continuación es realmente todo lo que necesita saber. Sentado junto a esas personas que escriben fábricas de fábricas inyectadas con dependencia de raíz agregada todo el día, hay un tipo que silenciosamente escribe código simple contra pruebas unitarias que funciona correctamente, es fácil de verificar y ya está documentado.
Robert Harvey
44
@gnat haciendo pruebas unitarias no implica automáticamente TDD, es una pregunta diferente
Joppe
11
"prueba unitaria (validando los valores en los campos)" : parece que combina pruebas unitarias con validación de entrada.
jonrsharpe
1
@jonrsharpe Dado que se trata de un código que analiza un archivo CSV, puede estar hablando de una prueba de unidad real que verifica que una determinada cadena CSV proporciona el resultado esperado.
JollyJoker

Respuestas:

3

Las pruebas unitarias no solo facilitan el diseño, sino que ese es uno de sus principales beneficios.

Escribir prueba primero elimina la modularidad y la estructura de código limpia.

Cuando escriba su prueba de código primero, encontrará que cualquier "condición" de una unidad de código dada se expulsa naturalmente a las dependencias (generalmente a través de simulaciones o apéndices) cuando las asume en su código.

"Dada la condición x, espere comportamiento y", a menudo se convertirá en un trozo para suministrar x(que es un escenario en el que la prueba necesita verificar el comportamiento del componente actual) y yse convertirá en un simulacro, una llamada a la que se verificará en el final de la prueba (a menos que sea un "debería regresar y", en cuyo caso la prueba solo verificará el valor de retorno explícitamente).

Luego, una vez que esta unidad se comporta como se especifica, pasa a escribir las dependencias (para xy y) que ha descubierto.

Esto hace que escribir código limpio y modular sea un proceso muy fácil y natural, donde de lo contrario a menudo es fácil difuminar las responsabilidades y unir los comportamientos sin darse cuenta.

Escribir pruebas más adelante le dirá cuándo su código está mal estructurado.

Cuando escribir pruebas para un fragmento de código se vuelve difícil porque hay demasiadas cosas para tropezar o burlarse, o porque las cosas están demasiado unidas, sabes que tienes mejoras que hacer en tu código.

Cuando "cambiar las pruebas" se convierte en una carga porque hay tantos comportamientos en una sola unidad, sabe que tiene que hacer mejoras en su código (o simplemente en su enfoque para escribir pruebas, pero este no es el caso en mi experiencia) .

Cuando sus escenarios se vuelven demasiado complicados ("if xand yand zthen ...") porque necesita abstraer más, sabe que tiene que hacer mejoras en su código.

Cuando terminas con las mismas pruebas en dos dispositivos diferentes debido a la duplicación y la redundancia, sabes que tienes mejoras que hacer en tu código.

Aquí hay una excelente charla de Michael Feathers que demuestra la estrecha relación entre la capacidad de prueba y el diseño en el código (originalmente publicado por displayName en los comentarios). La charla también aborda algunas quejas comunes y conceptos erróneos sobre el buen diseño y la capacidad de prueba en general.

Hormiga P
fuente
@SSECommunity: con solo 2 votos a favor a partir de hoy, esta respuesta es muy fácil de pasar por alto. Recomiendo encarecidamente la charla de Michael Feathers que se ha vinculado en esta respuesta.
displayName
103

Lo mejor de las pruebas unitarias es que le permiten usar su código de la misma manera que otros programadores usarán su código.

Si su código es incómodo para la prueba unitaria, entonces probablemente será incómodo usarlo. Si no puede inyectar dependencias sin saltar a través de aros, entonces su código probablemente será inflexible de usar. Y si necesita pasar mucho tiempo configurando datos o averiguar en qué orden hacer las cosas, su código bajo prueba probablemente tenga demasiado acoplamiento y será difícil trabajar con él.

Telastyn
fuente
77
Gran respuesta. Siempre me gusta pensar en mis pruebas como el primer cliente del código; si es doloroso escribir las pruebas, será doloroso escribir código que consuma la API o lo que sea que esté desarrollando.
Stephen Byrne
41
En mi experiencia, la mayoría de las pruebas unitarias no "usan su código como otros programadores usarán su código". Usan su código como las pruebas unitarias usarán el código. Es cierto que revelarán muchos defectos graves. Pero una API diseñada para pruebas unitarias puede no ser la API más adecuada para uso general. Las pruebas unitarias escritas de manera simplista a menudo requieren que el código subyacente exponga demasiados elementos internos. Nuevamente, según mi experiencia, estaría interesado en saber cómo manejó esto. (Vea mi respuesta a continuación)
user949300
77
@ user949300 - No creo mucho en la prueba primero. Mi respuesta se basa primero en la idea del código (y ciertamente del diseño). Las API no deben diseñarse para pruebas unitarias, deben diseñarse para su cliente. Las pruebas unitarias ayudan a aproximarse a su cliente, pero son una herramienta. Están ahí para servirle, no al revés. Y ciertamente no te van a impedir que hagas código malo.
Telastyn
3
El mayor problema con las pruebas unitarias en mi experiencia es que escribir las buenas es tan difícil como escribir un buen código en primer lugar. Si no puede distinguir el código bueno del código malo, escribir pruebas unitarias no mejorará su código. Al escribir la prueba unitaria, debe poder distinguir entre lo que es un uso suave y agradable y uno "incómodo" o difícil. Pueden hacer que use un poco su código, pero no lo obligan a reconocer que lo que está haciendo es malo.
jpmc26
2
@ user949300: el ejemplo clásico que tenía en mente aquí es un repositorio que necesita una conexión. Suponga que lo expone como una propiedad pública que se puede escribir, y tiene que configurarlo después de crear un nuevo () repositorio. La idea es que después de la 5ta o 6ta vez que haya escrito una prueba que se olvide de dar ese paso, y por lo tanto se cuelgue, estará "naturalmente" inclinado a obligar a que la conexión sea una invariante de clase, aprobada en el constructor, lo que hace que su API mejor y haciendo más probable que se pueda escribir código de producción que evite esta trampa. No es una garantía, pero ayuda, en mi opinión.
Stephen Byrne
31

Me tomó bastante tiempo darme cuenta, pero el beneficio real (editar: para mí, su kilometraje puede variar) de hacer un desarrollo impulsado por pruebas ( usando pruebas unitarias) es que tiene que hacer el diseño API por adelantado .

Un enfoque típico para el desarrollo es descubrir primero cómo resolver un problema determinado y, con ese conocimiento y el diseño de implementación inicial, de alguna manera invocar su solución. Esto puede dar algunos resultados bastante interesantes.

Al hacer TDD, primero debe escribir el código que usará su solución. Parámetros de entrada y salida esperada para que pueda asegurarse de que sea correcto Eso a su vez requiere que descubras lo que realmente necesitas que haga, para que puedas crear pruebas significativas. Entonces y solo entonces implementas la solución. También es mi experiencia que cuando sabes exactamente lo que se supone que debe lograr tu código, se vuelve más claro.

Luego, después de la implementación, las pruebas unitarias lo ayudan a garantizar que la refactorización no rompa la funcionalidad, y brindan documentación sobre cómo usar su código (¡lo cual sabe que es correcto cuando pasó la prueba!). Pero estos son secundarios: el mayor beneficio es la mentalidad en la creación del código en primer lugar.

Thorbjørn Ravn Andersen
fuente
Ciertamente es un beneficio, pero no creo que sea el beneficio "real": el beneficio real proviene del hecho de que escribir pruebas para su código empuja naturalmente las "condiciones" a las dependencias y anula la sobreinyección de dependencias (promoviendo aún más la abstracción ) antes de que comience.
Ant P
El problema es que escribe un conjunto completo de pruebas por adelantado que coinciden con esa API, luego no funciona exactamente como es necesario y tiene que volver a escribir su código y todas las pruebas. Para las API públicas, es probable que no cambien y este enfoque está bien. Sin embargo, las API de código que sólo se utilizan internamente sí cambia mucho, ya que encontrar la manera de implementar una característica que necesita una gran cantidad de APIs privadas semi trabajando juntos
Juan Mendes
@AntP Sí, esto es parte del diseño de la API.
Thorbjørn Ravn Andersen
@JuanMendes Esto no es infrecuente y esas pruebas deberán cambiarse, al igual que cualquier otro código cuando cambie los requisitos. Un IDE buena le ayudará a refactoriza las clases como parte del trabajo que se realiza de forma automática cuando se cambia el método de firmas, etc.
Thorbjørn Ravn Andersen
@JuanMendes si estás escribiendo buenas pruebas y unidades pequeñas, el impacto del efecto que estás describiendo es pequeño o nulo en la práctica.
Ant P
6

Estoy de acuerdo al 100% en que las pruebas unitarias ayudan a "ayudarnos a refinar nuestro diseño y refactorizar las cosas".

Tengo dos opiniones sobre si te ayudan a hacer el diseño inicial . Sí, revelan defectos obvios y te obligan a pensar en "¿cómo puedo hacer que el código sea verificable"? Esto debería conducir a menos efectos secundarios, configuración y configuraciones más fáciles, etc.

Sin embargo, en mi experiencia, las pruebas unitarias excesivamente simplistas, escritas antes de que realmente comprenda cuál debería ser el diseño, (es cierto que es una exageración de la TDD de núcleo duro, pero con demasiada frecuencia los codificadores escriben una prueba antes de pensar demasiado) a menudo conducen a la anemia. modelos de dominio que exponen demasiados elementos internos.

Mi experiencia con TDD fue hace varios años, por lo que estoy interesado en saber qué técnicas más nuevas podrían ayudar a escribir pruebas que no sesguen demasiado el diseño subyacente. Gracias.

user949300
fuente
Un gran número de parámetros del método son un olor a código y un defecto de diseño.
Sufian
5

La prueba de unidad le permite ver cómo funcionan las interfaces entre funciones y, a menudo, le da una idea de cómo mejorar tanto el diseño local como el diseño general. Además, si desarrolla sus pruebas unitarias mientras desarrolla su código, tiene un conjunto de pruebas de regresión listo. No importa si está desarrollando una interfaz de usuario o una biblioteca de fondo.

Una vez que se desarrolla el programa (con pruebas unitarias), a medida que se descubren errores, puede agregar pruebas para confirmar que los errores se corrigieron.

Yo uso TDD para algunos de mis proyectos. Puse un gran esfuerzo en la elaboración de ejemplos que extraigo de libros de texto o de documentos que se consideran correctos, y pruebo el código que estoy desarrollando utilizando estos ejemplos. Cualquier malentendido que tenga sobre los métodos se hace muy evidente.

Tiendo a ser un poco más flexible que algunos de mis colegas, ya que no me importa si el código se escribe primero o si la prueba se escribe primero.

Robert Baron
fuente
Esa es una gran respuesta para mí. ¿Le importaría poner algunos ejemplos, por ejemplo, uno para cada caso (cuando tenga una idea del diseño, etc.).
Usuario039402
5

Cuando desee realizar una prueba unitaria de su analizador sintáctico detectando el valor delimitado correctamente, puede pasar una línea desde un archivo CSV. Para hacer que su prueba sea directa y corta, puede probarla a través de un método que acepte una línea.

Esto hará que separe automáticamente la lectura de líneas de la lectura de valores individuales.

En otro nivel, es posible que no desee colocar todo tipo de archivos CSV físicos en su proyecto de prueba, pero haga algo más legible, simplemente declarando una gran cadena CSV dentro de su prueba para mejorar la legibilidad y la intención de la prueba. Esto lo llevará a desacoplar su analizador de cualquier E / S que haría en otro lugar.

Solo un ejemplo básico, solo comienza a practicarlo, sentirás la magia en algún momento (lo he hecho).

Joppe
fuente
4

En pocas palabras, escribir pruebas unitarias ayuda a exponer fallas en su código.

Esta espectacular guía para escribir código comprobable , escrita por Jonathan Wolter, Russ Ruffer y Miško Hevery, contiene numerosos ejemplos de cómo los defectos en el código, que inhiben las pruebas, también evitan la reutilización fácil y la flexibilidad del mismo código. Por lo tanto, si su código es comprobable, es más fácil de usar. La mayoría de las "morales" son consejos ridículamente simples que mejoran enormemente el diseño del código ( Dependency Injection FTW).

Por ejemplo: es muy difícil probar si el método computeStuff funciona correctamente cuando el caché comienza a expulsar cosas. Esto se debe a que debe agregar manualmente basura al caché hasta que el "bigCache" esté casi lleno.

public OopsIHardcoded {

   Cache cacheOfExpensiveComputations;

   OopsIHardcoded() {
       this.cacheOfExpensiveComputation = buildBigCache();
   }

   ExpensiveValue computeStuff() {
      //DOES THIS WORK CORRECTLY WHEN CACHE EVICTS DATA?
   }
}

Sin embargo, cuando usamos la inyección de dependencias, es mucho más fácil probar si el método computeStuff funciona correctamente cuando el caché comienza a expulsar cosas. Todo lo que hacemos es crear una prueba en la que llamamos new HereIUseDI(buildSmallCache()); Aviso, tenemos un control más matizado del objeto y paga dividendos de inmediato.

public HereIUseDI {

   Cache cacheOfExpensiveComputations;

   HereIUseDI(Cache cache) {
       this.cacheOfExpensiveComputation = cache;
   }

   ExpensiveValue computeStuff() {
      //DOES THIS WORK CORRECTLY WHEN CACHE EVICTS DATA?
   }
}

Se pueden obtener beneficios similares cuando nuestro código requiere datos que generalmente se encuentran en una base de datos ... simplemente pase EXACTAMENTE los datos que necesita.

Ivan
fuente
2
Honestamente, no estoy seguro de cómo te refieres al ejemplo. ¿Cómo se relaciona el método computeStuff con el caché?
John V
1
@ user970696 - Sí, estoy implicando que "computeStuff ()" usa el caché. La pregunta es "¿computeStuff () funciona correctamente todo el tiempo (lo que depende del estado de la memoria caché)" En consecuencia, es difícil confirmar que computeStuff () haga lo que desea PARA TODOS LOS ESTADOS POSIBLES DE CACHE si no puede configurar directamente / construye el caché porque codificó la línea "cacheOfExpensiveComputation = buildBigCache ();" (en lugar de pasar el caché directamente a través del constructor)
Ivan
0

Dependiendo de lo que se entiende por 'Pruebas de unidad', no creo que las pruebas de Unidad de bajo nivel faciliten un buen diseño tanto como las pruebas de integración de nivel ligeramente superior , pruebas que prueban que un grupo de actores (clases, funciones, lo que sea) en su código se combina correctamente para producir una gran cantidad de comportamientos deseables que se han acordado entre el equipo de desarrollo y el propietario del producto.

Si puede escribir pruebas en esos niveles, lo empuja hacia la creación de un código agradable, lógico, similar a la API que no requiere muchas dependencias locas; el deseo de tener una configuración de prueba simple lo conducirá naturalmente a no tener muchas dependencias locas o código estrechamente acoplado.

Sin embargo, no se equivoque: las pruebas unitarias pueden llevarlo a un mal diseño, así como a un buen diseño. He visto a los desarrolladores tomar un poco de código que ya tiene un diseño lógico agradable y una sola preocupación, y lo separan e introducen más interfaces con el único propósito de probar, y como resultado hacen que el código sea menos legible y más difícil de cambiar , así como posiblemente incluso tener más errores si el desarrollador ha decidido que tener muchas pruebas unitarias de bajo nivel significa que no tienen que tener pruebas de alto nivel. Un ejemplo favorito en particular es un error que arreglé donde había un montón de código 'comprobable' muy desglosado relacionado con la obtención de información dentro y fuera del portapapeles. Todo desglosado y desacoplado a niveles muy pequeños de detalle, con muchas interfaces, muchas simulaciones en las pruebas y otras cosas divertidas. Solo un problema: no había ningún código que realmente interactuara con el mecanismo del portapapeles del sistema operativo,

Las pruebas unitarias definitivamente pueden impulsar su diseño, pero no lo guían automáticamente a un buen diseño. Es necesario tener ideas sobre qué buen diseño es que vaya más allá de "este código se prueba, por lo tanto, es comprobable, por lo tanto es bueno".

Por supuesto, si usted es una de esas personas para quienes 'pruebas unitarias' significa 'cualquier prueba automatizada que no se maneja a través de la interfaz de usuario', entonces algunas de esas advertencias pueden no ser tan relevantes, como dije, creo que esas son más altas Las pruebas de integración de nivel son a menudo las más útiles cuando se trata de impulsar su diseño.

topo Restablecer Monica
fuente
-2

Las pruebas unitarias pueden ayudar con la refactorización cuando el nuevo código pasa todos los las antiguas pruebas.

Supongamos que ha implementado un bubbleort porque tenía prisa y no le preocupaba el rendimiento, pero ahora desea un quicksort porque los datos se están alargando. Si todas las pruebas pasan, las cosas se ven bien.

Por supuesto, las pruebas deben ser exhaustivas para que esto funcione. En mi ejemplo, es posible que sus pruebas no cubran la estabilidad porque eso no fue una preocupación con la clasificación de burbujas.

om
fuente
1
Esto es cierto, pero es más un beneficio de mantenimiento que un impacto directo en la calidad del diseño del código.
Ant P
@AntP, el OP preguntó sobre refactorización y pruebas unitarias.
om
1
La pregunta mencionaba la refactorización, pero la pregunta real era sobre cómo las pruebas unitarias podrían mejorar / verificar el diseño del código, no facilitar el proceso de refactorización en sí.
Ant P
-3

He encontrado que las pruebas unitarias son las más valiosas para facilitar el mantenimiento a largo plazo de un proyecto. Cuando vuelvo a un proyecto después de meses y no recuerdo muchos de los detalles, ejecutar pruebas me impide romper cosas.

David Baker
fuente
66
Ese es ciertamente un aspecto importante de las pruebas, pero en realidad no responde la pregunta (que no es por qué las pruebas son buenas, sino cómo influyen en el diseño).
Hulk