() translation by (you can also view the original English article)
Este artículo es una introducción a las aguas avanzadas de las pruebas de AntiPatterns en Rails. Si eres un novato en el desarrollo orientado a pruebas y quieres aprender un par de buenas prácticas muy valiosas, este artículo ha sido escrito exactamente para ti.
Temas
- Let
- Invitados misteriosos
- Pruebas oscuras
- Pruebas lentas
- Accesorios
- Pruebas de fragilidad
- Atributos de datos
Let
1 |
describe Mission do |
2 |
|
3 |
let(:james_bond) { build_stubbed(:agent, name: 'James Bond', number: '007') } |
4 |
let(:mission) { build_stubbed(:mission, title: 'Moonraker') } |
5 |
|
6 |
... |
7 |
|
8 |
end |
El método let
helper en RSpec se utiliza con mucha frecuencia para crear variables de instancia que están disponibles entre múltiples pruebas. Como estudiante entusiasta de las prácticas de TDD, probablemente has escrito tu parte justa de estos, pero seguir esta práctica puede conducir fácilmente a tener un montón de invitados misteriosos que aparecen, ver más abajo, ¡lo que definitivamente no es algo que necesitamos para arruinar nuestra fiesta!
Este efecto secundario particular de let
se ha ganado un poco de reputación por causar posiblemente un mayor mantenimiento de las pruebas y una legibilidad inferior en todo tu conjunto de pruebas. let
seguro que suena tentador porque se evalúa de forma perezosa y ayuda a adherirse al concepto normalmente de cero defectos de DRY y todo eso. Por lo tanto, parece demasiado bueno para no utilizarlo de forma habitual. Su primo cercano, subject
, también debería evitarse la mayoría de las veces.
Es peor cuando empiezas a anidar estas cosas. Las sentencias let
pegadas por todos los bloques descriptivos
anidados son un favorito de todos los tiempos. Creo que no es injusto llamar a esto una receta para ahorcarse rápidamente. Un alcance más limitado es generalmente más fácil de entender y seguir.
No queremos construir un castillo de naipes con accesorios semi-globales que oscurezcan la comprensión y aumenten las posibilidades de romper las pruebas relacionadas. Las probabilidades de crear código de calidad están en nuestra contra con este enfoque. La extracción de la configuración común de los objetos también es más fácil de hacer a través de métodos de rubí o incluso clases si es necesario.
Esta criatura de let
es un accesorio ampliamente compartido que a menudo tendrá que ser descifrado primero antes de saber exactamente qué negocio tiene este objeto en tus pruebas. Además, ir de un lado a otro para entender de qué están hechos exactamente y qué relaciones tienen a través de las asociaciones puede ser un dolor de cabeza que consume tiempo.
La claridad de estos detalles en la configuración de tus pruebas suele ayudar mucho a indicar a otros desarrolladores todo lo que necesitan para trabajar con cada parte concreta de tu conjunto de pruebas, ¡no te olvides de tu futuro yo! En un mundo en el que nunca tuvieras que volver a revisar pruebas concretas e incluso nunca refactorizaras partes de tu conjunto de pruebas, eso podría no importar tanto, pero eso es una quimera por ahora.
Queremos tener el menor número de colaboradores y el menor número de datos posible para cada prueba. Estos let
fixtures pueden acumular un montón de atributos y métodos que los hacen demasiado grandes también.
Si empiezas a ir por el camino del let, a menudo acabarás con objetos bastante gordos que intentan hacer felices a muchas pruebas al mismo tiempo. Seguro que puedes crear muchas variaciones de estas cositas let
, pero eso hace que toda la idea de ellas sea un poco irrelevante, creo. ¿Por qué no ir un paso más allá, evitar let, y confiar en Ruby sin la magia de RSpec DSL?
Estoy más en el campo de estar en el lado de código de configuración repetida para cada prueba que ser excesivamente DRY, oscuro o críptico en mi conjunto de pruebas. Siempre me decantaría por una mayor legibilidad. El método de prueba debe dejar en claro la causa y el efecto de sus piezas involucradas, el uso de colaboradores de objetos que posiblemente se definen lejos de tu ejercicio de prueba no es en tu mejor interés. Si necesitas extraer cosas, utiliza métodos expresivos que encapsulen ese conocimiento.
Estos son casi siempre una apuesta segura. De esta manera también puede suministrar la configuración que realmente necesita para cada prueba y no causar pruebas lentas porque tiene datos innecesarios involucrados Las viejas variables, métodos y clases son a menudo todo lo que necesitas para proporcionar pruebas más rápidas, estables y fáciles de leer.
Invitados misteriosos
Los invitados misteriosos son rompecabezas de RSpec DSL, en realidad. Durante un tiempo, los diversos objetos definidos a través de RSpec DSL let
no son tan difíciles de mantener a raya, pero pronto, cuando el conjunto de pruebas crece, invitas a un montón de invitados misteriosos a tus especificaciones. Esto proporciona a tu futuro yo y a los demás rompecabezas contextuales innecesarios que resolver.
El resultado serán pruebas oscuras que requieren que te pongas en modo Sherlock Holmes. Supongo que suena más divertido de lo que es. En resumen, es una pérdida de tiempo para todos.
Los invitados misteriosos plantean dos cuestiones problemáticas:
- ¿De dónde viene este objeto?
- ¿De qué se compone exactamente?
1 |
describe Mission do |
2 |
|
3 |
let(:agent_01) { build_stubbed(:agent, name: 'James Bond', number: '007') } |
4 |
let(:agent_02) { build_stubbed(:agent, name: 'Moneypenny', number: '243') } |
5 |
let(:title) { 'Moonraker' } |
6 |
let(:mission) { build_stubbed(:mission, title: title) } |
7 |
mission.agents << agent_01 << agent_02 |
8 |
|
9 |
...
|
10 |
|
11 |
...
|
12 |
|
13 |
...
|
14 |
|
15 |
#lots of other tests |
16 |
|
17 |
describe '#top_agent' do |
18 |
it 'returns highest ranking agent associated to a mission' do |
19 |
|
20 |
expect(mission.top_agent).to eq('James Bond') |
21 |
end
|
22 |
end
|
23 |
end
|
Este bloque de descripción para #top_agent
carece de claridad y contexto. ¿De qué agente se trata y de qué misión estamos hablando? Esto obliga a los desarrolladores a ir a la caza de objetos que aparecen de repente en tus pruebas.
Ejemplo clásico de invitado misterioso. Cuando tenemos mucho código entre la prueba relevante y el origen de estos objetos, aumentas las posibilidades de oscurecer lo que está pasando en nuestras pruebas.
La solución es bastante fácil: necesitas "fixtures" frescos y construir versiones locales de los objetos con exactamente los datos que necesitas, ¡y no más que eso! Factory Girl es una buena opción para manejar esto.
Este enfoque puede considerarse más verboso, y es posible que se dupliquen cosas a veces, ya que extraer cosas en un método es a menudo una buena idea, pero es mucho más expresivo y mantiene las pruebas centradas al tiempo que proporciona contexto.
1 |
describe Mission do |
2 |
|
3 |
#... |
4 |
|
5 |
#... |
6 |
|
7 |
#... |
8 |
|
9 |
#lots of other tests |
10 |
|
11 |
describe '#top_agent' do |
12 |
it 'returns a list of all agents associated to a mission' do |
13 |
agent_01 = build_stubbed(:agent, name: 'James Bond', number '007') |
14 |
agent_02 = build_stubbed(:agent, name: 'Moneypenny', number '243') |
15 |
mission = build_stubbed(:mission, title: 'Moonraker') |
16 |
mission.agents << agent_01 << agent_02 |
17 |
|
18 |
expect(mission.top_agent).to eq('James Bond') |
19 |
end
|
20 |
end
|
21 |
end
|
El ejemplo anterior construye todos los objetos necesarios para nuestras pruebas en el caso de prueba real y proporciona todo el contexto deseado. El desarrollador puede centrarse en un caso de prueba concreto y no necesita "descargar" otro caso de prueba, posiblemente totalmente ajeno, para resolver la situación en cuestión. Se acabó la oscuridad.
Sí, tienes razón, este enfoque significa que no estamos logrando el menor nivel de duplicación posible, pero la claridad en estos casos es mucho más importante para la calidad de tu conjunto de pruebas y, por tanto, para la robustez de tu proyecto. La rapidez con la que puedes aplicar eficazmente los cambios en tus pruebas también juega un papel importante en este sentido.
Otro aspecto importante de las pruebas es que el conjunto de pruebas no solo puede funcionar como documentación, sino que debería hacerlo. La duplicación cero no es un objetivo que tenga un efecto positivo para las especificaciones que documentan tu aplicación. Sin embargo, mantener la duplicación innecesaria es un objetivo importante que hay que tener en cuenta: ¡el equilibrio es el rey!
Pruebas oscuras
A continuación se muestra otro ejemplo que trata de configurar todo lo que necesita localmente en la prueba, pero también falla porque no nos está diciendo la historia completa.
1 |
... |
2 |
|
3 |
context "agent status" do |
4 |
it "returns the status of the mission’s agent" do |
5 |
double_o_seven = build_stubbed(:agent) |
6 |
mission = build_stubbed(:mission, agent: double_o_seven) |
7 |
|
8 |
expect(mission.agent_status).to eq(double_o_seven.status) |
9 |
end |
10 |
end |
Estamos creando un agente genérico. ¿Cómo sabemos que es 007? También estamos comprobando el estado del agente, pero tampoco se encuentra en ninguna parte, ni en la configuración ni explícitamente durante la fase de verificación en nuestra declaración expect
. La relación entre el double_o_seven.status
y el estado de la misión podría ser confusa ya que sale de la nada realmente. Podemos hacerlo mejor:
1 |
... |
2 |
|
3 |
context "agent status" do |
4 |
it "returns the status of the mission’s agent" do |
5 |
double_o_seven = build_stubbed(:agent, name: 'James Bond', status: 'Missing in action')) |
6 |
mission = build_stubbed(:mission, agent: double_o_seven) |
7 |
|
8 |
expect(mission.agent_status).to eq('James Bond: Missing in action') |
9 |
end |
10 |
end |
De nuevo, aquí tenemos todo lo que necesitamos para contar una historia. Todos los datos que necesitamos están delante de nosotros.
Pruebas lentas
Así que has empezado a adentrarte en el Test-Driven-Development, y has empezado a apreciar lo que ofrece. Enhorabuena, ¡esto es genial! Estoy seguro de que ni la decisión de hacerlo ni la curva de aprendizaje para llegar a él han sido precisamente pan comido. Pero lo que suele ocurrir después de este paso inicial es que te esfuerzas por tener una cobertura de pruebas completa y empiezas a darte cuenta de que algo falla cuando la velocidad de tus especificaciones empieza a molestarte.
¿Por qué tu suite de pruebas es cada vez más lenta aunque crees que estás haciendo todo lo correcto? ¿Te sientes un poco castigado por escribir pruebas? Las pruebas lentas apestan, ¡a lo grande! Hay un par de problemas con ellos. La cuestión más importante es que las pruebas lentas llevan a omitir pruebas a largo plazo. Una vez que estés en un punto en el que tu conjunto de pruebas tarda una eternidad en terminar, estarás mucho más dispuesto a pensar para ti mismo: "¡Al diablo con esto, las ejecutaré más tarde! I got better things to do than waiting for this stuff to finish.” And you are absolutely right, you have better things to do.
La cuestión es que las pruebas lentas son más propensas a dar la bienvenida a compromisos en la calidad de tu código que lo que puede ser obvio al principio. Las pruebas lentas también alimentan los argumentos de la gente en contra de TDD, injustamente, creo. No quiero ni saber qué dirán los gestores de productos no técnicos si regularmente tienes que salir a tomar un largo café para ejecutar el conjunto de pruebas antes de que puedas seguir trabajando.
¡No vayamos por ese camino! Cuando solo necesitas un poco de tiempo para ejercitar tus pruebas y como resultado obtienes ciclos de retroalimentación súper rápidos para el desarrollo de cada paso de las nuevas características, practicar TDD se vuelve mucho más atractivo y menos argumento. Con un poco de trabajo y cuidado en el camino, podemos evitar las pruebas de cámara lenta con bastante eficacia.
Las pruebas lentas también son un asesino para entrar en la "zona". Si te sacan del flujo con tanta frecuencia en tu proceso, la calidad de tu trabajo en general también puede verse afectada por tener que esperar a que las pruebas lentas vuelvan de un costoso viaje de ida y vuelta. Quieres conseguir el mayor tiempo posible "en la zona", las pruebas insoportablemente lentas son grandes asesinos del flujo.
Otra cuestión que vale la pena mencionar en este contexto es que esto podría llevar a tener pruebas que cubran tu código, pero como no te tomarás el tiempo para terminar de ejercitar toda la suite, o escribirás pruebas después del hecho, el diseño de tus aplicaciones ya no será impulsado por las pruebas. Si no estás en el tren del bombo de las pruebas, puede que esto no te moleste mucho, pero para la gente de TDD, ese aspecto es esencial y no debe ser descuidado.
En definitiva, cuanto más rápidas sean tus pruebas, más ganas tendrás de ejercitarlas, lo cual es la mejor manera de diseñar aplicaciones y de detectar errores a tiempo y con frecuencia.
¿Qué podemos hacer para acelerar las pruebas? Hay dos velocidades que son importantes aquí:
- la velocidad en la que tus pruebas pueden realmente ejecutar tu suite
- la velocidad para obtener información de tu conjunto de pruebas para diseñar tu aplicación
Evita escribir en la base de datos tanto como puedas
Esto no significa que debas evitarlo a toda costa. A menudo no es necesario escribir pruebas que ejerciten la base de datos, y puedes recortar mucho tiempo de ejecución de tus pruebas. Usar solo new
para instanciar un objeto es a menudo suficiente para las configuraciones de prueba. Otra opción viable es falsificar los objetos que no están directamente bajo prueba.
La creación de dobles de prueba es una buena manera de hacer que sus pruebas sean más rápidas mientras mantiene los objetos de colaboración que necesitas para tu configuración súper enfocada y ligera. Factory Girl también te ofrece varias opciones para "crear" de forma inteligente tus datos de prueba. Pero a veces no hay manera de evitar guardar en la base de datos (que es mucho menos a menudo de lo que se podría esperar), y esto es exactamente donde se debe trazar la línea. En cualquier otro momento, evítalo como el infierno y tu suite de pruebas se mantendrá rápida y ágil.
En este sentido, también debes apuntar a una cantidad mínima de dependencias, lo que significa la cantidad mínima de objetos que necesitas que colaboren para que tus pruebas pasen, mientras ahorras lo menos posible en la base de datos en el camino. La eliminación de objetos, que son meros colaboradores y no están directamente bajo prueba, a menudo también hace que tu configuración sea más fácil de digerir y más simple de crear. Un buen aumento de la velocidad en general con muy poco esfuerzo.
Construye tus pruebas teniendo en cuenta la pirámide de pruebas
Esto significa que quieres tener una mayoría de pruebas unitarias en la parte inferior de esta jerarquía, que se centran en partes muy específicas de tu aplicación de forma aislada, y el menor número de pruebas de integración en la parte superior de esta pirámide. Las pruebas de integración simulan que un usuario recorre tu sistema mientras interactúa con un grupo de componentes que se ejercitan al mismo tiempo.
Son fáciles de escribir, pero no son tan fáciles de mantener, y las pérdidas de velocidad no merecen la pena ir por el camino fácil. Las pruebas de integración son prácticamente lo contrario de las pruebas unitarias en lo que respecta a ser de alto nivel y absorber una gran cantidad de componentes que necesitas configurar en tus pruebas, lo cual es una de las principales razones por las que son más lentas que las pruebas unitarias.
Supongo que esto deja claro por qué deben estar en la cima de tu pirámide de pruebas para evitar pérdidas significativas de velocidad. Otra cuestión importante aquí es que quieres tener el menor solapamiento posible entre estas dos categorías de pruebas; después de todo, lo ideal es probar las cosas solo una vez. No se puede esperar una separación perfecta, pero aspirar a la menor cantidad posible es un objetivo razonable y alcanzable.
A diferencia de las pruebas unitarias, con las pruebas de integración se quiere probar el menor número de detalles posible. La mecánica interna ya debería estar cubierta por extensas pruebas unitarias. ¡En su lugar, céntrate solo en las partes más esenciales que las interacciones deben ser capaces de ejercer! La otra razón principal es que un webdriver necesita simular el paso por un navegador y la interacción con una página. Este enfoque no falsifica nada o muy poco, guarda las cosas en la base de datos y realmente pasa por la UI.
Esa es también una de las razones por las que pueden llamarse pruebas de aceptación, ya que estas pruebas tratan de simular una experiencia de usuario real. Este es otro de los principales problemas de velocidad que se quiere ejercitar lo menos posible. Si tienes una tonelada de estas pruebas, supongo que más del 10% de tu número total de pruebas, deberías ir más despacio y reducir ese número al mínimo posible.
Además, ten en cuenta que a veces no es necesario ejercitar toda la aplicación: una prueba de vista más pequeña y enfocada a menudo también sirve. Serás mucho más rápido si reescribes un par de tus pruebas de integración que solo prueben un poco de lógica que no necesite una comprobación de integración completa. Pero tampoco te pongas a escribir una tonelada de ellas; son las que menos provecho ofrecen. Dicho esto, las pruebas de integración son vitales para la calidad de tu conjunto de pruebas, y necesitas encontrar un equilibrio entre ser demasiado tacaño aplicándolas y no tener demasiadas.
Obtén rápidamente información sobre tu aplicación y tus pruebas
La retroalimentación rápida y los ciclos de iteración rápidos son la clave para el diseño de sus objetos. Una vez que empiezas a evitar la ejecución de estas pruebas con frecuencia, estás perdiendo esta ventaja, lo que es una gran ayuda para el diseño de objetos. No esperes a que tu servicio de integración continua entre en funcionamiento para probar toda tu aplicación.
Entonces, ¿cuál es el número mágico que debemos tener en cuenta al realizar las pruebas? Bueno, diferentes personas te dirán diferentes puntos de referencia para esto. Creo que mantenerse por debajo de los 30 segundos es un número muy razonable que hace muy probable el ejercicio de una prueba completa de forma regular. Si dejas ese punto de referencia cada vez más atrás, podría ser necesaria alguna refactorización. Merecerá la pena y te hará sentir mucho más cómodo porque podrás comprobarlo con más regularidad. Lo más probable es que también avances mucho más rápido.
Quieres que ese diálogo con tus pruebas sea lo más rápido posible. No hay que subestimar la necesidad de reforzar este ciclo de retroalimentación mediante el uso de un editor que también te permita ejercitar tus pruebas. Cambiar de un lado a otro entre tu editor y tu terminal no es la mejor solución para manejar esto. Esto se vuelve viejo muy rápidamente.
Si te gusta usar Vim, tienes una razón más para invertir algo de tiempo en ser más eficiente en el uso de tu editor. Hay muchas herramientas útiles disponibles para los usuarios de Vim. Recuerdo que Sublime Text también ofrece la posibilidad de realizar pruebas desde el editor, pero aparte de eso, hay que investigar un poco para saber qué es capaz de hacer tu editor de elección en ese sentido. El argumento que oirás con frecuencia de los entusiastas de TDD es que no quieres dejar tu editor porque en general pasarás demasiado tiempo haciéndolo. Quieres permanecer mucho más en la zona y no perder el tren de pensamiento cuando puedes hacer este tipo de cosas a través de un atajo rápido desde dentro de tu editor de código.
Otra cosa que hay que tener en cuenta es que también quieres ser capaz de cortar las pruebas que quieres ejecutar. Si no necesitas ejecutar todo el archivo, es bueno ejecutar una sola prueba o un bloque que se centre solo en lo que necesitas para obtener información en este momento. Disponer de accesos directos que te ayuden a ejecutar pruebas individuales, archivos individuales o simplemente la última prueba de nuevo te ahorra un montón de tiempo y te mantiene en la zona, por no mencionar el alto grado de conveniencia y la sensación de ser súper genial también. Es increíble lo increíbles que pueden ser a veces las herramientas de codificación.
Una última cosa para el camino. Utiliza un precargador como Spring. Te sorprenderá el tiempo que puedes ahorrar cuando no tengas que cargar Rails para cada prueba. Tu aplicación se ejecutará en segundo plano y no tendrá que arrancar todo el tiempo. ¡Hazlo!
Accesorios
No estoy seguro de si los fixtures siguen siendo un problema para los novatos que llegan a la tierra de Ruby/Rails. En caso de que nadie te haya instruido sobre ellos, intentaré ponerte al día en un santiamén sobre estas temidas cosas.
Los fixtures de la base de datos ActiveRecord son un gran ejemplo de cómo tener toneladas de Mystery Guests en tu suite de pruebas. En los primeros días de Rails y Ruby TDD, las fijaciones YAML eran el estándar de facto para configurar los datos de prueba en tu aplicación. Desempeñaron un papel importante y ayudaron a que la industria avanzara. Sin embargo, hoy en día tienen una reputación razonablemente mala.
Accesorios YAML
1 |
Quartermaster: |
2 |
name: Q |
3 |
favorite_gadget: Broom radio |
4 |
skills: Inventing gizmos and hacking |
5 |
|
6 |
00Agent: |
7 |
name: James Bond |
8 |
favorite_gadget: Submarine Lotus Esprit |
9 |
skills: Getting Bond Girls killed and covert infiltration |
La estructura de tipo hash parece muy práctica y fácil de usar. Incluso puede hacer referencia a otros nodos si quieres simular asociaciones de tus modelos. Pero ahí se acaba la música y muchos dicen que empieza su dolor. Para los conjuntos de datos que son un poco más complicados, los accesorios YAML son difíciles de mantener y difíciles de cambiar sin afectar a otras pruebas. Es decir, puedes hacer que funcionen, por supuesto, después de todo, los desarrolladores los usaron mucho en el pasado, pero montones de desarrolladores estarán de acuerdo en que el precio a pagar por la gestión de los accesorios es un poco tacaño.
Un escenario que definitivamente queremos evitar es el de cambiar pequeños detalles en un fixture existente y causar que toneladas de pruebas fallen. Si estas pruebas que fallan no están relacionadas, la situación es aún peor: un buen ejemplo de que las pruebas son demasiado frágiles. Con el fin de "proteger" las pruebas existentes de este escenario, esto también puede conducir a crecer su conjunto de accesorios más allá de cualquier tamaño razonable, siendo DRY con accesorios es más probable que no en la mesa en ese momento.
Para evitar la ruptura de tus datos de prueba cuando se producen los inevitables cambios, los desarrolladores estaban contentos de adoptar nuevas estrategias que ofrecieran más flexibilidad y comportamiento dinámico. Ahí es donde entró Factory Girl y se despidió de los días de YAML.
Otro problema es la fuerte dependencia entre la prueba y el archivo .yml fixture. Dado que las fijaciones se definen en un archivo .yml separado, los invitados misteriosos también son un gran dolor que espera morderte debido a que son oscuros. ¿He mencionado que los accesorios se importan a la base de datos de pruebas sin pasar por ninguna validación y no se adhieren al ciclo de vida de Active Record? Sí, eso no es tan impresionante, ¡se mire por donde se mire!
Factory Girl te permite evitar todo eso creando objetos relevantes para las pruebas en línea, y solo con los datos necesarios para ese caso específico. El lema es, solo define lo mínimo en tus definiciones de fábrica y agrega el resto en una base de prueba por prueba. Anular localmente (en tus pruebas) los valores por defecto definidos en tus fábricas es un enfoque mucho mejor que tener toneladas de unicornios de fixture esperando a ser obsoletos en un archivo de fixture.
Este enfoque también es más escalable. Factory Girl te da muchas herramientas para crear todos los datos que necesites, tan matizados como quieras, pero también te proporciona toneladas de munición para mantenerte DRY donde sea necesario. Creo que los pros y los contras están bien equilibrados con esta biblioteca. El hecho de no tener que lidiar con las validaciones tampoco es motivo de preocupación. Creo que utilizar el patrón de fábrica para los datos de prueba es más que razonable y es una de las principales razones por las que Factory Girl fue tan bien recibida por la comunidad.
La complejidad es un enemigo que crece rápidamente y que los fixtures de YAML apenas están equipados para afrontar con eficacia. En cierto modo, considero que los accesorios let
son como los esteroides. No solo estás colocándolos aún más lejos, al estar en un archivo separado y todo eso, sino que también estás precargando potencialmente muchos más accesorios de los que realmente podrías necesitar. ¡RIP!
Pruebas de fragilidad
Si los cambios en tus especificaciones conducen a fallos aparentemente no relacionados en otras pruebas, es probable que estés ante un conjunto de pruebas que se ha vuelto frágil debido a las causas mencionadas anteriormente. Estas pruebas, a menudo en forma de rompecabezas e infestadas de misterios, conducen fácilmente a un castillo de naipes inestable.
Cuando los objetos necesarios para las pruebas se definen "lejos" del escenario real de la prueba, no es tan difícil pasar por alto las relaciones que estos objetos tienen con sus pruebas. Cuando el código se elimina, se ajusta o simplemente el objeto de configuración en cuestión se anula accidentalmente, sin saber cómo esto podría influir en otras pruebas alrededor, las pruebas que fallan no son un encuentro raro. Es fácil que parezcan fallos totalmente inconexos. Creo que es justo incluir estos escenarios en la categoría de código estrechamente acoplado.
1 |
describe Mission do |
2 |
let(:agent) { build_stubbed(:agent, name: 'James Bond', number: '007') } |
3 |
let(:title) { 'Moonraker' } |
4 |
let(:mission) { build_stubbed(:mission, title: title) } |
5 |
|
6 |
#... |
7 |
|
8 |
#... |
9 |
|
10 |
#... |
11 |
|
12 |
#lots of other tests |
13 |
|
14 |
describe '#joint_operation_agent_name' do |
15 |
let(:agent) { build_stubbed(:agent, name: 'Felix Leiter', agency: 'CIA') |
16 |
mission.agents << agent |
17 |
|
18 |
it “returns mission’s joint operation’s agent name” do |
19 |
expect(mission.joint_operation_agent_name).to eq('Felix Leiter') |
20 |
end
|
21 |
end
|
22 |
end
|
En este escenario, hemos modificado claramente a nivel local el estado de un objeto que estaba definido en nuestra configuración. El agent
en cuestión es ahora un agente de la CIA y tiene un nombre diferente. La mission
vuelve a salir de la nada también. Una cosa desagradable, realmente.
No es de extrañar que otras pruebas que posiblemente se basan en una versión diferente del agent
empiecen a explotar. Deshagámonos de las tonterías de los let
y construyamos los objetos que necesitamos de nuevo justo donde los probamos, con solo los atributos que necesitamos para el caso de prueba, por supuesto.
1 |
describe Mission do |
2 |
|
3 |
#... |
4 |
|
5 |
#... |
6 |
|
7 |
#... |
8 |
|
9 |
#lots of other tests |
10 |
|
11 |
describe '#joint_operation_agent_name' do |
12 |
agent = build_stubbed(:agent, name: 'Felix Leiter', agency: 'CIA') |
13 |
mission = build_stubbed(:mission) |
14 |
mission.agents << agent |
15 |
|
16 |
it “returns mission’s joint operation’s agent name” do |
17 |
expect(mission.joint_operation_agent_name).to eq('Felix Leiter') |
18 |
end
|
19 |
end
|
20 |
end
|
Es importante entender cómo se relacionan los objetos, idealmente con la mínima cantidad de código de configuración. No querrás enviar a otros desarrolladores a una búsqueda inútil para averiguar estas cosas cuando se tropiecen con tu código.
Si es superdifícil de entender rápidamente y hay que implementar una nueva función ayer, no se puede esperar que estos rompecabezas reciban la máxima prioridad. Esto, a su vez, a menudo significa que las cosas nuevas se desarrollan sobre ese contexto poco claro, lo que es una base frágil para seguir adelante y también una gran invitación a los errores en el camino. La lección que hay que extraer de esto es que no hay que anular cosas siempre que sea posible.
Atributos de datos
Un último consejo útil para evitar las pruebas frágiles es utilizar atributos de datos en las etiquetas HTML. Hazte un favor y úsalos, puedes agradecérmelo después. Esto te permite desvincular los elementos necesarios bajo prueba de la información de estilo que tus diseñadores podrían tocar frecuentemente sin tu participación.
Si codificas con fuerza una clase como class='mission-wrapper'
en tu prueba y un diseñador inteligente decide cambiar este pobre nombre, tu prueba se verá afectada innecesariamente. Y el diseñador no tiene la culpa, por supuesto. Cómo diablos iba a saber que esto afecta a parte de tu suite de pruebas: muy poco probable, al menos.
1 |
<div class='mission data-role='single-mission'> |
2 |
|
3 |
<h2><% = @mission.agent_status %></h2> |
4 |
|
5 |
...
|
6 |
|
7 |
</div> |
1 |
context "mission’s agent status" do |
2 |
it 'does something with a mission' do |
3 |
... |
4 |
|
5 |
... |
6 |
|
7 |
expect(page).to have_css '[data-role=single-mission]' |
8 |
end |
9 |
end |
Esperamos ver algún elemento HTML en una página y lo marcamos con un data-role
. Los diseñadores no tienen ninguna razón para tocar eso, y estás protegido contra las pruebas frágiles que ocurren debido a los cambios en el lado del estilo de las cosas.
Es una estrategia bastante eficaz y útil que básicamente no te cuesta nada a cambio. Lo único que puede ser necesario es mantener una breve conversación con los diseñadores. ¡Es pan comido!
Reflexiones finales
Queremos evitar distraer a la gente que va a leer nuestras pruebas o, peor aún, confundirla. Eso es abrir la puerta a los errores, pero también puede resultar caro porque puede costar un tiempo y una capacidad cerebral muy valiosos. Cuando crees tus pruebas, procura no anular las cosas: no ayuda a crear claridad. Lo más probable es que conduzca a errores sutiles que requieren mucho tiempo y no afectará positivamente al aspecto de la documentación de tu código.
Esto crea una carga innecesaria que podemos evitar. Mutar los datos de las pruebas más de lo absolutamente necesario también merece la pena ser un poco paranoico. Asegúrate de que todo sea lo más sencillo posible. Esto te ayudará a evitar que otros desarrolladores o tu futuro yo se vean envueltos en una búsqueda inútil.
Todavía hay mucho que aprender sobre las cosas que se deben evitar mientras se realizan las pruebas, pero creo que esto es un buen comienzo. La gente que es bastante nueva en todo lo relacionado con TDD debería ser capaz de manejar estos pocos AntiPatrones de inmediato antes de sumergirse en aguas más avanzadas.