En este momento hay un debate en nuestro equipo sobre si modificar el diseño del código para permitir la prueba de la unidad es un olor a código, o en qué medida se puede hacer sin ser un olor a código. Esto se debe a que recién estamos comenzando a implementar prácticas que están presentes en casi todas las demás empresas de desarrollo de software.
Específicamente, tendremos un servicio de API web que será muy delgado. Su responsabilidad principal será reunir las solicitudes / respuestas web y llamar a una API subyacente que contenga la lógica empresarial.
Un ejemplo es que planeamos crear una fábrica que devolverá un tipo de método de autenticación. No tenemos necesidad de que herede una interfaz, ya que no anticipamos que sea otra cosa que no sea el tipo concreto que será. Sin embargo, para realizar una prueba unitaria del servicio API web tendremos que burlarnos de esta fábrica.
Esto significa esencialmente que diseñamos la clase de controlador de API web para aceptar DI (a través de su constructor o configurador), lo que significa que estamos diseñando parte del controlador solo para permitir DI e implementando una interfaz que de otro modo no necesitamos, o usamos un marco de terceros como Ninject para evitar tener que diseñar el controlador de esta manera, pero aún tendremos que crear una interfaz.
Algunos en el equipo parecen reacios a diseñar código solo por el simple hecho de realizar pruebas. Me parece que debe haber algún compromiso si espera realizar una prueba unitaria, pero no estoy seguro de cómo calmar sus preocupaciones.
Para ser claros, este es un proyecto nuevo, por lo que no se trata realmente de modificar el código para permitir la prueba de la unidad; se trata de diseñar el código que vamos a escribir para que sea comprobable por unidad.
fuente
Respuestas:
La renuencia a modificar el código en aras de las pruebas muestra que un desarrollador no ha entendido el papel de las pruebas y, por implicación, su propio papel en la organización.
El negocio del software gira en torno a la entrega de una base de código que crea valor comercial. Hemos descubierto, a través de una larga y amarga experiencia, que no podemos crear tales bases de código de tamaño no trivial sin realizar pruebas. Por lo tanto, las suites de prueba son una parte integral del negocio.
Muchos programadores prestan atención a este principio pero inconscientemente nunca lo aceptan. Es fácil entender por qué es esto; La conciencia de que nuestra propia capacidad mental no es infinita, y de hecho, sorprendentemente limitada cuando se enfrenta a la enorme complejidad de una base de código moderna, no es bienvenida y se suprime o racionaliza fácilmente. El hecho de que el código de prueba no se entregue al cliente hace que sea fácil creer que es un ciudadano de segunda clase y que no es esencial en comparación con el código comercial "esencial". Y la idea de agregar código de prueba al código comercial parece doblemente ofensiva para muchos.
El problema para justificar esta práctica tiene que ver con el hecho de que la imagen completa de cómo se crea el valor en un negocio de software a menudo solo es entendida por los superiores en la jerarquía de la compañía, pero estas personas no tienen la comprensión técnica detallada de el flujo de trabajo de codificación que se requiere para comprender por qué no se pueden eliminar las pruebas. Por lo tanto, los practicantes los tranquilizan con demasiada frecuencia y les aseguran que las pruebas pueden ser una buena idea en general, pero "Somos programadores de élite que no necesitamos muletas como esa", o que "no tenemos tiempo para eso en este momento", etc. etc. El hecho de que el éxito empresarial es un juego de números y que evita la deuda técnica, asegurando la calidad, etc. muestra que su valor solo a largo plazo significa que a menudo son bastante sinceros en esa creencia.
En pocas palabras: hacer que el código sea comprobable es una parte esencial del proceso de desarrollo, no es diferente que en otros campos (muchos microchips están diseñados con una proporción sustancial de elementos solo para fines de prueba), pero es muy fácil pasar por alto las muy buenas razones para ese. No caigas en esa trampa.
fuente
No es tan simple como podría pensar. Vamos a desglosarlo.
¡PERO!
Cualquier cambio en su código puede introducir un error. Por lo tanto, cambiar el código sin una buena razón comercial no es una buena idea.
Su webapi 'muy delgada' no parece ser el mejor caso para las pruebas unitarias.
Cambiar el código y las pruebas al mismo tiempo es algo malo.
Sugeriría el siguiente enfoque:
Escribir pruebas de integración . Esto no debería requerir ningún cambio de código. Le dará sus casos de prueba básicos y le permitirá verificar que cualquier cambio de código adicional que realice no presente ningún error.
Asegúrese de que el nuevo código sea comprobable y tenga pruebas de unidad e integración.
Asegúrese de que su cadena de CI ejecuta pruebas después de compilaciones e implementaciones.
Cuando tenga esas cosas configuradas, solo entonces comience a pensar en refactorizar los proyectos heredados para la comprobabilidad.
Esperemos que todos hayan aprendido lecciones del proceso y tengan una buena idea de dónde es más necesario realizar las pruebas, cómo desea estructurarlo y el valor que aporta al negocio.
EDITAR : desde que escribí esta respuesta, el OP ha aclarado la pregunta para mostrar que están hablando de código nuevo, no de modificaciones en el código existente. Tal vez ingenuamente pensé "¿Está bien la prueba de unidad?" El argumento se resolvió hace algunos años.
Es difícil imaginar qué cambios de código requerirían las pruebas unitarias, pero no sería una buena práctica general que desearía en cualquier caso. Probablemente sería prudente examinar las objeciones reales, posiblemente sea el estilo de las pruebas unitarias a las que se está objetando.
fuente
Diseñar código para que sea inherentemente comprobable no es un olor a código; por el contrario, es el signo de un buen diseño. Existen varios patrones de diseño conocidos y ampliamente utilizados basados en esto (por ejemplo, Model-View-Presenter) que ofrecen una prueba fácil (más fácil) como una gran ventaja.
Entonces, si necesita escribir una interfaz para su clase concreta para probarla más fácilmente, eso es algo bueno. Si ya tiene la clase concreta, la mayoría de los IDE pueden extraer una interfaz, haciendo que el esfuerzo sea mínimo. Es un poco más trabajo mantener los dos sincronizados, pero una interfaz no debería cambiar mucho de todos modos, y los beneficios de las pruebas pueden superar ese esfuerzo adicional.
Por otro lado, como @MatthieuM. mencionado en un comentario, si está agregando puntos de entrada específicos en su código que nunca deberían usarse en producción, solo por el simple hecho de probar, eso podría ser un problema.
fuente
_ForTest
marque los métodos (por ejemplo, debe tener un nombre ) y verifique la base de código para llamadas de código que no sea de prueba.En mi humilde opinión, es muy simple entender que para crear pruebas unitarias, el código que se debe probar debe tener al menos ciertas propiedades. Por ejemplo, si el código no consta de unidades individuales que se pueden probar de forma aislada, la palabra "prueba de unidad" ni siquiera comienza a tener sentido. Si el código no tiene estas propiedades, primero debe cambiarse, eso es bastante obvio.
Dijo que, en teoría, uno puede intentar escribir primero una unidad de código comprobable, aplicando todos los principios SÓLIDOS, y luego intentar escribir una prueba para ello después, sin modificar más el código original. Desafortunadamente, escribir código que es realmente comprobable por unidad no siempre es simple, por lo que es muy probable que se necesiten algunos cambios que solo se detectarán al intentar crear las pruebas. Esto es cierto para el código incluso cuando fue escrito con la idea de la prueba de unidad en mente, y definitivamente es más cierto para el código que fue escrito donde la "capacidad de prueba de la unidad" no estaba en la agenda al principio.
Existe un enfoque bien conocido que intenta resolver el problema escribiendo primero las pruebas unitarias: se llama Test Driven Development (TDD), y seguramente puede ayudar a hacer que el código sea más comprobable desde el principio.
Por supuesto, la renuencia a cambiar el código después para que sea comprobable surge a menudo en una situación en la que el código se probó manualmente primero y / o funciona bien en la producción, por lo que cambiarlo podría introducir nuevos errores, eso es cierto. El mejor enfoque para mitigar esto es crear primero un conjunto de pruebas de regresión (que a menudo se puede implementar con solo cambios muy mínimos en la base del código), así como otras medidas complementarias como revisiones de código o nuevas sesiones de prueba manual. Eso debería dar suficiente confianza para asegurarse de que el rediseño de algunas partes internas no rompa nada importante.
fuente
Estoy en desacuerdo con la afirmación (sin fundamento) que haces:
Eso no es necesariamente cierto. Hay un montón de maneras de escribir las pruebas, y no son formas de escribir pruebas unitarias que no implican burla. Más importante aún, hay otros tipos de pruebas, como pruebas funcionales o de integración. Muchas veces es posible encontrar una "costura de prueba" en una "interfaz" que no es un lenguaje de programación OOP
interface
.Algunas preguntas para ayudarlo a encontrar una costura de prueba alternativa, que podría ser más natural:
Otra afirmación sin fundamento que usted hace es sobre DI:
La inyección de dependencia no significa necesariamente crear una nueva
interface
. Por ejemplo, en la causa de un token de autenticación: ¿puede simplemente crear un token de autenticación real mediante programación? Luego, la prueba puede crear tales tokens e inyectarlos. ¿El proceso para validar un token depende de algún tipo de secreto criptográfico? Espero que no haya codificado un secreto: espero que pueda leerlo del almacenamiento de alguna manera, y en ese caso simplemente puede usar un secreto diferente (conocido) en sus casos de prueba.Esto no quiere decir que nunca debas crear uno nuevo
interface
. Pero no se obsesione con que solo haya una forma de escribir una prueba o una forma de fingir un comportamiento. Si piensa fuera de la caja, generalmente puede encontrar una solución que requerirá un mínimo de contorsiones de su código y aún así le dará el efecto que desea.fuente
Estás de suerte ya que este es un nuevo proyecto. Descubrí que Test Driven Design funciona muy bien para escribir un buen código (por eso lo hacemos en primer lugar).
Al averiguar por adelantado cómo invocar una determinada pieza de código con los datos de entrada realistas, y luego obtener datos de salida realista que se puede comprobar es como se pretende, que hace el diseño API muy temprano en el proceso y tienen una buena oportunidad de conseguir una diseño útil porque no se ve obstaculizado por el código existente que debe reescribirse para adaptarse. Además, es más fácil de entender por sus pares para que pueda tener buenas discusiones nuevamente al principio del proceso.
Tenga en cuenta que "útil" en la oración anterior significa no solo que los métodos resultantes son fáciles de invocar, sino que también tiende a obtener interfaces limpias que son fáciles de armar en las pruebas de integración y para escribir maquetas.
Considéralo. Especialmente con la revisión por pares. En mi experiencia, la inversión de tiempo y esfuerzo se devolverá muy rápidamente.
fuente
UserCanChangeTheirPassword
, en la prueba llama a la función (aún no existente) para cambiar la contraseña y luego afirma que la contraseña sí ha cambiado. Luego escribe la función, hasta que pueda ejecutar la prueba y no arroje excepciones ni tenga una afirmación incorrecta. Si en ese momento tiene una razón para agregar algún código, entonces esa razón pasa a otra prueba, por ejemploUserCantChangePasswordToEmptyString
.CalculateFactorial
que solo devuelve 120 y la prueba pasa. Ese es el mínimo. Obviamente tampoco es lo que se pretendía, pero eso solo significa que necesita otra prueba para expresar lo que se pretendía.Si necesita modificar el código, ese es el olor del código.
Por experiencia personal, si es difícil escribir pruebas para mi código, es un código incorrecto. No es un código incorrecto porque no se ejecuta ni funciona según lo diseñado, es malo porque no puedo entender rápidamente por qué funciona. Si encuentro un error, sé que va a ser un trabajo largo y doloroso solucionarlo. El código también es difícil / imposible de reutilizar.
El código bueno (limpio) divide las tareas en secciones más pequeñas que se entienden fácilmente de un vistazo (o al menos un buen aspecto). Probar estas secciones más pequeñas es fácil. También puedo escribir pruebas que solo prueben una parte de la base de código con similar facilidad si estoy bastante seguro de las subsecciones (la reutilización también ayuda aquí, ya que ya se ha probado).
Mantenga el código fácil de probar, fácil de refactorizar y fácil de reutilizar desde el principio y no se suicidará cada vez que necesite hacer cambios.
Estoy escribiendo esto mientras reconstruyo por completo un proyecto que debería haber sido un prototipo desechable en un código más limpio. Es mucho mejor acertar desde el principio y refactorizar el código incorrecto lo antes posible en lugar de mirar una pantalla durante horas sin parar de tener miedo de tocar algo por miedo a romper algo que funciona parcialmente.
fuente
Yo diría que escribir código que no puede ser probado por la unidad es un olor a código. En general, si su código no puede ser probado por la unidad, entonces no es modular, lo que dificulta su comprensión, mantenimiento o mejora. Tal vez si el código es un código adhesivo que realmente solo tiene sentido en términos de pruebas de integración, puede sustituir las pruebas de integración por las pruebas unitarias, pero incluso entonces, cuando la integración falla, tendrá que aislar el problema y las pruebas unitarias son una excelente manera de hazlo.
Tu dices
Realmente no sigo esto. La razón para tener una fábrica que cree algo es permitirle cambiar de fábrica o cambiar lo que la fábrica crea fácilmente, para que otras partes del código no tengan que cambiar. Si su método de autenticación nunca va a cambiar, entonces la fábrica es un código inútil. Sin embargo, si desea tener un método de autenticación diferente en la prueba que en la producción, tener una fábrica que devuelva un método de autenticación diferente en la prueba que en la producción es una gran solución.
No necesita DI o simulacros para esto. Solo necesita que su fábrica admita los diferentes tipos de autenticación y que sea configurable de alguna manera, como desde un archivo de configuración o una variable de entorno.
fuente
En cada disciplina de ingeniería que se me ocurra, solo hay una forma de lograr niveles de calidad decentes o superiores:
Para tener en cuenta la inspección / prueba en el diseño.
Esto es válido en construcción, diseño de chips, desarrollo de software y fabricación. Ahora, esto no significa que las pruebas sean el pilar sobre el que se debe construir cada diseño, en absoluto. Pero con cada decisión de diseño, los diseñadores deben ser claros sobre los impactos en los costos de las pruebas y tomar decisiones conscientes sobre la compensación.
En algunos casos, las pruebas manuales o automatizadas (p. Ej., Selenio) serán más convenientes que las pruebas unitarias, al tiempo que proporcionan una cobertura de prueba aceptable por sí mismas. En casos raros, arrojar algo que casi no se haya probado también puede ser aceptable. Pero estos deben ser conscientes de las decisiones caso por caso. Llamar a un diseño que explica la prueba de un "olor a código" indica una grave falta de experiencia.
fuente
Descubrí que las pruebas unitarias (y otros tipos de pruebas automatizadas) tienden a reducir los olores de código, y no puedo pensar en un solo ejemplo en el que introduzcan olores de código. Las pruebas unitarias generalmente lo obligan a escribir un mejor código. Si no puede usar un método fácilmente bajo prueba, ¿por qué debería ser más fácil en su código?
Las pruebas unitarias bien escritas le muestran cómo se debe usar el código. Son una forma de documentación ejecutable. He visto pruebas unitarias horriblemente escritas y demasiado largas que simplemente no se podían entender. ¡No escribas esos! Si necesita escribir pruebas largas para configurar sus clases, sus clases deben ser refactorizadas.
Las pruebas unitarias resaltarán dónde están algunos de los olores de su código. Aconsejaría leer Michael C. Feathers ' Working Effectively with Legacy Code' . Aunque su proyecto es nuevo, si aún no tiene ninguna (o muchas) pruebas unitarias, es posible que necesite algunas técnicas no obvias para que su código se pruebe bien.
fuente
En una palabra:
El código comprobable es (generalmente) un código que se puede mantener, o mejor dicho, un código que es difícil de probar generalmente es difícil de mantener. Diseñar un código que no sea comprobable es similar a diseñar una máquina que no sea reparable: lástima el pobre imbécil a quien se le asignará la reparación eventualmente (podría ser usted).
Sabes que necesitarás cinco tipos diferentes de métodos de autenticación en tres años, ahora que lo has dicho, ¿verdad? Los requisitos cambian, y aunque debe evitar diseñar demasiado su diseño, tener un diseño comprobable significa que su diseño tiene (solo) costuras suficientes para ser alteradas sin (demasiado) dolor, y que las pruebas del módulo le proporcionarán medios automatizados para ver que Tus cambios no rompen nada.
fuente
Diseñar alrededor de la inyección de dependencia no es un olor a código, es la mejor práctica. Usar DI no es solo por la capacidad de prueba. La construcción de sus componentes en torno a la DI ayuda a la modularidad y la reutilización, permite más fácilmente intercambiar los componentes principales (como una capa de interfaz de base de datos). Si bien agrega un grado de complejidad, si se hace correctamente, permite una mejor separación de capas y un aislamiento de la funcionalidad que hace que la complejidad sea más fácil de administrar y navegar. Esto facilita la validación adecuada del comportamiento de cada componente, reduce los errores y también puede facilitar la localización de errores.
fuente
Veamos la diferencia entre un comprobable:
y controlador no comprobable:
La primera opción tiene literalmente 5 líneas adicionales de código, dos de las cuales pueden ser autogeneradas por Visual Studio. Una vez que haya configurado su marco de inyección de dependencia para sustituir un tipo concreto
IMyDependency
en tiempo de ejecución, que para cualquier marco DI decente, es otra línea única de código, todo funciona, excepto que ahora puede burlarse y probar su controlador al contenido de su corazón .¿6 líneas de código adicionales para permitir la capacidad de prueba ... y sus colegas argumentan que es "demasiado trabajo"? Ese argumento no vuela conmigo, y no debería volar contigo.
Y no tiene que crear e implementar una interfaz para las pruebas: Moq , por ejemplo, le permite simular el comportamiento de un tipo concreto para fines de pruebas unitarias. Por supuesto, eso no será de mucha utilidad si no puede inyectar esos tipos en las clases que está probando.
La inyección de dependencia es una de esas cosas que una vez que la entiendes, te preguntas "¿cómo funcioné sin esto?". Es simple, efectivo y simplemente tiene sentido. Por favor, no permita que la falta de comprensión de sus colegas sobre cosas nuevas se interponga en el camino para hacer que su proyecto sea comprobable.
fuente
Cuando escribo pruebas unitarias, empiezo a pensar en lo que podría salir mal dentro de mi código. Me ayuda a mejorar el diseño del código y aplicar el principio de responsabilidad única (SRP). Además, cuando vuelvo a modificar el mismo código unos meses después, me ayuda a confirmar que la funcionalidad existente no está rota.
Existe una tendencia a utilizar funciones puras tanto como sea posible (aplicaciones sin servidor). Las pruebas unitarias me ayudan a aislar el estado y escribir funciones puras.
Escriba primero las pruebas unitarias para la API subyacente y, si tiene suficiente tiempo de desarrollo, también necesita escribir pruebas para el servicio de API web delgada.
TL; DR, las pruebas unitarias ayudan a mejorar la calidad del código y ayudan a realizar cambios futuros en el código sin riesgos. También mejora la legibilidad del código. Use pruebas en lugar de comentarios para expresar su punto.
fuente
La conclusión, y cuál debería ser su argumento con el grupo reacio, es que no hay conflicto. El gran error parece haber sido que alguien acuñó la idea de "diseñar para probar" a las personas que odian las pruebas. Deberían haber cerrado la boca o decirlo de manera diferente, como "tomemos el tiempo para hacer esto bien".
La idea de que "tienes que implementar una interfaz" para hacer que algo sea comprobable está mal. La interfaz ya está implementada, simplemente no está declarada en la declaración de clase todavía. Se trata de reconocer los métodos públicos existentes, copiar sus firmas en una interfaz y declarar esa interfaz en la declaración de la clase. Sin programación, sin cambios en la lógica existente.
Al parecer, algunas personas tienen una idea diferente sobre esto. Te sugiero que intentes arreglar esto primero.
fuente