¿Deberíamos diseñar nuestro código desde el principio para permitir pruebas unitarias?

91

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.

Sotavento
fuente
33
Permítanme repetir esto: ¿sus colegas quieren pruebas unitarias para el nuevo código, pero se niegan a escribir el código de una manera que sea comprobable por unidad, aunque no hay riesgo de romper algo existente? Si eso es cierto, ¡debe aceptar la respuesta de @ KilianFoth y pedirle que resalte la primera oración en negrita! Al parecer, sus colegas tienen un gran malentendido acerca de cuál es su trabajo.
Doc Brown
20
@Lee: ¿Quién dice que desacoplar siempre es una buena idea? ¿Alguna vez ha visto una base de código en la que todo se pasa como una interfaz creada desde una fábrica de interfaces utilizando alguna interfaz de configuración? Yo tengo; fue escrito en Java, y fue un desastre completo, imposible de mantener y con errores. El desacoplamiento extremo es la ofuscación del código.
Christian Hackl
8
El trabajo eficaz de Michael Feathers con Legacy Code trata muy bien este problema y debería darle una buena idea sobre las ventajas de probar incluso en una nueva base de código.
l0b0
8
@ l0b0 Esa es más o menos la biblia para esto. En stackexchange no sería una respuesta a la pregunta, pero en RL le diría a OP que leyera este libro (al menos en parte). OP, comience a trabajar eficazmente con código heredado y léalo, al menos en parte (o dígale a su jefe que lo obtenga). Aborda preguntas como estas. Especialmente si no hiciste las pruebas y ahora te estás metiendo en ellas, podrías tener 20 años de experiencia, pero ahora harás cosas con las que no tienes experiencia . Es mucho más fácil leer sobre ellos que aprender minuciosamente todo eso por prueba y error.
R. Schmitz
44
Gracias por la recomendación del libro de Michael Feathers, definitivamente recogeré una copia.
Lee

Respuestas:

204

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.

Kilian Foth
fuente
39
Yo diría que depende del tipo de cambio. Hay una diferencia entre hacer que el código sea más fácil de probar e introducir ganchos específicos de prueba que NUNCA deberían usarse en la producción. Personalmente desconfío de este último, porque Murphy ...
Matthieu M.
61
Las pruebas unitarias a menudo rompen la encapsulación y hacen que el código bajo prueba sea más complejo de lo que se requeriría (por ejemplo, al introducir tipos de interfaz adicionales o agregar banderas). Como siempre en la ingeniería de software, cada buena práctica y cada buena regla tiene su parte de responsabilidades. Producir a ciegas muchas pruebas unitarias puede tener un efecto perjudicial en el valor comercial, sin mencionar que escribir y mantener las pruebas ya cuesta tiempo y esfuerzo. En mi experiencia, las pruebas de integración tienen un ROI mucho mayor y tienden a mejorar las arquitecturas de software con menos compromisos.
Christian Hackl
20
@Lee Claro, pero debe considerar si tener un tipo específico de pruebas garantiza el aumento de la complejidad del código. Mi experiencia personal es que las pruebas unitarias son una gran herramienta hasta el punto en que requieren cambios fundamentales de diseño para acomodar la burla. Ahí es donde cambio a un tipo diferente de pruebas. Escribir pruebas unitarias a expensas de hacer que la arquitectura sea sustancialmente más compleja, con el único propósito de tener pruebas unitarias, es mirar el ombligo.
Konrad Rudolph
21
@ChristianHackl ¿por qué una prueba unitaria rompería la encapsulación? Descubrí que para el código en el que he trabajado, si se percibe la necesidad de agregar una funcionalidad adicional para permitir la prueba, el problema real es que la función que se debe probar necesita refactorización, por lo que toda la funcionalidad es la misma. nivel de abstracción (son las diferencias en el nivel de abstracción las que generalmente crean esta "necesidad" de código adicional), con el código de nivel inferior movido a sus propias funciones (comprobables).
Baldrickk
29
@ChristianHackl Las pruebas unitarias nunca deberían romper la encapsulación, si está intentando acceder a variables privadas, protegidas o locales desde una prueba unitaria, lo está haciendo mal. Si está probando la funcionalidad foo, solo está probando si realmente funcionó, no si la variable local x es la raíz cuadrada de la entrada y en la tercera iteración del segundo bucle. Si alguna funcionalidad es privada, entonces que así sea, la probará de forma transitiva de todos modos. si es realmente grande y privado? Esa es una falla de diseño, pero probablemente ni siquiera sea posible fuera de C y C ++ con separación de implementación de encabezado.
opa
75

No es tan simple como podría pensar. Vamos a desglosarlo.

  • Escribir pruebas unitarias es definitivamente algo bueno.

¡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:

  1. 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.

  2. Asegúrese de que el nuevo código sea ​​comprobable y tenga pruebas de unidad e integración.

  3. 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.

Ewan
fuente
12
Esta es una respuesta mucho mejor que la aceptada. El desequilibrio en los votos es desalentador.
Konrad Rudolph
44
@Lee Una prueba de unidad debe probar una unidad de funcionalidad , que puede corresponder o no a una clase. Se debe probar una unidad de funcionalidad en su interfaz (que puede ser la API en este caso). Las pruebas pueden resaltar los olores del diseño y la necesidad de aplicar alguna nivelación diferente / más. Construya sus sistemas a partir de pequeñas piezas compostables, serán más fáciles de razonar y probar.
Wes Toleman
2
@KonradRudolph: Supongo que se perdió el punto en el que el OP agregó que esta pregunta era sobre diseñar un nuevo código, no cambiar el existente. Por lo tanto, no hay nada que romper, lo que hace que la mayor parte de esta respuesta no sea aplicable.
Doc Brown
1
Estoy totalmente en desacuerdo con la afirmación de que escribir pruebas unitarias siempre es algo bueno. Las pruebas unitarias son buenas solo en algunos casos. Es una tontería usar pruebas unitarias para probar el código de interfaz de usuario (UI), están hechas para probar la lógica empresarial. Además, es bueno escribir pruebas unitarias para reemplazar las comprobaciones de compilación que faltan (por ejemplo, en Javascript). La mayoría de los códigos frontend deberían escribir pruebas de extremo a extremo exclusivamente, no pruebas unitarias.
Sulthan
1
Los diseños definitivamente pueden sufrir de "Prueba de daño inducido". Por lo general, la capacidad de prueba mejora el diseño: al escribir pruebas, se nota que algo no se puede recuperar, pero se debe pasar, lo que hace que las interfaces sean más claras, etc. Pero ocasionalmente se topará con algo que requiere un diseño incómodo solo para pruebas. Un ejemplo podría ser un constructor de solo prueba requerido en su nuevo código debido al código de terceros existente que usa un singleton, por ejemplo. Cuando eso suceda: dé un paso atrás y realice una prueba de integración solamente, en lugar de dañar su propio diseño en nombre de la capacidad de prueba.
Anders Forsgren
18

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.

mmathis
fuente
Ese problema se puede resolver a través del análisis de código estático: _ForTestmarque 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.
Riking
13

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.

Doc Brown
fuente
Es interesante que mencione TDD. Estamos intentando incorporar BDD / TDD, que también ha encontrado cierta resistencia, es decir, lo que realmente significa "el código mínimo para aprobar".
Lee
2
@Lee: introducir cambios en una organización siempre causa cierta resistencia, y siempre necesita algo de tiempo para adaptar cosas nuevas, eso no es sabiduría nueva. Este es un problema de personas.
Doc Brown
Absolutamente. ¡Ojalá nos hubieran dado más tiempo!
Lee
Con frecuencia, se trata de mostrar a las personas que hacerlo de esta manera les ahorrará tiempo (y con suerte también rápidamente). ¿Por qué hacer algo que no te beneficiará?
Thorbjørn Ravn Andersen
@ ThorbjørnRavnAndersen: El equipo también puede mostrar al OP que su enfoque ahorrará tiempo. ¿Quién sabe? Pero me pregunto si no estamos enfrentando problemas de una naturaleza menos técnica aquí; El OP sigue viniendo aquí para decirnos qué hace mal su equipo (en su opinión), como si estuviera tratando de encontrar aliados para su causa. Podría ser más beneficioso discutir el proyecto junto con el equipo, no con extraños en Stack Exchange.
Christian Hackl
11

Estoy en desacuerdo con la afirmación (sin fundamento) que haces:

para probar el servicio API web, tendremos que burlarnos de esta fábrica

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:

  • ¿Alguna vez querré escribir una API web delgada sobre una API diferente ?
  • ¿Puedo reducir la duplicación de código entre la API web y la API subyacente? ¿Se puede generar uno en términos del otro?
  • ¿Puedo tratar toda la API web y la API subyacente como una sola unidad de "recuadro negro" y hacer afirmaciones significativas sobre cómo se comporta todo?
  • Si la API web tuviera que ser reemplazada por una nueva implementación en el futuro, ¿cómo lo haríamos?
  • Si la API web se reemplazara con una nueva implementación en el futuro, ¿los clientes de la API web podrían darse cuenta? ¿Si es así, cómo?

Otra afirmación sin fundamento que usted hace es sobre DI:

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 utilizamos un tercero marco como Ninject para evitar tener que diseñar el controlador de esta manera, pero aún tendremos que crear una interfaz.

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.

Daniel Pryden
fuente
Punto tomado sobre las afirmaciones con respecto a las interfaces, pero incluso si no las usáramos, todavía tendríamos que inyectar objetos de alguna manera, esta es la preocupación del resto del equipo. es decir, algunos miembros del equipo estarían contentos con un ctr sin parámetros que creara una instancia de la implementación concreta y la dejara así. De hecho, un miembro planteó la idea de usar la reflexión para inyectar simulacros para que no tengamos que diseñar código para aceptarlos. Lo cual es un código de olor olor imo
Lee
9

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.

Thorbjørn Ravn Andersen
fuente
También tenemos un problema con TDD, a saber, lo que constituye el "código mínimo para pasar". Le demostré al equipo este proceso y no se limitaron a escribir lo que ya hemos diseñado, lo que puedo entender. El "mínimo" no parece estar definido. Si escribimos una prueba y tenemos planes y diseños claros, ¿por qué no escribir eso para aprobar la prueba?
Lee
@Lee "código mínimo para pasar" ... bueno, esto puede sonar un poco tonto, pero es literalmente lo que dice. Por ejemplo, si tiene una prueba 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 ejemplo UserCantChangePasswordToEmptyString.
R. Schmitz
@Lee En última instancia, sus pruebas terminarán siendo la documentación de lo que hace su código, excepto que es documentación que verifica si se cumple, en lugar de ser solo tinta en papel. También compare con esta pregunta : un método CalculateFactorialque 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.
R. Schmitz
1
@Lee Pequeños pasos. El mínimo puede ser más de lo que piensas cuando el código se eleva por encima de lo trivial. Además, el diseño que realiza al implementar todo de una vez puede ser menos óptimo porque hace suposiciones sobre cómo se debe hacer sin haber escrito las pruebas que lo demuestran. Nuevamente recuerde, el código debería fallar al principio.
Thorbjørn Ravn Andersen
1
Además, las pruebas de regresión son muy importantes. ¿Están en alcance para el equipo?
Thorbjørn Ravn Andersen
8

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.

David
fuente
3
"Prototipo desechable": cada proyecto comienza su vida como uno de esos ... mejor pensar que las cosas nunca son eso. escribiendo esto como estoy ... ¿adivina qué? ... refactorizando un prototipo desechable que resultó no serlo;)
Algy Taylor
44
Si desea estar seguro de que se desechará un prototipo de descarte, escríbalo en un lenguaje de prototipo que nunca se permitirá en la producción. Clojure y Python son buenas opciones.
Thorbjørn Ravn Andersen
2
@ ThorbjørnRavnAndersen Eso me hizo reír. ¿Estaba destinado a ser una excavación en esos idiomas? :)
Lee
@Sotavento. No, solo ejemplos de idiomas que podrían no ser aceptables para la producción, generalmente porque nadie en la organización puede mantenerlos porque no están familiarizados con ellos y sus curvas de aprendizaje son empinadas. Si esos son aceptables, elija otro que no lo sea.
Thorbjørn Ravn Andersen
4

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

Planeamos crear una fábrica que devuelva 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.

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.

Viejo pro
fuente
2

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.

Peter
fuente
1

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.

CJ Dennis
fuente
3
Puede tener la tentación de introducir muchas capas de indirección para poder probar, y luego nunca usarlas como se esperaba.
Thorbjørn Ravn Andersen
1

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).

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á.

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.

CharonX
fuente
1

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.

Zenilogix
fuente
1
"hecho bien" es un problema. Tengo que mantener dos proyectos donde DI se hizo mal (aunque con el objetivo de hacerlo "bien"). Esto hace que el código sea horrible y mucho peor que los proyectos heredados sin DI y pruebas unitarias. Hacer DI correctamente no es fácil.
enero
@ Jan eso es interesante. ¿Cómo lo hicieron mal?
Lee
1
El proyecto @Lee One es un servicio que necesita un tiempo de inicio rápido, pero su inicio es terriblemente lento porque toda la inicialización de la clase se realiza por adelantado mediante el marco DI (Castle Windsor en C #). Otro problema que veo en estos proyectos es mezclar DI con la creación de objetos con "nuevo", esquivando la DI. Eso hace que las pruebas sean difíciles nuevamente y provocó algunas condiciones de carrera desagradables.
enero
1

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.

Veamos la diferencia entre un comprobable:

public class MyController : Controller
{
    private readonly IMyDependency _thing;

    public MyController(IMyDependency thing)
    {
        _thing = thing;
    }
}

y controlador no comprobable:

public class MyController : Controller
{
}

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 IMyDependencyen 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.

Ian Kemp
fuente
1
Lo que descarta tan rápidamente como "falta de comprensión de las cosas nuevas" puede llegar a ser una buena comprensión de las cosas viejas. La inyección de dependencia ciertamente no es nueva. La idea, y probablemente las primeras implementaciones, tienen décadas de antigüedad. Y sí, creo que su respuesta es un ejemplo de código que se vuelve más complicado debido a las pruebas unitarias, y posiblemente un ejemplo de pruebas unitarias que rompen la encapsulación (porque ¿quién dice que la clase tiene un constructor público en primer lugar?). A menudo he eliminado la inyección de dependencia de las bases de código que heredé de otra persona, debido a las compensaciones.
Christian Hackl
Los controladores siempre tienen un constructor público, implícito o no, porque MVC lo requiere. "Complicado" - tal vez, si no comprende cómo funcionan los constructores. Encapsulación: sí, en algunos casos, pero el debate DI vs encapsulación es un debate continuo y muy subjetivo que no ayudará aquí, y en particular para la mayoría de las aplicaciones, DI le servirá mejor que la encapsulación IMO.
Ian Kemp
Con respecto a los constructores públicos: de hecho, esa es una particularidad del marco utilizado. Estaba pensando en el caso más general de una clase ordinaria que no es instanciada por un marco. ¿Por qué cree que ver parámetros de métodos adicionales como complejidad adicional es igual a una falta de comprensión sobre cómo funcionan los constructores? Sin embargo, agradezco que reconozca la existencia de una compensación entre DI y encapsulación.
Christian Hackl
0

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.

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.

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.

Ashutosh
fuente
0

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.

Martin Maat
fuente