Probar solo a través de métodos públicos es malo

En programación y en TDD, en particular, hay buenos principios a los que es útil adherirse: DRY y pruebas a través de métodos públicos. Repetidamente demostraron su valía en la práctica, pero en proyectos con un gran código heredado pueden tener un "lado oscuro". Por ejemplo, puede escribir código guiado por estos principios, y luego encontrarse desarmando pruebas que cubren un montón de más de 20 abstracciones con una configuración que es incomparablemente más grande que la lógica probada. Este "lado oscuro" asusta a las personas e inhibe el uso de TDD en proyectos. Bajo el corte, analizo por qué las pruebas a través de métodos públicos son malas y cómo reducir los problemas que surgen debido a este principio.

Descargo de responsabilidad
Inmediatamente quiero disipar la posible impresión. Es posible que algunos ni siquiera sientan los inconvenientes que se discutirán, debido, por ejemplo, al tamaño de sus proyectos. Además, estas deficiencias, en mi opinión, son parte de la deuda técnica y tienen las mismas características: el problema crecerá si no se le presta atención. Por lo tanto, es necesario decidir según la situación.

La idea que subyace al principio suena bien: debe probar el comportamiento, no la implementación. Esto significa que solo necesita probar la interfaz de clase. En la práctica, este no es siempre el caso. Para presentar la esencia del problema, imagine que tiene un método que calcula el costo de los trabajadores que trabajan en turnos. Esta es una tarea no trivial cuando se trata de turnos de trabajo, ya que tienen propinas, bonos, fines de semana, feriados, reglas corporativas, etc., etc. Este método realiza internamente muchas operaciones y utiliza otros servicios que le brindan información sobre feriados, propinas, etc. al escribir una prueba unitaria para ello, es necesario crear una configuración para todos los servicios utilizados, si el código probado se encuentra en algún lugar al final del método. Al mismo tiempo, el código probado solo puede usar parcialmente o no usar servicios configurables. Y ya hay algunas pruebas unitarias escritas de esta manera.

Menos 1: sobreconfiguración de prueba de unidad


Ahora desea agregar una reacción a una nueva característica que tiene una lógica no trivial y también se usa en algún lugar al final del método. La naturaleza del indicador es tal que forma parte de la lógica del servicio y, al mismo tiempo, no forma parte de la interfaz del servicio. En el caso anterior, este código solo es relevante para este método público, y generalmente puede inscribirse dentro del método anterior.

Si el proyecto adoptó la regla para probar todo solo a través de métodos públicos, lo más probable es que el desarrollador simplemente copie alguna prueba unitaria existente y la ajuste un poco. En la nueva prueba, todavía habrá una configuración de todos los servicios para ejecutar el método. Por un lado, cumplimos con el principio, pero, por otro lado, obtuvimos una prueba unitaria con sobreconfiguración. En el futuro, si algo se rompe o requiere un cambio de configuración, tendrá que hacer el trabajo de mono para ajustar las pruebas. Es tedioso, largo y no brinda alegría ni beneficio aparente para el cliente. Parece que estamos siguiendo el principio correcto, pero estamos en la misma situación de la que queríamos escapar, negándonos a probar métodos privados.

Menos 2: cobertura incompleta


Además, puede intervenir un factor humano como la pereza. Por ejemplo, un método privado con lógica de bandera no trivial puede verse en este ejemplo.

private bool HasShifts(DateTime date, int tolerance, bool clockIn, Shift[] shifts, int[] locationIds) { bool isInLimit(DateTime date1, DateTime date2, int limit) => Math.Abs(date2.Subtract(date1).TotalMinutes) <= limit; var shiftsOfLocations = shifts.Where(x => locationIds.Contains(x.LocationId)); return clockIn ? shiftsOfLocations.Any(x => isInLimit(date, x.StartDate, tolerance)) : shiftsOfLocations.Any(x => isInLimit(date, x.EndDate, tolerance)); } 

Este método requiere 10 controles para cubrir todos los casos, 8 de los cuales son significativos.

Decodificando 8 casos importantes
  • shiftsOfLocations - 2 valores - si o no
  • clockIn - 2 valores - verdadero o falso
  • tolerancia - 2 significados diferentes

Total: 2 x 2 x 2 = 8

Al escribir pruebas unitarias para probar esta lógica, un desarrollador tendrá que escribir al menos 8 pruebas unitarias grandes. Me encontré con casos en los que la configuración de prueba de la unidad ocupaba más de 50 líneas de código, con 4 líneas de una llamada directa. Es decir solo alrededor del 10% del código lleva una carga útil. En este caso, la tentación es excelente para reducir la cantidad de trabajo al escribir menos pruebas unitarias. Como resultado, de 8, por ejemplo, solo quedan dos pruebas unitarias, para cada valor clockIn. Esta situación lleva al hecho de que, una vez más, es tedioso y largo escribir todas las pruebas necesarias, creando la configuración (Ctrl + C, V funciona, sin ella), o el método solo queda parcialmente cubierto. Cada opción tiene sus consecuencias desagradables.

Posibles soluciones


Además del principio "comportamiento de prueba", todavía hay OCP (principio abierto / cerrado). Al aplicarlo correctamente, puede olvidar lo que son las "pruebas frágiles", probando el comportamiento interno del módulo. Si necesita un nuevo comportamiento del módulo, escribirá nuevas pruebas unitarias para la nueva clase sucesora en la que se cambiará el comportamiento que necesita. Entonces no tendrá que perder tiempo en volver a verificar y actualizar las pruebas existentes. En este caso, este método puede declararse como interno o protegido interno, y probarse agregando InternalsVisibleTo al ensamblaje. En este caso, su interfaz IClass no sufrirá, y las pruebas serán las más lacónicas, no sujetas a cambios frecuentes.

Otra alternativa sería declarar una clase auxiliar adicional en la que nuestro método se pueda extraer declarándolo como público. Entonces se observará el principio y la prueba será concisa. En mi opinión, este enfoque no siempre vale la pena. Por ejemplo, algunos pueden decidir incluir incluso un método en una clase, lo que lleva a la creación de un grupo de clases con un método. El otro extremo puede ser volcar tales métodos en una clase auxiliar, que se convierte en una clase auxiliar de Dios. Pero esta opción con un ayudante puede ser la única si el ensamblaje de trabajo está firmado con un nombre seguro y, por alguna razón, no puede firmar el ensamblaje de prueba. InternalsVisibleTo funcionará cuando ambos conjuntos estén firmados o no a la vez.

Resumen


Y al final, debido a una combinación de tales problemas, la idea de TDD y pruebas unitarias sufre, porque nadie tiene el deseo de escribir pruebas volumétricas y apoyarlas. Me alegrará ver ejemplos de cómo la estricta adherencia a este principio condujo a problemas y redujo la motivación para escribir pruebas del equipo de desarrollo.

Source: https://habr.com/ru/post/es419873/


All Articles