Cuando realizo pruebas unitarias de la manera "correcta", es decir, en cada llamada pública y devuelvo valores o simulaciones preestablecidas, siento que en realidad no estoy probando nada. Literalmente estoy mirando mi código y creando ejemplos basados en el flujo de la lógica a través de mis métodos públicos. Y cada vez que cambia la implementación, tengo que ir y cambiar esas pruebas, una vez más, sin sentir realmente que estoy logrando algo útil (ya sea a mediano o largo plazo). También hago pruebas de integración (incluidas las rutas no felices) y realmente no me importa el aumento de los tiempos de prueba. Con eso, siento que en realidad estoy probando regresiones, porque han detectado múltiples, mientras que todas las pruebas unitarias me muestran que la implementación de mi método público cambió, lo cual ya sé.
Las pruebas unitarias son un tema amplio, y siento que soy yo quien no comprende algo aquí. ¿Cuál es la ventaja decisiva de las pruebas unitarias frente a las pruebas de integración (excluyendo la sobrecarga de tiempo)?
fuente
Respuestas:
Esto parece que el método que está probando necesita varias otras instancias de clase (de las que tiene que burlarse), y llama a varios métodos por sí solo.
Este tipo de código es realmente difícil de probar por unidad, por las razones que usted describe.
Lo que he encontrado útil es dividir tales clases en:
Luego, las clases de 1. son fáciles de probar por unidad, porque solo aceptan valores y devuelven un resultado. En casos más complejos, estas clases pueden necesitar realizar llamadas por su cuenta, pero solo llamarán a las clases desde 2. (y no llamarán directamente, por ejemplo, a una función de base de datos), y las clases desde 2. son fáciles de burlar (porque solo exponga las partes del sistema envuelto que necesita).
Las clases de 2. y 3. por lo general no se pueden probar de manera significativa en la unidad (porque no hacen nada útil por sí mismas, solo son códigos de "pegamento"). OTOH, estas clases tienden a ser relativamente simples (y pocas), por lo que deberían cubrirse adecuadamente con pruebas de integración.
Un ejemplo
Una clase
Supongamos que tiene una clase que recupera un precio de una base de datos, aplica algunos descuentos y luego actualiza la base de datos.
Si tiene todo esto en una clase, deberá llamar a las funciones de DB, que son difíciles de burlar. En pseudocódigo:
Los tres pasos necesitarán acceso a la base de datos, por lo que una gran cantidad de burlas (complejas), que probablemente se rompan si el código o la estructura de la base de datos cambian.
Separar
Se divide en tres clases: PriceCalculation, PriceRepository, App.
PriceCalculation solo realiza el cálculo real y obtiene los valores que necesita. La aplicación une todo:
De esa manera:
Finalmente, puede resultar que PriceCalculation debe hacer sus propias llamadas a la base de datos. Por ejemplo, porque solo PriceCalculation sabe qué datos necesita, por lo que App no puede obtenerlos por adelantado. Luego puede pasarle una instancia de PriceRepository (o alguna otra clase de repositorio), adaptada a las necesidades de PriceCalculation. Entonces será necesario burlarse de esta clase, pero será simple, porque la interfaz de PriceRepository es simple, por ejemplo
PriceRepository.getPrice(articleNo, contractType)
. Lo más importante es que la interfaz de PriceRepository aísla PriceCalculation de la base de datos, por lo que es poco probable que los cambios en el esquema de la base de datos o la organización de datos cambien su interfaz y, por lo tanto, rompan las simulaciones.fuente
Esa es una falsa dicotomía.
Las pruebas unitarias y las pruebas de integración sirven para dos propósitos similares pero diferentes. El propósito de las pruebas unitarias es asegurarse de que sus métodos funcionen. En términos prácticos, las pruebas unitarias aseguran que el código cumpla con el contrato descrito por las pruebas unitarias. Esto es evidente en la forma en que se diseñan las pruebas unitarias: establecen específicamente lo que se supone que debe hacer el código y afirman que el código lo hace.
Las pruebas de integración son diferentes. Las pruebas de integración ejercitan la interacción entre los componentes del software. Puede tener componentes de software que pasen todas sus pruebas y aún así fallen las pruebas de integración porque no interactúan correctamente.
Sin embargo, si hay una ventaja decisiva para las pruebas unitarias, es esto: las pruebas unitarias son mucho más fáciles de configurar y requieren mucho menos tiempo y esfuerzo que las pruebas de integración. Cuando se usan correctamente, las pruebas unitarias fomentan el desarrollo de código "comprobable", lo que significa que el resultado final será más confiable, más fácil de entender y más fácil de mantener. El código comprobable tiene ciertas características, como una API coherente, un comportamiento repetible y devuelve resultados que son fáciles de afirmar.
Las pruebas de integración son más difíciles y más caras, porque a menudo necesita burlas elaboradas, configuraciones complejas y afirmaciones difíciles. En el nivel más alto de integración del sistema, imagine intentar simular la interacción humana en una interfaz de usuario. Sistemas de software completos están dedicados a ese tipo de automatización. Y lo que buscamos es la automatización; Las pruebas en humanos no son repetibles y no se escalan como lo hacen las pruebas automatizadas.
Finalmente, las pruebas de integración no garantizan la cobertura del código. ¿Cuántas combinaciones de bucles de código, condiciones y ramas está probando con sus pruebas de integración? ¿De verdad lo sabes? Existen herramientas que puede usar con las pruebas unitarias y los métodos bajo prueba que le indicarán cuánta cobertura de código tiene y cuál es la complejidad ciclomática de su código. Pero solo funcionan bien a nivel de método, donde viven las pruebas unitarias.
Si sus pruebas cambian cada vez que refactoriza, ese es un problema diferente. Se supone que las pruebas unitarias consisten en documentar lo que hace su software, probar que lo hace y luego probar que lo hace nuevamente cuando refactoriza la implementación subyacente. Si su API cambia, o necesita que sus métodos cambien de acuerdo con un cambio en el diseño del sistema, eso es lo que se supone que sucederá. Si está sucediendo mucho, considere escribir sus pruebas primero, antes de escribir el código. Esto lo obligará a pensar en la arquitectura general y le permitirá escribir código con la API ya establecida.
Si pasa mucho tiempo escribiendo pruebas unitarias para código trivial como
entonces deberías reexaminar tu enfoque. Se supone que las pruebas unitarias prueban el comportamiento, y no hay comportamiento en la línea de código anterior. Sin embargo, ha creado una dependencia en su código en alguna parte, ya que es casi seguro que se hará referencia a esa propiedad en otra parte de su código. En lugar de hacerlo, considere escribir métodos que acepten la propiedad necesaria como parámetro:
Ahora su método no tiene ninguna dependencia de algo fuera de sí mismo, y ahora es más comprobable, ya que es completamente autónomo. De acuerdo, no siempre podrá hacer esto, pero mueve su código en la dirección de ser más comprobable, y esta vez está escribiendo una prueba unitaria para el comportamiento real.
fuente
Las pruebas unitarias con simulacros son para asegurarse de que la implementación de la clase sea correcta. Se burla de las interfaces públicas de las dependencias del código que está probando. De esta manera, tiene control sobre todo lo externo a la clase y está seguro de que una prueba fallida se debe a algo interno de la clase y no a uno de los otros objetos.
También está probando el comportamiento de la clase bajo prueba, no la implementación. Si refactoriza el código (creando nuevos métodos internos, etc.), las pruebas unitarias no deberían fallar. Pero si está cambiando lo que hace el método público, entonces absolutamente las pruebas deberían fallar porque ha cambiado el comportamiento.
También parece que está escribiendo las pruebas después de haber escrito el código, intente escribir las pruebas primero. Intente delinear el comportamiento que la clase debería tener y luego escriba la cantidad mínima de código para que las pruebas pasen.
Tanto las pruebas unitarias como las pruebas de integración son útiles para garantizar la calidad de su código. Las pruebas unitarias examinan cada componente de forma aislada. Y las pruebas de integración aseguran que todos los componentes interactúen correctamente. Quiero tener ambos tipos en mi suite de prueba.
Las pruebas unitarias me han ayudado en mi desarrollo, ya que puedo concentrarme en una parte de la aplicación a la vez. Burlándose de los componentes que aún no he hecho. También son excelentes para la regresión, ya que documentan cualquier error en la lógica que he encontrado (incluso en las pruebas unitarias).
ACTUALIZAR
Crear una prueba que solo se asegure de que se invoquen los métodos tiene valor, ya que se asegura de que los métodos realmente se llamen. Particularmente si primero escribe sus pruebas, tiene una lista de verificación de los métodos que deben suceder. Dado que este código es bastante de procedimiento, no tiene mucho que verificar, aparte de que se llaman los métodos. Estás protegiendo el código para el cambio en el futuro. Cuando necesita llamar a un método antes que al otro. O que siempre se llama a un método, incluso si el método inicial arroja una excepción.
Es posible que la prueba para este método nunca cambie o solo cambie cuando está cambiando los métodos. ¿Por qué es esto algo malo? Ayuda a reforzar el uso de las pruebas. Si tiene que arreglar una prueba después de cambiar el código, tendrá la costumbre de cambiar las pruebas con el código.
fuente
Estaba experimentando una pregunta similar, hasta que descubrí el poder de las pruebas de componentes. En resumen, son lo mismo que las pruebas unitarias, excepto que no se burla de forma predeterminada, sino que usa objetos reales (idealmente mediante inyección de dependencia).
De esa manera, puede crear rápidamente pruebas robustas con una buena cobertura de código. No es necesario actualizar tus simulacros todo el tiempo. Puede ser un poco menos preciso que las pruebas unitarias con 100% de simulacros, pero el tiempo y el dinero que ahorra lo compensan. Lo único que realmente necesita para usar simulacros o accesorios son backends de almacenamiento o servicios externos.
En realidad, la burla excesiva es un antipatrón: los antipatrones y los simulacros de TDD son malos .
fuente
Aunque la operación ya ha marcado una respuesta, solo estoy agregando mis 2 centavos aquí.
Y también en respuesta a
Hay un útil pero no exactamente lo que OP ha pedido:
¿Las pruebas unitarias funcionan pero todavía hay errores?
Desde mi poca experiencia en las suites de pruebas, entiendo que las pruebas unitarias siempre son para probar la funcionalidad de nivel de método más básica de una clase. En mi opinión, cada método, ya sea público, privado o interno, merece tener una unidad de pruebas dedicada. Incluso en mi experiencia reciente, tenía un método público que llamaba a otro pequeño método privado. Entonces, había dos enfoques:
Si piensa lógicamente, el punto de tener un método privado es: el método público principal se está volviendo demasiado grande o desordenado. Para resolver esto, usted refactoriza sabiamente y crea pequeños fragmentos de código que merecen ser métodos privados separados que a su vez hacen que su método público principal sea menos voluminoso. Refactoriza teniendo en cuenta que este método privado podría reutilizarse más adelante. Puede haber casos en los que no exista otro método público que dependa de ese método privado, pero quién sabe sobre el futuro.
Considerando el caso cuando el método privado es reutilizado por muchos otros métodos públicos.
Entonces, si hubiera elegido el enfoque 1: habría duplicado las pruebas unitarias y habrían sido complicadas, ya que tenía varias pruebas unitarias para cada rama del método público y privado.
Si hubiera elegido el enfoque 2: el código escrito para las pruebas unitarias sería relativamente menor, y sería mucho más fácil probarlo.
Considerando el caso cuando el método privado no se reutiliza No tiene sentido escribir una prueba unitaria separada para ese método.
En cuanto a las pruebas de integración , tienden a ser exhaustivas y más de alto nivel. Le dirán que, dado un aporte, todas sus clases deben llegar a esta conclusión final. Para comprender más sobre la utilidad de las pruebas de integración, consulte el enlace mencionado.
fuente