¿Hay un punto para pruebas de unidad que apunten y se burlen de todo lo público?

56

Cuando hago pruebas unitarias de la manera "adecuada", es decir, aplastando cada llamada pública y devolviendo valores predeterminados o simulacros, siento que en realidad no estoy probando nada. Literalmente estoy viendo mi código y creando ejemplos basados en el flujo de lógica a través de mis métodos públicos. Y cada vez que cambia la implementación, tengo que ir a cambiar esas pruebas, nuevamente, sin sentir realmente que estoy logrando nada útil (ya sea a mediano o largo plazo). También realizo pruebas de integración (incluidas las rutas que no son felices) y realmente no me importa el aumento de los tiempos de prueba. Con esos, siento que en realidad estoy probando para regresiones, porque se han detectado múltiples, mientras que todas las pruebas unitarias me muestran que la implementación de mi método público cambió, lo que ya sé.

La prueba de unidad es un tema muy 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)?

    
pregunta enthrops 17.05.2013 - 03:11

5 respuestas

35
  

Al realizar pruebas unitarias de la manera "adecuada", es decir, aplastar a todos los públicos   Llama y devuelve valores predeterminados o simulacros, siento que no soy realmente   probando cualquier cosa Estoy literalmente mirando mi código y creando   Ejemplos basados en el flujo de la lógica a través de mis métodos públicos.

Esto suena como que el método que estás probando necesita otras instancias de clase (de las que tienes que burlarte), y llama a varios métodos por sí mismo.

Este tipo de código es realmente difícil de probar por unidades, por las razones que describe.

Lo que he encontrado útil es dividir estas clases en:

  1. Clases con la "lógica empresarial" real. Estos utilizan pocas o ninguna llamada a otras clases y son fáciles de probar (valor (es) in - value out).
  2. Clases que interactúan con sistemas externos (archivos, bases de datos, etc.). Estos envuelven el sistema externo y proporcionan una interfaz conveniente para sus necesidades.
  3. Clases que "unen todo"

Luego, las clases de 1. son fáciles de probar por unidades, porque solo aceptan valores y devuelven un resultado. En casos más complejos, es posible que estas clases necesiten realizar llamadas por sí mismas, 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 simular (porque solo exponga las partes del sistema envuelto que necesita).

Las clases de 2. y 3. por lo general no pueden ser probadas de manera significativa por unidad (porque no hacen nada útil por sí mismas, simplemente son código de "pegamento"). OTOH, estas clases tienden a ser relativamente simples (y pocas), por lo que deberían ser cubiertas adecuadamente por las 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:

1 select price from database
2 perform price calculation, possibly fetching parameters from database
3 update price in database

Los tres pasos necesitarán acceso a la base de datos, por lo que habrá muchas burlas (complejas) que probablemente se romperán si el código o la estructura de la base de datos cambian.

Dividir

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:

App:
fetch price data from PriceRepository
call PriceCalculation with input values
call PriceRepository to update prices

De esa manera:

  • PriceCalculation encapsula la "lógica de negocios". Es fácil de probar porque no llama a nada por sí solo.
  • PriceRepository puede ser probado por pseudo-unidad configurando una base de datos simulada y probando las llamadas de lectura y actualización. Tiene poca lógica, por lo tanto pocos codepaths, por lo que no necesita demasiadas de estas pruebas.
  • La aplicación no se puede probar de manera significativa, ya que es un código de pegamento. Sin embargo, también es muy simple, por lo que las pruebas de integración deberían ser suficientes. Si la aplicación posterior se vuelve demasiado compleja, se dividen más clases de "lógica empresarial".

Finalmente, puede resultar que PriceCalculation deba hacer sus propias llamadas a la base de datos. Por ejemplo, porque solo PriceCalculation sabe qué datos necesita, por lo que la aplicación no puede recuperarlos por adelantado. Luego, puede pasarle una instancia de PriceRepository (o alguna otra clase de repositorio), personalizada según las necesidades de PriceCalculation. Esta clase tendrá que ser burlada, pero esto será simple, porque la interfaz de PriceRepository es simple, por ejemplo. %código%. Lo más importante es que la interfaz de PriceRepository aísla a PriceCalculation de la base de datos, por lo que es poco probable que los cambios en el esquema de base de datos o en la organización de datos cambien su interfaz y, por lo tanto, rompan las simulaciones.

    
respondido por el sleske 22.05.2013 - 08:53
27
  

¿Cuál es la ventaja decisiva de las pruebas unitarias frente a las pruebas de integración?

Eso es una falsa dicotomía.

La prueba de unidad y la prueba de integración tienen 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 en las pruebas unitarias. Esto es evidente en la forma en que se diseñan las pruebas unitarias: establecen específicamente qué se supone que hace el código. y afirma que el código hace eso.

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í no pasen las pruebas de integración porque no interactúan correctamente.

Sin embargo, si las pruebas unitarias tienen una ventaja decisiva, es esta: 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 un código "verificable", 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 verificable tiene ciertas características, como una API coherente, un comportamiento repetible y proporciona resultados que son fáciles de afirmar.

Las pruebas de integración son más difíciles y más costosas, ya que a menudo se necesitan burlas complicadas, configuraciones complejas y afirmaciones difíciles. En el nivel más alto de integración del sistema, imagine que intenta 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 las pruebas automáticas.

Finalmente, las pruebas de integración no ofrecen garantías sobre la cobertura del código. ¿Cuántas combinaciones de bucles de código, condiciones y ramas está probando con sus pruebas de integración? ¿Realmente 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 realmente bien en el nivel de método, donde se realizan 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 demostrar que lo hace de nuevo cuando refactoriza la implementación subyacente. Si su API cambia, o si necesita que sus métodos cambien de acuerdo con un cambio en el diseño del sistema, eso es lo que debe suceder. Si está ocurriendo mucho, considera escribir primero tus pruebas, antes de escribir código. Esto te obligará a pensar en la arquitectura general y te permitirá escribir código con la API ya establecida.

Si pasas mucho tiempo escribiendo pruebas unitarias para códigos triviales como

public string SomeProperty { get; set; }

entonces deberías reexaminar tu enfoque. Se supone que la prueba de unidad prueba el comportamiento de , y no hay ningún 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 esa propiedad será mencionada en otra parte de su código. En lugar de hacerlo, considere la posibilidad de escribir métodos que acepten la propiedad necesaria como un parámetro:

public string SomeMethod(string someProperty);

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. Por supuesto, no siempre podrás hacer esto, pero mueve tu código en la dirección de ser más comprobable, y esta vez estás escribiendo una prueba de unidad para el comportamiento real .

    
respondido por el Robert Harvey 17.05.2013 - 05:16
4

Las pruebas unitarias con simulacros son para asegurarse de que la implementación de la clase sea correcta. Te burlas de las interfaces públicas de las dependencias del código que estás probando. De esta manera, tiene control sobre todo lo externo a la clase y está seguro de que una prueba que falla no se debe a algo interno de la clase y no en 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 deben 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, en su lugar, intente escribir las pruebas primero. Intente describir el comportamiento que debe tener la clase y luego escriba la cantidad mínima de código para hacer que se aprueben las pruebas.

Tanto la prueba de unidad como la prueba 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 no he hecho todavía. 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 asegure que los métodos se llamen tiene valor, ya que te aseguras de que realmente se llame a los métodos. En particular, si primero escribe las pruebas, tiene una lista de verificación de los métodos que deben realizarse. Dado que este código es bastante procedimental, no tiene mucho que verificar, aparte de que se llame a los métodos. Usted está protegiendo el código para el cambio en el futuro. Cuando necesitas llamar a un método antes que al otro. O que un método siempre es llamado incluso si el método inicial produce una excepción.

La prueba para este método nunca puede cambiar o puede cambiar solo cuando está cambiando los métodos. ¿Por qué es esto algo malo? Ayuda a reforzar el uso de las pruebas. Si tiene que corregir una prueba después de cambiar el código, tendrá la costumbre de cambiar las pruebas con el código.

    
respondido por el Schleis 17.05.2013 - 03:32
2

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 te burlas por defecto sino que utilizas objetos reales (idealmente a través de la inyección de dependencia).

De esa manera, puede crear rápidamente pruebas robustas con una buena cobertura de código. No hay necesidad de actualizar tus mocks todo el tiempo. Puede que sea un poco menos preciso que las pruebas unitarias con simulacros al 100%, pero el tiempo y el dinero que ahorra lo compensan. Lo único que realmente necesitas para usar simulacros o accesorios son los backends de almacenamiento o los servicios externos.

En realidad, la burla excesiva es un anti-patrón: Anti-Patterns TDD y Mocks are evil .

    
respondido por el lastzero 21.11.2015 - 15:39
0

Aunque la operación ya ha marcado una respuesta, solo estoy agregando mis 2 centavos aquí.

  

¿Cuál es la ventaja decisiva de las pruebas unitarias frente a las pruebas de integración (excluyendo el gasto general)?

Y también en respuesta a

  

Cuando hago pruebas unitarias de la manera "adecuada", es decir, aplastando cada llamada pública y devolviendo valores predeterminados o simulacros, siento que en realidad no estoy probando nada.

Hay una información útil pero no exactamente lo que ha pedido OP:

¿Las pruebas de unidad funcionan pero todavía hay errores?

por mi pequeña experiencia en suites de prueba, entiendo que Pruebas de unidad 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 pruebas de unidad dedicadas. Incluso en mi experiencia reciente, tenía un método público que llamaba otro método privado pequeño. Entonces, había dos enfoques:

  1. no cree una (s) prueba (s) de unidad para el método privado.
  2. cree una (s) prueba (s) de unidad para un método privado.

Si piensa lógicamente, el punto de tener un método privado es: el método público principal es demasiado grande o desordenado. Para resolver esto, refactorice sabiamente y cree pequeños trozos de código que merecen ser métodos privados separados que a su vez hacen que su principal método público 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 haya otro método público que dependa de ese método privado, pero quién sabe sobre el futuro.

Teniendo en cuenta el caso cuando el método privado es reutilizado por muchos otros métodos públicos.

Por lo tanto, si hubiera elegido el enfoque 1: habría duplicado las pruebas de unidad y habrían sido complicadas, ya que tenía varias pruebas de unidad para cada rama del método público, así como también el método 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 de probar.

Teniendo en cuenta el caso cuando el método privado no se reutiliza No hay ningún punto de escribir una prueba de unidad separada para ese método.

En cuanto a Pruebas de integración , tienden a ser exhaustivas y más de alto nivel. Le dirán que si se les da una opinión, todas sus clases deberían llegar a esta conclusión final. Para saber más sobre la utilidad de las pruebas de integración, consulte el enlace mencionado.

    
respondido por el shankbond 07.08.2013 - 14:42

Lea otras preguntas en las etiquetas