¿Cómo probar cuando organizar los datos es demasiado engorroso?

19

Estoy escribiendo un analizador sintético y, como parte de eso, tengo una Expanderclase que "expande" un enunciado complejo simple en múltiples enunciados simples. Por ejemplo, expandiría esto:

x = 2 + 3 * a

dentro:

tmp1 = 3 * a
x = 2 + tmp1

Ahora estoy pensando en cómo evaluar esta clase, específicamente cómo organizar las pruebas. Podría crear manualmente el árbol de sintaxis de entrada:

var input = new AssignStatement(
    new Variable("x"),
    new BinaryExpression(
        new Constant(2),
        BinaryOperator.Plus,
        new BinaryExpression(new Constant(3), BinaryOperator.Multiply, new Variable("a"))));

O podría escribirlo como una cadena y analizarlo:

var input = new Parser().ParseStatement("x = 2 + 3 * a");

La segunda opción es mucho más simple, más corta y legible. Pero también introduce una dependencia Parser, lo que significa que un error Parserpodría fallar en esta prueba. Entonces, la prueba dejaría de ser una prueba unitaria de Expander, y supongo que técnicamente se convierte en una prueba de integración de Parsery Expander.

Mi pregunta es: ¿está bien confiar principalmente (o completamente) en este tipo de pruebas de integración para probar esta Expanderclase?

svick
fuente
3
Que un error Parserpueda fallar en alguna otra prueba no es un problema si habitualmente comete solo cero fallas, por el contrario, significa que tiene más cobertura Parser. De lo que preferiría preocuparme es que un error Parserpodría hacer que esta prueba tenga éxito cuando debería haber fallado . Las pruebas unitarias están ahí para encontrar errores, después de todo, una prueba se rompe cuando no es así, pero debería haberlo hecho.
Jonas Kölker

Respuestas:

27

Te encontrarás escribiendo muchas más pruebas, de un comportamiento mucho más complicado, interesante y útil, si puedes hacerlo simplemente. Entonces la opción que involucra

var input = new Parser().ParseStatement("x = 2 + 3 * a");

Es bastante válido. Depende de otro componente. Pero todo depende de docenas de otros componentes. Si te burlas de algo a menos de una pulgada de su vida útil, probablemente dependas de muchas características de burla y accesorios de prueba.

Los desarrolladores a veces se centran demasiado en la pureza de sus pruebas unitarias , o desarrollan pruebas unitarias y pruebas unitarias únicamente , sin ningún módulo, integración, estrés u otro tipo de pruebas. Todos esos formularios son válidos y útiles, y todos son responsabilidad de los desarrolladores, no solo preguntas y respuestas o del personal de operaciones más adelante.

Un enfoque que he usado es comenzar con estas ejecuciones de nivel superior, luego usar los datos producidos a partir de ellas para construir la expresión de forma larga, de mínimo común denominador de la prueba. Por ejemplo, cuando volca la estructura de datos del inputproducto anterior, puede construir fácilmente:

var input = new AssignStatement(
    new Variable("x"),
    new BinaryExpression(
        new Constant(2),
        BinaryOperator.Plus,
        new BinaryExpression(new Constant(3), BinaryOperator.Multiply, new Variable("a"))));

tipo de prueba que prueba al nivel más bajo. De esa forma, obtienes una buena combinación: un puñado de las pruebas primitivas más básicas (pruebas unitarias puras), pero no has pasado una semana escribiendo pruebas en ese nivel primitivo. Eso le brinda el recurso de tiempo necesario para escribir muchas más pruebas atómicas, un poco menos, utilizando Parsercomo ayudante. Resultado final: más pruebas, más cobertura, más esquinas y otros casos interesantes, mejor código y mayor garantía de calidad.

Jonathan Eunice
fuente
2
Esto es razonable, especialmente con respecto al hecho de que todo depende de muchos otros. Una buena prueba de unidad debería probar el mínimo posible. Cualquier cosa que esté dentro de esa cantidad mínima posible debe ser probada por una prueba de unidad anterior. Si ha probado Parser por completo, puede suponer que puede usar Parser de forma segura para probar ParseStatement
Jon Story
66
La principal preocupación de pureza (creo) es evitar escribir dependencias circulares en sus pruebas unitarias. Si las pruebas del analizador o del analizador utilizan el expansor, y esta prueba del expansor depende del funcionamiento del analizador, entonces tiene un riesgo difícil de manejar de que todo lo que está probando es que el analizador y el expansor son consistentes , mientras que lo que querías hacer era probar que el expansor realmente hace lo que se supone que debe hacer . Pero siempre y cuando no haya dependencia en sentido contrario, usar el analizador en esta prueba unitaria no es realmente diferente de usar una biblioteca estándar en una prueba unitaria.
Steve Jessop
@SteveJessop Buen punto. Es importante usar componentes independientes .
Jonathan Eunice
3
Algo que he hecho en los casos en que el analizador en sí es una operación costosa (por ejemplo, leer datos de archivos de Excel a través de interoperabilidad com) es escribir métodos de generación de prueba que ejecuten el analizador y el código de salida en la consola para recrear la estructura de datos que devuelve el analizador . Luego copio la salida del generador en pruebas unitarias más convencionales. Esto permite reducir la dependencia cruzada, ya que el analizador solo necesita funcionar correctamente cuando las pruebas se crearon, no cada vez que se ejecutan. (No perder unos segundos / probar para crear / destruir procesos de Excel fue una buena ventaja).
Dan Neely
+1 para el enfoque de @ DanNeely. Utilizamos algo similar para almacenar varias versiones serializadas de nuestro modelo de datos como datos de prueba, de modo que podamos estar seguros de que el nuevo código aún puede funcionar con datos más antiguos.
Chris Hayes
6

¡Por supuesto que está bien!

Siempre necesita una prueba funcional / de integración que ejercite la ruta completa del código. Y la ruta de código completa en este caso significa incluir la evaluación del código generado. Es decir, prueba que el análisis x = 2 + 3 * aproduce código que si se ejecuta con a = 5se establecerá xen 17y si se ejecuta con a = -2se establecerá xen -4.

Debajo de esto, debe hacer pruebas unitarias para bits más pequeños siempre que realmente ayude a depurar el código . Las pruebas más finas que tendrá, la mayor probabilidad de que cualquier cambio en el código también necesite cambiar la prueba, porque la interfaz interna cambia. Dicha prueba tiene poco valor a largo plazo y agrega trabajo de mantenimiento. Entonces hay un punto de rendimientos decrecientes y debes detenerte antes.

Jan Hudec
fuente
4

Las pruebas unitarias le permiten señalar elementos específicos que se rompen y en qué parte del código se rompieron. Entonces son buenos para pruebas de grano muy fino. Las buenas pruebas unitarias ayudarán a disminuir el tiempo de depuración.

Sin embargo, según mi experiencia, las pruebas unitarias rara vez son lo suficientemente buenas como para verificar la operación correcta. Por lo tanto, las pruebas de integración también son útiles para verificar una cadena o secuencia de operaciones. Las pruebas de integración lo ayudan a realizar pruebas funcionales. Sin embargo, como señaló, debido a la complejidad de las pruebas de integración, es más difícil encontrar el lugar específico en el código donde se rompe la prueba. También tiene algo más de fragilidad porque las fallas en cualquier parte de la cadena harán que la prueba falle. Sin embargo, todavía tendrá esa cadena en el código de producción, por lo que probar la cadena real sigue siendo útil.

Lo ideal sería tener ambos, pero en cualquier caso, generalmente es mejor tener una prueba automatizada que no tenerla.

Peter Smith
fuente
0

Realice muchas pruebas en el analizador y, a medida que el analizador pase las pruebas, guarde esas salidas en un archivo para simular el analizador y probar el otro componente.

Tulains Córdova
fuente