Estoy trabajando en un comparador de listas para ayudar a ordenar una lista desordenada de resultados de búsqueda según los requisitos muy específicos de nuestro cliente. Los requisitos requieren un algoritmo de relevancia clasificado con las siguientes reglas en orden de importancia:
- Coincidencia exacta en el nombre
- Todas las palabras de consulta de búsqueda en nombre o sinónimo del resultado
- Algunas palabras de consulta de búsqueda en nombre o sinónimo del resultado (% descendente)
- Todas las palabras de la consulta de búsqueda en la descripción.
- Algunas palabras de la consulta de búsqueda en la descripción (% descendente)
- Fecha de última modificación descendiente
La elección de diseño natural para este comparador parecía ser una clasificación puntuada basada en poderes de 2. La suma de reglas menos importantes nunca puede ser más que una coincidencia positiva en una regla de mayor importancia. Esto se logra mediante el siguiente puntaje:
- 32
- dieciséis
- 8 (puntaje secundario de desempate basado en% descendente)
- 4 4
- 2 (Puntuación secundaria de desempate basada en% descendente)
- 1
En el espíritu TDD, decidí comenzar primero con mis pruebas unitarias. Tener un caso de prueba para cada escenario único sería, como mínimo, 63 casos de prueba únicos sin considerar casos de prueba adicionales para la lógica secundaria de desempate en las reglas 3 y 5. Esto parece excesivo.
Sin embargo, las pruebas reales serán menos. Según las reglas reales, ciertas reglas aseguran que las reglas inferiores siempre serán verdaderas (por ejemplo, cuando "Todas las palabras de consulta de búsqueda aparecen en la descripción", entonces la regla "Algunas palabras de consulta de búsqueda aparecen en la descripción" siempre será verdadera). ¿Aún vale la pena el nivel de esfuerzo al escribir cada uno de estos casos de prueba? ¿Es este el nivel de prueba que generalmente se requiere cuando se habla de una cobertura de prueba del 100% en TDD? Si no, ¿cuál sería una estrategia de prueba alternativa aceptable?
fuente
Respuestas:
Su pregunta implica que TDD tiene algo que ver con "escribir todos los casos de prueba primero". En mi humilde opinión eso no está "en el espíritu de TDD", en realidad está en contra . Recuerde que TDD significa " desarrollo impulsado por pruebas ", por lo que solo necesita aquellos casos de prueba que realmente "impulsen" su implementación, no más. Y siempre que su implementación no esté diseñada de manera que el número de bloques de código crezca exponencialmente con cada nuevo requisito, tampoco necesitará un número exponencial de casos de prueba. En su ejemplo, el ciclo TDD probablemente se verá así:
Luego, comience con el segundo requisito:
Aquí viene el truco : cuando agrega casos de prueba para el requisito / número de categoría "n", solo tendrá que agregar pruebas para asegurarse de que el puntaje de la categoría "n-1" sea más alto que el puntaje de la categoría "n" . No tendrá que agregar ningún caso de prueba para cualquier otra combinación de las categorías 1, ..., n-1, ya que las pruebas que ha escrito anteriormente se asegurarán de que los puntajes de esas categorías sigan en el orden correcto.
Por lo tanto, esto le dará una serie de casos de prueba que crecen aproximadamente lineales con el número de requisitos, no exponencialmente.
fuente
Considere escribir una clase que pase por una lista predefinida de condiciones y multiplique un puntaje actual por 2 por cada verificación exitosa.
Esto se puede probar muy fácilmente, utilizando solo un par de pruebas simuladas.
Luego puede escribir una clase para cada condición y solo hay 2 pruebas para cada caso.
Realmente no entiendo su caso de uso, pero espero que este ejemplo ayude.
Notarás que tus pruebas de condiciones 2 ^ se reducen rápidamente a 4+ (condiciones 2 *). 20 es mucho menos dominante que 64. Y si agrega otra más tarde, no tiene que cambiar NINGUNA de las clases existentes (principio abierto-cerrado), por lo que no tiene que escribir 64 nuevas pruebas, solo tiene que para agregar otra clase con 2 pruebas nuevas e inyectar eso en su clase ScoreBuilder.
fuente
Tendrá que definir "vale la pena". El problema con este tipo de escenario es que las pruebas tendrán un rendimiento decreciente de la utilidad. Ciertamente, la primera prueba que escriba valdrá la pena. Puede encontrar errores obvios en la prioridad e incluso cosas como errores de análisis al intentar separar las palabras.
La segunda prueba valdrá la pena porque cubre una ruta diferente a través del código, probablemente verificando otra relación de prioridad.
La 63a prueba probablemente no valdrá la pena porque es algo en lo que tienes una confianza del 99,99% y está cubierto por la lógica de tu código u otra prueba.
Tengo entendido que la cobertura del 100% significa que se ejercen todas las rutas de código. Esto no significa que haga todas las combinaciones de sus reglas, pero todas las diferentes rutas que podría seguir su código (como señala, algunas combinaciones no pueden existir en el código). Pero como está haciendo TDD, todavía no hay un "código" para verificar las rutas. La letra del proceso diría make 63+.
Personalmente, encuentro que el 100% de cobertura es un sueño imposible. Más allá de eso, no es pragmático. Las pruebas unitarias existen para servirle, no al revés. A medida que realiza más pruebas, obtiene rendimientos decrecientes en el beneficio (la probabilidad de que la prueba evite un error + la confianza de que el código es correcto). Dependiendo de lo que haga su código, define en qué escala deslizante deja de hacer pruebas. Si su código está ejecutando un reactor nuclear, entonces quizás todas las más de 63 pruebas valen la pena. Si su código está organizando su archivo de música, entonces probablemente podría salirse con la suya con mucho menos.
fuente
Yo diría que este es un caso perfecto para TDD.
Tiene un conjunto conocido de criterios para probar, con un desglose lógico de esos casos. Asumiendo que los va a probar unitariamente ahora o más tarde, parece tener sentido tomar el resultado conocido y construirlo a su alrededor, asegurando que, de hecho, esté cubriendo cada una de las reglas de forma independiente.
Además, puede averiguar a medida que avanza si agregar una nueva regla de búsqueda infringe una regla existente. Si hace todo esto al final de la codificación, presumiblemente corre un mayor riesgo de tener que cambiar uno para arreglar uno, que rompe otro, que rompe otro ... Y, al implementar las reglas, aprende si su diseño es válido o necesita ajustes.
fuente
No soy fanático de interpretar estrictamente el 100% de la cobertura de prueba como escribir especificaciones contra cada método o probar cada permutación del código. Hacer esto fanáticamente tiende a conducir a un diseño de sus clases basado en pruebas que no encapsula adecuadamente la lógica de negocios y produce pruebas / especificaciones que generalmente no tienen sentido en términos de describir la lógica de negocios admitida. En cambio, me concentro en estructurar las pruebas de forma muy parecida a las reglas comerciales en sí mismas y me esfuerzo por ejercitar cada rama condicional del código con pruebas con la expectativa explícita de que esas pruebas sean fácilmente entendidas por el probador como lo serían generalmente los casos de uso y realmente describen reglas comerciales que fueron implementadas.
Con esta idea en mente, probaría exhaustivamente los 6 factores de clasificación que enumeró de forma aislada entre sí, seguidos de 2 o 3 pruebas de estilo de integración que aseguran que está acumulando sus resultados a los valores de clasificación generales esperados. Por ejemplo, en el caso n. ° 1, Exact Match on Name, tendría al menos dos pruebas unitarias para probar cuándo es exacto y cuándo no, y que los dos escenarios devuelven la puntuación esperada. Si distingue entre mayúsculas y minúsculas, también un caso para probar "Coincidencia exacta" frente a "coincidencia exacta" y posiblemente otras variaciones de entrada, como puntuación, espacios adicionales, etc., también devuelve las puntuaciones esperadas.
Una vez que he trabajado con todos los factores individuales que contribuyen a los puntajes de clasificación, esencialmente asumo que estos funcionan correctamente en el nivel de integración y me concentro en asegurar que sus factores combinados contribuyan correctamente al puntaje de clasificación final esperado.
Suponiendo que los casos # 2 / # 3 y # 4 / # 5 están generalizados a los mismos métodos subyacentes, pero pasando diferentes campos, solo tiene que escribir un conjunto de pruebas unitarias para los métodos subyacentes y escribir pruebas unitarias adicionales simples para probar el específico campos (título, nombre, descripción, etc.) y puntuación en la factorización designada, por lo que esto reduce aún más la redundancia de su esfuerzo de prueba general.
Con este enfoque, el enfoque descrito anteriormente probablemente arrojaría 3 o 4 pruebas unitarias en el caso n. ° 1, tal vez 10 especificaciones en algunos / todos los w / sinónimos explicados, más 4 especificaciones en la calificación correcta de los casos n. ° 2 - n. ° 5 y 2 a 3 especificaciones en la clasificación ordenada de la fecha final, luego 3 a 4 pruebas de nivel de integración que miden los 6 casos combinados de manera probable (olvídate de los casos de borde oscuro por ahora a menos que veas claramente un problema en tu código que debe ejercerse para garantizar esa condición se maneja) o asegúrese de que no sea violado / roto por revisiones posteriores. Eso produce alrededor de 25 o más especificaciones para ejercer el 100% del código escrito (aunque no llamó directamente al 100% de los métodos escritos).
fuente
Nunca he sido fanático del 100% de cobertura de prueba. En mi experiencia, si algo es lo suficientemente simple como para probar con solo uno o dos casos de prueba, entonces es lo suficientemente simple como para raramente fallar. Cuando falla, generalmente se debe a cambios arquitectónicos que requerirían cambios de prueba de todos modos.
Dicho esto, para requisitos como el tuyo, siempre realizo pruebas exhaustivas, incluso en proyectos personales en los que nadie me obliga, porque esos son los casos en que las pruebas unitarias te ahorran tiempo y molestias. Cuantas más pruebas unitarias se requieran para probar algo, más tiempo se ahorrarán las pruebas unitarias.
Esto se debe a que solo puedes mantener tantas cosas en tu cabeza a la vez. Si está tratando de escribir código que funcione para 63 combinaciones diferentes, a menudo es difícil arreglar una combinación sin romper otra. Terminas probando manualmente otras combinaciones una y otra vez. Las pruebas manuales son mucho más lentas, lo que hace que no desee volver a ejecutar todas las combinaciones posibles cada vez que realice un cambio. Eso te hace más propenso a perderte algo y a perder tiempo buscando caminos que no funcionan en todos los casos.
Además del tiempo ahorrado en comparación con las pruebas manuales, hay mucha menos tensión mental, lo que hace que sea más fácil concentrarse en el problema en cuestión sin preocuparse por la introducción accidental de regresiones. Eso te permite trabajar más rápido y por más tiempo sin agotamiento. En mi opinión, solo los beneficios de salud mental valen el costo de la prueba unitaria del código complejo, incluso si no le ahorró tiempo.
fuente