Efectividad en pruebas con RSpec 3
Efectividad en pruebas con RSpec 3
Introducción
“¡Nuestras pruebas están rotas otra vez!” "¿Por qué la suite tarda tanto en ejecutarse?" "¿Qué
valor estamos obteniendo de estas pruebas de todos modos?"
Los años pasan y las tecnologías cambian, pero las quejas sobre las pruebas automatizadas
son las mismas. Los equipos intentan mejorar el código y terminan luchando contra las fallas de
las pruebas. Los tiempos de prueba lentos reducen la productividad. Las pruebas mal escritas
hacen un mal trabajo comunicando, guiando el diseño del software o detectando errores.
No importa si es nuevo en las pruebas automatizadas o si las ha estado usando durante años,
este libro lo ayudará a escribir pruebas más efectivas. Por efectivo, nos referimos a pruebas que
le brindan más valor que el tiempo dedicado a escribirlas.
Usaremos el marco RSpec 3 para explorar el arte de escribir pruebas. Cada aspecto de RSpec
fue diseñado para resolver algún problema que los desarrolladores han encontrado en la
naturaleza. Con él, puede crear aplicaciones de Ruby con confianza.
Como usar este libro
Con este libro, aprenderá RSpec 3 en tres fases:
• Parte I: Ejercicios introductorios para familiarizarse con RSpec
• Parte II: un ejemplo resuelto que abarca varios capítulos, para que pueda ver
RSpec en acción en un proyecto de tamaño significativo
• Partes III–V: una serie de inmersiones profundas en aspectos específicos de RSpec, que
le ayudará a aprovechar al máximo RSpec
Escribimos este libro para ser leído de cabo a rabo. Cualquiera que sea su nivel de experiencia,
leer los capítulos en orden le dará el mayor valor. Sin embargo, si tiene poco tiempo y quiere
saber dónde buscar primero, podemos hacerle algunas sugerencias.
Si está familiarizado con otros marcos de prueba pero es nuevo en RSpec, le recomendamos
que lea las dos primeras partes del libro y luego pruebe RSpec en una.
informar fe de erratas • discutir
Machine Translated by Google
Introducción • xiv
de sus propios proyectos. Al hacerlo, es probable que tenga preguntas para las que puede
consultar capítulos específicos de análisis profundo.
Si es un usuario de RSpec desde hace mucho tiempo, puede comenzar con las Partes III, IV y V.
Estos contienen recetas detalladas para situaciones que probablemente hayas encontrado en la
naturaleza. Más adelante, puede volver al principio del libro para repasar la filosofía de RSpec.
Finalmente, si usa RSpec 3 todos los días, tenga a mano las partes más detalladas de este libro.
Los encontrará útiles para consultarlos en situaciones específicas. ¡Nosotros lo hacemos y
hemos estado usando RSpec durante años!
Fragmentos de código
Hemos proporcionado fragmentos de código a lo largo del libro que muestran cómo se usa RSpec
en situaciones del mundo real. La mayoría de estos ejemplos están destinados a que los siga en
su computadora, particularmente los de la Parte I y la Parte II.
Un fragmento típico contendrá una o más líneas de código Ruby destinadas a que las escriba en
su editor de texto para que pueda ejecutarlas más tarde. Aquí hay un ejemplo:
00introduction/01/type_me_in.rb
pone "Puedes escribirme; ¡está bien!"
Mostraremos cada archivo de código unas pocas líneas a la vez. Si necesita más contexto para
un fragmento dado, puede hacer clic en el título del archivo (en el libro electrónico) o abrir el
código fuente del libro (enlazado al final de este capítulo) para ver el archivo completo de una
sola vez.
Algunos ejemplos de código no tienen banner; estos generalmente representan una sesión en
su terminal, ya sea en Ruby interactivo (IRB) o en un shell como Bash. Para fragmentos de IRB,
ejecutará el comando de terminal irb y luego escribirá solo las partes después del indicador verde
>> :
>> %w[Escriba solo el bit después de la indicación].join(' ')
=> "Escriba solo el bit después de la indicación"
En su lugar , representaremos las sesiones de shell con un símbolo de $ verde . Al igual que con
las sesiones de IRB, no escribirá en el indicador ni en las líneas de salida, solo los comandos
después del indicador:
$ echo '¡La RSpec es genial!'
RSpec es genial!
informar fe de erratas • discutir
Machine Translated by Google
RSpec y Desarrollo Impulsado por el Comportamiento • xv
Más adelante en el libro, a veces mostramos fragmentos aislados de un proyecto más grande; estos no
están diseñados para que los ejecute en su computadora. Si está interesado en ejecutarlos por su cuenta,
puede descargar todos los archivos del proyecto desde el repositorio de código fuente del libro.
La mayoría de los capítulos tienen una sección “Tu Turno” con ejercicios para que pruebes. ¡No te saltes
estos! Practicar por tu cuenta asegurará que cada capítulo se base en las habilidades que has perfeccionado
a lo largo del libro.
RSpec y desarrollo impulsado por el comportamiento
RSpec se anuncia a sí mismo como un marco de prueba de desarrollo basado en el comportamiento (BDD).
Nos gustaría tomarnos un momento para hablar sobre el uso que hacemos de ese término, junto con un
término relacionado, desarrollo basado en pruebas (TDD).
Sin TDD, puede verificar el comportamiento de su programa ejecutándolo manualmente o escribiendo un
arnés de prueba único. En situaciones en las que tiene la intención de descartar el programa poco después,
estos enfoques están bien. Pero cuando el mantenimiento a largo plazo es una prioridad, TDD brinda
beneficios importantes.
Con TDD, escribe cada caso de prueba justo antes de implementar el siguiente comportamiento. Cuando
tiene pruebas bien escritas, termina con un código más fácil de mantener. Puede realizar cambios con la
confianza de que su conjunto de pruebas le informará si ha fallado algo.
Sin embargo, el término TDD es un poco inapropiado. A pesar de que tiene la palabra "prueba" en el nombre,
TDD no se trata solo de sus pruebas. Se trata de la forma en que permiten mejoras intrépidas en su diseño.
Por esta razón, Dan North acuñó el término desarrollo impulsado por el comportamiento en 2006 para
encapsular las partes más importantes de TDD.1
BDD pone el énfasis donde se supone que debe estar: el comportamiento de su código.
La comunidad destaca la importancia de la expresividad en tus pruebas, algo de lo que hablaremos mucho
en este libro. BDD también se trata de tratar sus requisitos de software con el mismo cuidado, ya que son
otra expresión de comportamiento. Se trata de involucrar a todas las partes interesadas en la redacción de
las pruebas de aceptación.
1. https://dannorth.net/introducingbdd/
informar fe de erratas • discutir
Machine Translated by Google
Introducción • xvi
Como marco de prueba, RSpec encaja bastante bien en un flujo de trabajo BDD. RSpec lo ayuda a "obtener
las palabras correctas" y especificar exactamente lo que quiere decir en sus pruebas.
Puede practicar fácilmente el enfoque de afuera hacia adentro favorecido en BDD, donde comienza con las
pruebas de aceptación y avanza hacia las pruebas unitarias.2 En todos los niveles, sus pruebas expresivas
guiarán su diseño de software.
Sin embargo, RSpec y BDD no son sinónimos. No tienes que practicar BDD para usar RSpec, ni usar RSpec
para practicar BDD. Y gran parte de BDD está fuera del alcance de RSpec; no hablaremos en este libro sobre
la participación de los interesados, por ejemplo.
Quienes somos
Myron Marston comenzó a usar RSpec en 2009 y comenzó a contribuir en 2010. Ha sido su principal
mantenedor desde finales de 2012. Estas son solo algunas de las mejoras importantes que ha realizado en
RSpec:
• Comparadores componibles, que expresan exactamente los criterios de aprobación/rechazo que necesita
• rspec bisect, que encuentra el conjunto mínimo de casos de prueba para reproducir una falla
• Integración de las bibliotecas de simulación y aserciones de RSpec con el marco Minitest que se envía
con Ruby
• Las opciones onlyfailures y nextfailure que le permiten volver a ejecutar solo su
pruebas para que pueda corregir errores más rápidamente
Con el conocimiento interno que Myron proporciona en este libro, aprenderá todas estas técnicas y más. Al
final, podrá librarse de cualquier problema que tenga con su conjunto de pruebas.
Ian Dees se topó con una antigua versión beta de RSpec en 2006. Era justo lo que necesitaba para crear las
pruebas de aceptación automatizadas para un dispositivo de pantalla táctil integrado. Desde entonces, ha
usado y enseñado RSpec para probar todo, desde pequeños microcontroladores hasta aplicaciones web y
de escritorio con todas las funciones.
Quien eres
Esperamos que este libro sea útil para una amplia gama de desarrolladores, desde personas que recién
comienzan con RSpec hasta aquellos que han escrito miles de pruebas con él. Dicho esto, hemos hecho
algunas suposiciones para evitar que el libro se atasque demasiado con material introductorio.
2. https://dannorth.net/whatsinastory/
informar fe de erratas • discutir
Machine Translated by Google
Una nota sobre las versiones • xvii
Primero, asumimos que está familiarizado con Ruby. No necesitas ser un experto.
Nos ceñimos a los conceptos básicos de clases, métodos y bloques en su mayor parte. Le indicaremos
que instale varias gemas de Ruby, por lo que también será útil familiarizarse con ese proceso. Si es
nuevo en Ruby, le recomendamos que primero aprenda un poco el lenguaje utilizando recursos como
el libro electrónico Learn Ruby the Hard Way de Zed Shaw o los tutoriales de Ruby en exercism.io.3,4
Aunque construirá un servicio web a lo largo de varios capítulos, no asumimos que ya es un
desarrollador web. Mucha gente usa RSpec para probar aplicaciones de línea de comandos,
aplicaciones GUI, etc. Explicaremos algunos conceptos de desarrollo web a medida que surjan
durante la discusión.
Cuando tenemos contenido destinado a una audiencia específica, como personas que provienen de
una versión anterior de RSpec o personas que son nuevas en el desarrollo web, colocaremos ese
contenido en una barra lateral.
Una nota sobre las versiones
Las bibliotecas que usamos en este libro, tanto las del marco RSpec como otras dependencias como
Sinatra y Sequel, están diseñadas para ser compatibles con versiones anteriores en actualizaciones
de versiones menores. Los ejemplos de código que ve aquí deberían funcionar bien en futuras
versiones de estas bibliotecas, al menos hasta sus próximas versiones principales .
Si bien hemos probado este código en varias versiones de Ruby desde Ruby 2.2, tendrá la mejor
experiencia si sigue exactamente las mismas versiones que mencionamos en el texto: Ruby 2.4,
RSpec 3.6, etc. . Con las mismas versiones que usamos, debe obtener resultados que reflejen
fielmente lo que mostramos en el libro.
Recursos en línea
Este libro tiene un sitio web.5 Allí encontrará enlaces al código fuente, foros de discusión y erratas.
También hemos configurado repositorios de GitHub que contienen todos los ejemplos del libro,
además de una versión del proyecto que construirá en Creación de un
6
Aplicación con RSpec 3.
3. https://learnrubythehardway.org
4. http://exercism.io/languages/ruby/about
5. https://pragprog.com/book/rspec3/effecttestingwithrspec3
6. https ://github.com/rspec3libro
informar fe de erratas • discutir
Machine Translated by Google
Introducción • xviii
Para obtener más información sobre RSpec, puede consultar el sitio oficial y la
documentación completa para desarrolladores.7,8
Myron Marston
Mantenedor principal de RSpec
[email protected]
Seattle, WA, agosto de 2017
Ian Dees
Ingeniero de software sénior, New Relic
[email protected]
Portland, OR, agosto de 2017
7. http://rspec.info
8. http://rspec.info/documentación/
informar fe de erratas • discutir
Machine Translated by Google
Parte I
Empezando
¡Bienvenido a RSpec! En esta parte del libro, se familiarizará
con el marco mientras escribe sus primeras pruebas de
trabajo.
Primero, instalará RSpec y escribirá sus primeras
especificaciones: la jerga de RSpec para las pruebas. La API
de RSpec se trata de decidir cómo desea que se comporte
su código y expresar esa decisión en sus especificaciones.
Una vez que tenga los conceptos básicos, no podemos
resistirnos a mostrarle algunas de las cosas que hacen que RSpec sea especial.
Machine Translated by Google
En este capítulo, verá:
• Cómo instalar RSpec y escribir su primera especificación
• Cómo organizar sus especificaciones usando describe y it • Cómo
verificar los resultados deseados con expect • Cómo
interpretar las fallas de las pruebas •
Cómo mantener sus especificaciones libres de códigos de configuración repetidos
CAPÍTULO 1
Primeros pasos con RSpec
RSpec 3 es un marco de prueba productivo de Ruby. Decimos productivo porque todo sobre él (su
estilo, API, bibliotecas y configuraciones) está diseñado para ayudarlo a escribir un software
excelente.
Escribir pruebas efectivas lo ayuda a lograr ese objetivo de enviar su aplicación.
Aquí tenemos una definición específica de efectivo : ¿esta prueba paga el costo de escribirla y
ejecutarla? Una buena prueba proporcionará al menos uno de estos beneficios:
• Guía de diseño: ayudándole a destilar todas esas fantásticas ideas en su cabeza en un código
ejecutable y mantenible
• Red de seguridad: encontrar errores en su código antes de que lo hagan sus clientes
• Documentación: capturar el comportamiento de un sistema de trabajo para ayudar a su
mantenedores
A medida que siga los ejemplos de este libro, practicará varios hábitos que lo ayudarán a evaluar
de manera efectiva:
• Cuando describe con precisión lo que quiere que haga su programa, evita ser demasiado
estricto (y fallar cuando cambia un detalle irrelevante) o demasiado laxo (y obtener una falsa
confianza de las pruebas incompletas).
• Al escribir sus especificaciones para informar fallas con el nivel correcto de detalle, brinda la
información suficiente para encontrar la causa de un problema, sin ahogarse en una producción
excesiva.
• Al separar claramente el código de prueba esencial del código de configuración ruidoso,
comunica lo que realmente se espera de la aplicación y evita repetir detalles innecesarios.
• Cuando reordena, perfila y filtra sus especificaciones, descubre dependencias de orden,
pruebas lentas y trabajo incompleto.
informar fe de erratas • discutir
Machine Translated by Google
Capítulo 1. Primeros pasos con RSpec • 4
Todo lo que escribirá a lo largo de este libro servirá para una de estas prácticas.
Instalación de RSpec
Primero, para usar RSpec 3, necesita una versión reciente de Ruby. Hemos probado nuestros
ejemplos en este libro con Ruby 2.4 y lo alentamos a que use esa versión para la ruta más fácil.
Puede obtener resultados ligeramente diferentes en otras versiones de Ruby. Si está usando
algo más antiguo, vaya a la página de descarga de Ruby y tome uno más nuevo.1
RSpec está hecho de tres gemas Ruby independientes:
• rspeccore es el arnés de prueba general que ejecuta sus especificaciones.
• rspecexpectations proporciona una sintaxis poderosa y legible para verificar las propiedades
de su código.
• rspecmocks facilita aislar el código que está probando del resto
del sistema.
Puede instalarlos individualmente y combinarlos con otros marcos de prueba, bibliotecas de
afirmación y herramientas de simulación. Pero van muy bien juntos, así que los usaremos juntos
en este libro.
Para instalar todo RSpec, simplemente instale la gema rspec:
$ gem install rspec v 3.6.0 Instalado
con éxito rspecsupport3.6.0 Instalado con éxito rspec
core3.6.0 Instalado con éxito difflcs1.3 Instalado
con éxito rspecexpectations3.6.0 Instalado
con éxito rspecmocks3.6 .0 Instalado con éxito rspec3.6.0
6 gemas instaladas
Puede ver las tres gemas enumeradas aquí, además de un par de bibliotecas de apoyo y la gema
contenedora rspec, para un total de seis gemas.
Ahora que RSpec está en su sistema, hagamos una revisión rápida para asegurarnos de que esté listo:
$ rspec version
RSpec 3.6
rspeccore 3.6.0
rspecexpectations 3.6.0 rspec
mocks 3.6.0 rspec
support 3.6.0
Perfecto. Es hora de tomarlo para una prueba de manejo.
1. https://www.rubylang.org
informar fe de erratas • discutir
Machine Translated by Google
Su primera especificación • 5
Tu primera especificación
En lugar de probar un sistema de producción intrincado, imaginemos algo un poco más concreto:
un sándwich. Sí, es una tontería, pero hará que los ejemplos sean breves; además, estábamos
hambrientos mientras escribíamos este capítulo.
¿Cuál es la propiedad más importante de un sándwich? ¿El pan? ¿Los condimentos? No, lo más
importante de un sándwich es que tenga buen sabor. Digámoslo usando el lenguaje de RSpec.
RSpec usa las palabras describe y it para expresar conceptos en un formato conversacional:
• “Describa un sándwich ideal”
• “Primero, es delicioso”
Cree un nuevo directorio de proyecto, con un subdirectorio llamado spec. Dentro de «tu_proyecto»/
spec, crea un archivo llamado sandwich_spec.rb con el siguiente contenido:
01primeros pasos/01/spec/
sandwich_spec.rb RSpec.describe 'Un sándwich ideal' do
es 'delicioso' hacer
final
final
Los desarrolladores trabajan de esta manera con RSpec todo el tiempo; comienzan con un
esquema y lo completan a medida que avanzan. Agregue las siguientes líneas resaltadas a su esquema:
01primeros pasos/02/spec/
sandwich_spec.rb RSpec.describe 'Un sándwich ideal' do
es 'delicioso' hacer
sandwich = Sandwich.new('delicioso', []) sabor =
sandwich.gusto expect(gusto).to
eq('delicioso')
fin
fin
Antes de ejecutar esta especificación, analicemos un poco el código.
Grupos, ejemplos y expectativas
Este archivo define sus pruebas, conocidas en RSpec como sus especificaciones, abreviatura de
especificaciones (porque especifican el comportamiento deseado de su código). El bloque externo
RSpec.describe crea un grupo de ejemplo. Un grupo de ejemplo define lo que está probando (en
este caso, un sándwich) y mantiene juntas las especificaciones relacionadas.
informar fe de erratas • discutir
Machine Translated by Google
Capítulo 1. Primeros pasos con RSpec • 6
El bloque anidado, el que comienza con 'es delicioso', es un ejemplo del uso del sándwich.
(Otros marcos de prueba podrían llamar a esto un caso de prueba). A medida que escribe
especificaciones, tenderá a mantener cada ejemplo enfocado en una parte particular del
comportamiento que está probando.
Pruebas frente a especificaciones frente a ejemplos
¿Cuál es la diferencia entre pruebas, especificaciones y ejemplos? Todos se refieren al código que escribes
para comprobar el comportamiento de tu programa. Los términos son semiintercambiables, pero cada uno
tiene un énfasis diferente:
• Una prueba valida que un fragmento de código funciona correctamente.
• Una especificación describe el comportamiento deseado de un
fragmento de código. • Un ejemplo muestra cómo se pretende utilizar una API en particular.
Usaremos todos estos términos en este libro, según el aspecto de las pruebas que queramos enfatizar.
Dentro del ejemplo, sigues el patrón Arrange/Act/Assert: configura un objeto, haz algo con
él y verifica que se comportó de la manera que deseas.2 Aquí, creas un sándwich, le pides
su sabor y verificas que el resultado es delicioso.
La línea que comienza con expect es una expectativa. Estas son como afirmaciones en
otros marcos de prueba, pero (como veremos más adelante) con algunos trucos más bajo
la manga.
Eche un vistazo más a los tres métodos RSpec que usamos en este fragmento:
• RSpec.describe crea un grupo de ejemplo (conjunto de pruebas
relacionadas). • crea un ejemplo (prueba
individual). • expect verifica un resultado esperado (afirmación).
Estos son los componentes básicos que buscará una y otra vez a medida que construya
sus conjuntos de pruebas.
Aprovechar al máximo RSpec
Las especificaciones de su sándwich tienen dos propósitos:
• Documentar lo que debe hacer su sándwich • Verificar
que el sándwich haga lo que se supone que debe hacer
Argumentaríamos que esta especificación se adapta bastante bien al primer propósito.
Incluso alguien nuevo en el proyecto puede leer este código y ver que los sándwiches deben
ser deliciosos.
2. http://xp123.com/articles/3aarrangeactassert/
informar fe de erratas • discutir
Machine Translated by Google
Comprender el fracaso • 7
Echa un vistazo a la línea de espera . Se lee casi como su equivalente en inglés: "Esperamos que el sabor del
sándwich sea delicioso". Con las afirmaciones de un marco de prueba tradicional, podría escribir una línea como
la siguiente:
01primerospasos/02/sandwich_test.rb
assert_equal('delicioso', sabor, 'Sándwich no es delicioso')
Este código funciona bien, pero en nuestra opinión es menos claro que la versión RSpec. A lo largo de este libro
vamos a insistir en mantener sus especificaciones legibles.
Las especificaciones también son código de trabajo. Debería poder ejecutarlos y verificar que el sándwich
realmente se comporte como se diseñó. En la siguiente sección, lo hará.
Comprender el fracaso
Para probar sus especificaciones, ejecute el comando rspec desde el directorio de su proyecto. RSpec buscará
dentro del subdirectorio de especificaciones los archivos llamados «algo»_spec.rb y los ejecutará:
$ especificación
Fallas:
1) Un sándwich ideal es delicioso Falla/Error:
sándwich = Sandwich.new('delicioso', [])
Error de nombre:
Sándwich constante no inicializado
# ./spec/sandwich_spec.rb:4:in ̀bloque (2 niveles) en <superior (obligatorio)>'
Finalizó en 0,00076 segundos (los archivos tardaron 0,08517 segundos en cargarse) 1
ejemplo, 1 error
Ejemplos fallidos:
rspec ./spec/sandwich_spec.rb:3 # Un sándwich ideal es delicioso
RSpec nos brinda un informe detallado que muestra qué especificación falló, la línea de código donde ocurrió el
error y una descripción del problema.
Además, la salida es en color. RSpec usa color para enfatizar diferentes partes de la salida:
• Las especificaciones aprobatorias
son verdes. • Las especificaciones que fallan y los detalles de la
falla están en rojo. • Las descripciones de ejemplo y el texto estructural están
en negro. • Los detalles adicionales, como los trazos de pila, son azules.
• Especificaciones pendientes (que veremos más adelante en Marcado de trabajo en progreso, en la página
26) son de color amarillo.
informar fe de erratas • discutir
Machine Translated by Google
Capítulo 1. Primeros pasos con RSpec • 8
Encontrar lo que está buscando en el resultado antes de que lo haya leído es un tremendo aumento de la
productividad. En De escribir especificaciones a ejecutarlas, veremos cómo ver nuestra salida de
especificaciones en diferentes formatos.
Comenzar con una especificación fallida, como lo ha hecho aquí, es el primer paso de la práctica de
desarrollo Red/Green/Refactor esencial para TDD y BDD.3 Con este flujo de trabajo, se asegurará de que
cada ejemplo detecte el código fallido o faltante antes implementas el comportamiento que estás probando.
El siguiente paso después de escribir una especificación fallida es hacer que pase. Para este ejemplo,
todo lo que tiene que hacer es agregar la siguiente línea en la parte superior del archivo:
01primeros pasos/03/spec/sandwich_spec.rb
Sandwich = Struct.new(:gusto, :ingredientes)
Aquí, ha definido una estructura Sandwich con dos campos, que es todo lo que necesitan sus
especificaciones para aprobar. Por lo general, colocaría este tipo de lógica de implementación en un
archivo separado, generalmente en el directorio lib . Para este ejemplo simple, está bien definirlo
directamente en el archivo de especificaciones.
Ahora, cuando vuelvas a ejecutar tus especificaciones, pasarán:
$ especificación
Terminado en 0.00101 segundos (los archivos tardaron 0.08408 segundos en cargarse) 1
ejemplo, 0 fallas
Los tres métodos que usó en su especificación (descríbalo y espere ) son las API principales de RSpec.
Puede recorrer un largo camino con RSpec usando solo estas piezas sin ningún otro adorno.
Dicho esto, no podemos resistirnos a mostrarte algunas cosas más.
Configuración para compartir (pero no sándwiches)
A medida que escribe más especificaciones, se encontrará repitiendo el código de configuración de un
ejemplo a otro. Esta repetición abarrota sus pruebas y hace que cambiar el código de configuración sea
más difícil.
Afortunadamente, RSpec proporciona formas de compartir una configuración común en varios ejemplos.
Comencemos agregando un segundo ejemplo después del primer bloque it :
01gettingstarted/04/spec/sandwich_spec.rb '
me permite agregar ingredientes' hacer
sandwich = Sandwich.new('delicioso', [])
3. https://webuild.envato.com/blog/makingthemostofbddpart1/
informar fe de erratas • discutir
Machine Translated by Google
Configuración para compartir (pero no sándwiches) • 9
sandwich.toppings << 'cheese'
toppings = sandwich.toppings
expect(toppings).not_to be_empty end
La expectativa de este ejemplo introduce dos nuevos giros. Primero, puede negar su expectativa, es
decir, verificar si hay falsedad, usando not_to en lugar de to. En segundo lugar, puede probar que
una colección como Array o Hash está vacía usando be_empty. Verá más sobre cómo funcionan
estas construcciones en Explorando las expectativas de RSpec.
Ahora, ejecuta tu nuevo ejemplo:
$ especificación
..
Terminado en 0.00201 segundos (los archivos tardaron 0.09252 segundos en
cargarse) 2 ejemplos, 0 fallas
Esta especificación funciona bien, pero es un poco repetitiva. Estamos copiando el código de
configuración del sándwich en cada ejemplo. Esta duplicación hace que sea más difícil cambiar el
código común más adelante. También nubla nuestros ejemplos con información de configuración.
Pongamos a disposición de todas nuestras pruebas un bocadillo común. RSpec admite varias formas
de hacerlo:
• Los ganchos RSpec se ejecutan automáticamente en momentos específicos durante
la prueba. • Los métodos auxiliares son métodos regulares de Ruby; usted controla cuándo se
ejecutan. • La construcción let de RSpec inicializa los datos a pedido.
Cada una de estas técnicas tiene sus ventajas; los usará todos a medida que siga los ejemplos del
libro. Echemos un vistazo a cada uno de ellos para ver cómo los usaría.
Manos
Lo primero que intentaremos es un RSpec antes del enlace, que se ejecutará automáticamente antes
de cada ejemplo. Agregue la siguiente línea resaltada dentro de su grupo de ejemplo, justo dentro
del bloque RSpec.describe :
01primeros pasos/05/spec/sandwich_spec.rb
RSpec.describe 'Un sándwich ideal' do
antes { @sandwich = Sandwich.new('delicioso', []) }
RSpec realiza un seguimiento de todos los ganchos que ha registrado. Cada vez que RSpec esté a
punto de comenzar a ejecutar uno de sus ejemplos, ejecutará cualquier enganche anterior que
corresponda. La variable de instancia @sandwich se configurará y estará lista para usar.
informar fe de erratas • discutir
Machine Translated by Google
Capítulo 1. Primeros pasos con RSpec • 10
El código de configuración se comparte entre las especificaciones, pero no la instancia individual de
Sandwich . Cada ejemplo tiene su propio sándwich. Eso significa que puede agregar coberturas (como
lo hace en la segunda especificación) con la confianza de que los cambios no afectarán a otros ejemplos.
Ahora que ha movido el código de configuración a un lugar común, puede eliminar el código repetido de
sus ejemplos. Deberá usar @sandwich en lugar de sándwich:
01gettingstarted/05/spec/sandwich_spec.rb
es 'delicioso' hacer
gusto = @sandwich.gusto
esperar (sabor). a eq ('delicioso') final
' me permite agregar ingredientes' do
@sandwich.toppings << 'cheese' toppings
= @sandwich.toppings
expect(toppings).not_to be_empty end
Una vez que haya realizado los cambios, ejecute sus nuevas especificaciones. Todos deberían pasar,
como antes.
Los ganchos son excelentes para ejecutar código de configuración común que tiene efectos secundarios
en el mundo real. Si necesita borrar una base de datos de prueba antes de cada ejemplo, un enlace es
un excelente lugar para hacerlo.
También funcionan bien para esconder sus objetos de prueba en variables de instancia, como lo hemos
hecho aquí. Sin embargo, las variables de instancia tienen algunos inconvenientes.
Primero, si escribe mal @sandwich, Ruby devolverá silenciosamente nil en lugar de abortar con una
falla de inmediato. El resultado suele ser un mensaje de error confuso sobre el código que está lejos del
error tipográfico.
En segundo lugar, para refactorizar sus especificaciones para usar variables de instancia, tuvo que
revisar todo el archivo y reemplazar sándwich con @sandwich.
Finalmente, cuando inicializas una variable de instancia en un anzuelo anterior , pagas el costo de ese
tiempo de configuración para todos los ejemplos del grupo, incluso si algunos de ellos nunca usan la
variable de instancia. Eso es ineficiente y puede ser bastante notable al configurar objetos grandes o
costosos.
Probemos un enfoque diferente. Deshaga los cambios que realizó para el enlace anterior para que su
archivo se vea como antes:
01primeros pasos/04/spec/sandwich_spec.rb
Sandwich = Struct.new(:gusto, :ingredientes)
informar fe de erratas • discutir
Machine Translated by Google
Configuración para compartir (pero no sándwiches) • 11
RSpec.describe 'Un bocadillo ideal' do
es 'delicioso' hacer
sandwich = Sandwich.new('delicioso', [])
gusto = sandwich.gusto
esperar (sabor). a eq ('delicioso') final
' me permite agregar ingredientes' hacer
sandwich = Sandwich.new('delicioso', [])
sandwich.toppings << 'cheese' toppings =
sandwich.toppings
expect(toppings).not_to be_empty end end
Ahora, veremos un enfoque de Ruby más tradicional para reducir la duplicación.
Métodos auxiliares
RSpec hace mucho por nosotros; es fácil olvidar que es simplemente Ruby debajo.
Cada grupo de ejemplo es una clase de Ruby, lo que significa que podemos definir métodos en él. Justo
después de la línea de descripción , agregue el siguiente código:
01primerospasos/06/spec/sandwich_spec.rb def
sándwich
Sandwich.new('delicioso', []) end
Hemos trasladado los pasos de configuración comunes a un método de ayuda típico de Ruby, como lo
haría en una de sus propias clases. Ahora, puede eliminar las líneas sándwich = de sus ejemplos. ...
Sin embargo, este método de ayuda no está listo para el horario de máxima audiencia. Mira lo que sucede
cuando volvemos a ejecutar las especificaciones:
$ rspec .F
Fallas:
1) Un sándwich ideal me permite agregar ingredientes
Falla/Error: expect(toppings).not_to be_empty esperaba que ̀[].empty?`
devolviera falso, se volvió verdadero
# ./spec/sandwich_spec.rb:18:in ̀bloque (2 niveles) en <superior (obligatorio)>'
Terminado en 0.0116 segundos (los archivos tardaron 0.08146 segundos en cargarse) 2 ejemplos, 1
falla
Ejemplos fallidos:
rspec ./spec/sandwich_spec.rb:14 # Un sándwich ideal me permite agregar ingredientes
informar fe de erratas • discutir
Machine Translated by Google
Capítulo 1. Primeros pasos con RSpec • 12
En nuestro ejemplo de coberturas, llamamos sándwich dos veces. Cada llamada crea una nueva instancia.
Por lo tanto, el sándwich al que le agregamos ingredientes es diferente al sándwich al que le estamos
revisando los ingredientes.
La solución tradicional de Ruby es una técnica común llamada memorización, donde almacenamos los
resultados de una operación (crear un sándwich) y nos referimos a la copia almacenada a partir de ese
momento. Para obtener más información sobre la memorización, consulte el artículo de Justin Weiss.4
Una implementación típica de Ruby podría parecerse al siguiente código.
Pruebe esta nueva definición para su método sándwich :
01primerospasos/07/spec/sandwich_spec.rb
def sándwich
@sándwich ||= Sándwich.nuevo('delicioso', []) end
Ahora, vuelva a ejecutar sus especificaciones para asegurarse de que su método memorizado corrigió el
asunto.
Este patrón es bastante fácil de encontrar en el código Ruby en la naturaleza, pero no está exento de
trampas. El operador ||= funciona al ver si @sandwich es "falso", es decir, falso o nulo, antes de crear un
nuevo sándwich. Eso significa que no funcionará si en realidad estamos tratando de almacenar algo falso.
Considere cómo probaría una clase de tostadora que le permita buscar una tostadora específica por su
número de serie. Si no existe tal tostadora, la búsqueda devolverá cero. Aquí hay un enfoque ingenuo para
escribir un método auxiliar para almacenar en caché este valor:
01primeros pasos/08/
tostadora.rb def
tostadora_actual @tostadora_actual ||= Tostadora.find_by_serial('HHGG42')
end
Si la búsqueda resulta vacía, almacenaremos cero en la variable @current_toaster .
En la próxima llamada al método auxiliar, haremos el equivalente del siguiente código:
01primerospasos/08/toaster.rb
@current_toaster = nil || Tostadora.find_by_serial('HHGG42')
Llamaremos al método potencialmente lento find_by_serial() cada vez; en realidad no estamos
memorizando nada. Podríamos inventar una solución para manejar este caso extremo. Pero con RSpec,
no es necesario.
4. http://www.justinweiss.com/articles/4simplememoizationpatternsinrubyandonegem/
informar fe de erratas • discutir
Machine Translated by Google
Tu turno • 13
Compartir objetos con let
RSpec nos da una construcción alternativa, let, que maneja este caso límite. También nos da una
sintaxis más agradable y menos habladora. Elimine su método sándwich y reemplácelo con el
siguiente código:
01primeros pasos/09/spec/sandwich_spec.rb
let(:sandwich) { Sandwich.new('delicious', []) }
Puede pensar en let como vincular un nombre (sándwich) al resultado de un cálculo (el bloque). Al
igual que con un método auxiliar memorizado, RSpec ejecutará el bloque la primera vez que un
ejemplo llame a sándwich.
Cuando ejecute sus especificaciones nuevamente con su definición let , aún deberían pasar.
Es posible exagerar en nuestra búsqueda para reducir la duplicación. Podemos terminar con conjuntos
de pruebas que solo podemos leer rebotando sin cesar entre los ejemplos y las declaraciones let .
Nuestra recomendación es utilizar estas técnicas de código compartido donde mejoran la capacidad
de mantenimiento, reducen el ruido y aumentan la claridad.
Tu turno
En este capítulo, instaló RSpec y lo probó. Escribió algunas especificaciones simples para tener una
idea de las partes principales del marco. Viste cómo ejecutar tus ejemplos e interpretar el resultado.
Finalmente, exploró algunas formas diferentes de reducir la duplicación en sus especificaciones.
Ahora es el momento de poner a prueba este conocimiento.
Ejercicios
1. Le mostramos tres formas principales de reducir la duplicación en RSpec: ganchos, métodos
auxiliares y declaraciones let . ¿Qué camino te gustó más para este ejemplo? ¿Por qué? ¿Puedes
pensar en situaciones en las que los demás podrían ser una mejor opción?
2. Ejecute rspec help y observe las opciones disponibles. Intente usar algunos de ellos para ejecutar
sus ejemplos de sándwich.
¿Listo para ver algunas de nuestras formas favoritas de usar RSpec? Tómese un breve descanso
para un sándwich y luego encuéntrenos en el próximo capítulo.
informar fe de erratas • discutir
Machine Translated by Google
En este capítulo, verá:
• Cómo generar documentación legible a partir de sus especificaciones •
Cómo identificar los ejemplos más lentos en una suite • Cómo
ejecutar solo las especificaciones que le interesan en cualquier momento dado
momento
• Cómo marcar el trabajo en curso y volver a él más tarde
CAPÍTULO 2
Desde escribir especificaciones hasta ejecutarlas
Ha instalado RSpec y lo ha probado. Ha escrito algunas especificaciones y se ha dado cuenta de cómo son diferentes
de los casos de prueba en los marcos tradicionales. También ha visto algunas formas de recortar la repetición de
sus ejemplos.
En el proceso, ha aplicado las siguientes prácticas:
• Estructurar sus ejemplos lógicamente en grupos • Escribir expectativas
claras que se prueben con el nivel de detalle adecuado • Compartir código de configuración
común en todas las especificaciones
RSpec está diseñado en torno a estos hábitos, pero también podría aprender a aplicarlos a otros marcos de prueba.
Quizás se pregunte si todo lo que separa a RSpec de la multitud es la sintaxis.
En este capítulo, le mostraremos que la utilidad de RSpec no se limita a la apariencia de sus especificaciones.
También se aplica a cómo se ejecutan. Aprenderá las siguientes prácticas que lo ayudarán a encontrar problemas
en el código más rápidamente:
• Vea el resultado de sus especificaciones impresas como documentación, para ayudar a su yo futuro a
comprender la intención del código cuando algo sale mal
• Ejecute un conjunto específico de ejemplos, para centrarse en una porción de su programa en
un momento
• Corrija un error y vuelva a ejecutar solo las especificaciones que fallaron la última vez
• Marque el trabajo en progreso para recordarle que debe terminar algo más tarde
La herramienta que hace que estas actividades sean posibles, e incluso fáciles, es el ejecutor de especificaciones
de RSpec. Decide cuál de sus especificaciones ejecutar y cuándo ejecutarlas. Echemos un vistazo a cómo hacer
que cante.
informar fe de erratas • discutir
Machine Translated by Google
Capítulo 2. Desde escribir especificaciones hasta ejecutarlas • 16
Personalización de la salida de sus especificaciones
Cuando usa RSpec en un proyecto del mundo real, creará un conjunto de docenas, cientos o incluso
miles de ejemplos. La mayoría de los marcos de prueba, incluido RSpec, están optimizados para este
tipo de uso. El formato de salida predeterminado oculta muchos detalles para que pueda mostrar el
progreso de sus especificaciones.
El formateador de progreso En
esta sección, veremos diferentes formas de ver el resultado de sus especificaciones. Cree un nuevo
archivo llamado spec/coffee_spec.rb con el siguiente contenido:
02runningspecs/01/spec/coffee_spec.rb
RSpec.describe 'Una taza de café' do let(:café)
{ Café.nuevo }
'cuesta $ 1 ' hacer
expect(café.precio).to eq(1.00) end
contexto 'con leche' hacer
antes de { café.añadir :leche }
' cuesta $ 1.25' espera
(precio.café). para eq (1.25) fin
fin
fin
Este archivo de especificaciones utiliza las mismas técnicas que vimos en el capítulo anterior, con un
nuevo giro: el bloque de contexto que comienza en la línea resaltada. Este método agrupa un conjunto
de ejemplos y su código de configuración junto con una descripción común, en este caso, "con leche".
Puede anidar estos grupos de ejemplo tan profundamente como desee.
No hay nada misterioso detrás de escena aquí: el contexto es solo un alias para describir. Puede
usarlos indistintamente, pero tendemos a usar el contexto para frases que modifican el objeto que
estamos probando, de la misma manera que "con leche" modifica "una taza de café".
Esta especificación necesitará una clase de café para probar. En un proyecto completo, colocaría su
definición en un archivo separado y usaría require en sus especificaciones. Pero para este ejemplo
simple, está bien poner la clase en la parte superior de su archivo de especificaciones. Este es el
comienzo de una implementación que aún no es suficiente para aprobar las especificaciones:
02runningspecs/01/spec/coffee_spec.rb
class Café def
ingredientes
@ingredients ||= [] fin
informar fe de erratas • discutir
Machine Translated by Google
Personalización de la salida de sus especificaciones • 17
def añadir(ingrediente)
ingredientes << fin de ingrediente
precio de
definición 1.00
fin
fin
Cuando ejecute sus especificaciones, verá un punto por cada ejemplo completado,
con fallas y excepciones indicadas con letras:
$ rspec .F
Fallas:
1) Una taza de café con leche cuesta $1.25
Fallo/Error: expect(coffee.price).to eq(1.25)
esperado: 1.25
obtenido: 1.0
(comparado usando ==)
# ./spec/coffee_spec.rb:26:in ̀bloque (3 niveles) en <superior (requerido)>'
Terminado en 0.01222 segundos (los archivos tardaron 0.08094 segundos en cargarse)
2 ejemplos, 1 falla
Ejemplos fallidos:
rspec ./spec/coffee_spec.rb:25 # Una taza de café con leche cuesta $1.25
Aquí, vemos un punto para el ejemplo de aprobación y una F para el fracaso. Este
formato es bueno para mostrar el progreso de sus especificaciones a medida que se
ejecutan. Cuando tenga cientos de ejemplos, verá una fila de puntos que se desplazan
por la pantalla.
Por otro lado, esta salida no proporciona ninguna indicación de qué ejemplo se está
ejecutando actualmente o cuál es el comportamiento esperado.
Cuando necesite más detalles en su informe de prueba, o necesite un formato
específico como HTML, RSpec lo tiene cubierto. Al elegir un formateador diferente,
puede adaptar la salida a sus necesidades.
Un formateador recibe eventos de RSpec, como cuando falla una prueba, y luego
informa los resultados. Debajo del capó, es solo un simple objeto Ruby. Puede crear
uno propio fácilmente y en Cómo funcionan los formateadores, en la página 153 , verá
cómo hacerlo. Los formateadores pueden escribir datos en cualquier formato y enviar
la salida a cualquier lugar (como a la consola, un archivo oa través de una red).
Echemos un vistazo a otro de los formateadores que se envía con RSpec.
informar fe de erratas • discutir
Machine Translated by Google
Capítulo 2. Desde escribir especificaciones hasta ejecutarlas • 18
El formateador de documentación
El formateador de documentación incorporado de RSpec enumera la salida de las
especificaciones en un formato de esquema, usando sangría para mostrar la agrupación. Si ha
escrito descripciones de ejemplo con una salida legible en mente, el resultado se leerá casi
como la documentación del proyecto. Hagamos un intento.
Para ver el resultado en formato de documentación, pase format documentation (o simplemente
f d) a rspec:
$ rspec documentación de formato
Una taza de café
cuesta $1
con leche
cuesta $1.25 (FALLIDO 1)
Fallas:
1) Una taza de café con leche cuesta $1.25
Fallo/Error: expect(coffee.price).to eq(1.25)
esperado: 1.25
obtenido: 1.0
(comparado usando ==) # ./
spec/coffee_spec.rb:26:in ̀bloque (3 niveles) en <superior (requerido)>'
Terminado en 0.01073 segundos (los archivos tardaron 0.08736 segundos en cargarse) 2
ejemplos, 1 falla
Ejemplos fallidos:
rspec ./spec/coffee_spec.rb:25 # Una taza de café con leche cuesta $1.25
El informe de prueba es una lista de las especificaciones de varias tazas de café que RSpec
verificó. Hay mucha información aquí, y RSpec usa espaciado y mayúsculas para mostrarle lo
que está pasando:
• Un grupo de ejemplo enumera todos sus ejemplos con sangría debajo de él.
• Los contextos crean anidamiento adicional, de la misma manera que se sangra el ejemplo con leche
más.
• Cualquier ejemplo fallido muestra el texto FAILED con un número de nota al pie para
buscando los detalles más adelante.
Después de la documentación en la parte superior del informe, la sección Fallas muestra los
siguientes detalles para cada falla:
• La expectativa que falló • Qué
resultado esperaba versus lo que realmente sucedió • El archivo y el
número de línea de la expectativa fallida
informar fe de erratas • discutir
Machine Translated by Google
Personalización de la salida de sus especificaciones • 19
Esta salida está diseñada para ayudarlo a encontrar de un vistazo qué salió mal y cómo.
Como veremos a continuación, RSpec puede proporcionar más pistas a través del resaltado de sintaxis.
Resaltado de sintaxis
Hemos visto cómo el resaltado de color de RSpec hace que sea mucho más fácil escanear la salida en
busca de especificaciones aprobadas y fallidas. Podemos ir un paso más allá instalando un resaltador de
código llamado CodeRay:1
$ gem install coderay v 1.1.1 Instalado con
éxito coderay1.1.1 1 gema instalada
Cuando se instala esta gema, los fragmentos de Ruby en la salida de sus especificaciones estarán
codificados por colores como lo estarían en su editor de texto. Por ejemplo:
$ rspecfd
Una taza de café
cuesta $1
con leche
cuesta $1.25 (FALLIDO 1)
Fallas:
1) Una taza de café con leche cuesta $1.25
Fallo/Error: expect(coffee.price).to eq(1.25)
esperado: 1.25
obtenido: 1.0
(comparado usando ==) # ./
spec/coffee_spec.rb:26:in ̀bloque (3 niveles) en <superior (requerido)>'
Terminado en 0.0102 segundos (los archivos tardaron 0.09104 segundos en cargarse) 2
ejemplos, 1 falla
Ejemplos fallidos:
rspec ./spec/coffee_spec.rb:25 # Una taza de café con leche cuesta $1.25
Ahora, la línea expect(coffee.price).to eq(1.25) tiene resaltado de sintaxis de Ruby. Las llamadas a
métodos normales como café y precio no están sombreadas, pero otros elementos sí lo están. En
particular, tanto el método esperado RSpec clave como el número 1.25 están resaltados en color. Este
resaltado de sintaxis es aún más útil para expresiones complejas de Ruby.
RSpec usará automáticamente CodeRay si está disponible. Para proyectos basados en Bundler, colóquelo
en su Gemfile y vuelva a ejecutar la instalación del paquete. Para proyectos que no sean de Bundler,
instálelo a través de gem install como lo hemos hecho aquí.
1. https://github.com/rubychan/coderay
informar fe de erratas • discutir
Machine Translated by Google
Capítulo 2. Desde escribir especificaciones hasta ejecutarlas • 20
Identificar ejemplos lentos
A lo largo de este libro, le daremos consejos sobre cómo mantener sus especificaciones funcionando
rápidamente. Para comprender dónde se encuentran los mayores cuellos de botella en su suite, debe poder
identificar los ejemplos más lentos.
El corredor de especificaciones de RSpec puede ayudarlo a hacerlo. Considere el siguiente grupo de
ejemplos que tardan demasiado en ejecutarse:
02runningspecs/03/spec/
slow_spec.rb RSpec.describe 'El método sleep()' do
it('puede dormir durante 0,1 segundos') { dormir 0,1 }
it('puede dormir durante 0,2 segundos') { dormir 0,2 }
it('puede dormir durante 0,3 segundos') { dormir 0,3 }
it('puede dormir durante 0,4 segundos ') { dormir 0.4 }
it('puede dormir por 0.5 segundos') { dormir 0.5 } end
Podemos pedirle a RSpec que enumere las principales pérdidas de tiempo pasando la opción profile junto
con la cantidad de infractores que nos gustaría ver:
$ rspec perfil 2
.....
Los 2 ejemplos más lentos (0,90618 segundos, 59,9 % del tiempo total):
El método sleep() puede dormir durante 0,5 segundos
0,50118 segundos ./spec/slow_spec.rb:6 El
método sleep() puede dormir durante 0,4 segundos
0,40501 segundos ./spec/slow_spec.rb:5
Terminado en 1,51 segundos (los archivos tardaron 0,08911 segundos en
cargarse) 5 ejemplos, 0 fallas
Solo dos ejemplos están tomando más de la mitad de nuestro tiempo de prueba. ¡Mejor ponte a optimizar!
Ejecutando justo lo que necesita
En los ejemplos de este capítulo, siempre hemos ejecutado todas las especificaciones juntas. En un
proyecto real, no necesariamente desea cargar todo su conjunto de pruebas cada vez que invoca RSpec.
Si está diagnosticando una falla específica, por ejemplo, querrá ejecutar solo ese ejemplo. Si está tratando
de obtener comentarios rápidos sobre su diseño, puede omitir las especificaciones lentas o no relacionadas.
La forma más sencilla de reducir la ejecución de la prueba es pasar una lista de nombres de archivos o
directorios a rspec:
$ rspec spec/unit # Carga *_spec.rb en este directorio y subdirectorios $ rspec spec/unit/
specific_spec.rb # Carga solo un archivo de especificaciones
informar fe de erratas • discutir
Machine Translated by Google
Ejecutando justo lo que necesita • 21
$ rspec spec/unit spec/smoke $ rspec # Cargar más de un directorio
spec/unit spec/foo_spec.rb # O mezclar y combinar archivos y directorios
No solo puede cargar archivos o directorios específicos, sino que también puede filtrar cuál de los
ejemplos cargados RSpec realmente ejecutará. Aquí, exploraremos algunas formas diferentes de
ejecutar ejemplos específicos.
Ejecución de ejemplos por nombre
En lugar de ejecutar todas las especificaciones cargadas, puede elegir un ejemplo específico por
nombre, usando la opción example o e más un término de búsqueda:
$ rspec e leche fd
Opciones de ejecución: incluir {:full_description=>/milk/}
Una taza de café con
leche
cuesta $1.25 (FALLIDO 1)
Fallas:
1) Una taza de café con leche cuesta $1.25
Fallo/Error: expect(coffee.price).to eq(1.25)
esperado: 1.25
obtenido: 1.0
(comparado usando ==) # ./
spec/coffee_spec.rb:26:in ̀bloque (3 niveles) en <superior (requerido)>'
Finalizó en 0,01014 segundos (los archivos tardaron 0,08249 segundos en cargarse) 1
ejemplo, 1 error
Ejemplos fallidos:
rspec ./spec/coffee_spec.rb:25 # Una taza de café con leche cuesta $1.25
RSpec ejecutó solo los ejemplos que contenían la palabra leche (en este caso, solo un ejemplo).
Cuando usa esta opción, RSpec busca la descripción completa de cada ejemplo; por ejemplo, Una
taza de café con leche cuesta $1.25. Estas búsquedas distinguen entre mayúsculas y minúsculas.
Ejecución de fallas específicas
A menudo, lo que realmente desea hacer es ejecutar solo la especificación fallida más reciente.
RSpec nos da un atajo útil aquí. Si pasa un nombre de archivo y un número de línea separados
por dos puntos, RSpec ejecutará el ejemplo que comienza en esa línea.
Ni siquiera tiene que escribir manualmente qué archivo y línea desea volver a ejecutar. Eche un
vistazo al final de la salida de especificaciones:
informar fe de erratas • discutir
Machine Translated by Google
Capítulo 2. Desde escribir especificaciones hasta ejecutarlas • 22
$ rspec .F
« truncado »
2 ejemplos, 1 fracaso
Ejemplos fallidos:
rspec ./spec/coffee_spec.rb:25 # Una taza de café con leche cuesta $1.25
Puede copiar y pegar la primera parte de esa línea final (antes del hash) en su terminal
para ejecutar solo la especificación que falla. Hagámoslo ahora:
$ rspec ./spec/coffee_spec.rb:25 Opciones
de ejecución: incluir {:ubicaciones=>{"./spec/coffee_spec.rb"=>[25]}}
F
« truncado »
1 ejemplo, 1 fracaso
Ejemplos fallidos:
rspec ./spec/coffee_spec.rb:25 # Una taza de café con leche cuesta $1.25
RSpec ejecutó solo el único ejemplo que especificó. Esta capacidad de enfoque se vuelve
aún más poderosa cuando agrega una combinación de teclas a su editor de texto.
Varios IDE y complementos de editor proporcionan este comportamiento, incluidos los
siguientes:
• Complemento rspec.vim de
ThoughtBot2 • Modo RSpec de Peter Williams
para Emacs3 • El paquete RSpec para Sublime
Text4 • Atom RSpec Runner5 de Felipe
Coury • RubyMine IDE de JetBrains6
Con un buen soporte del editor, puede ejecutar rápidamente el ejemplo debajo del cursor
con solo presionar una tecla.
Use la integración del editor para una experiencia
más productiva Tener que alternar entre su editor y una ventana de terminal
para ejecutar rspec realmente interrumpe su flujo de trabajo. Recomendamos
tomarse el tiempo para instalar un complemento de editor para que
ejecutar rspec esté a solo una pulsación de tecla.
2. https://github.com/thoughtbot/vimrspec
3. https://www.emacswiki.org/emacs/RspecMode
4. https://github.com/SublimeText/RSpec
5. https://github .com/fcoury/atomrspec
6. https://www.jetbrains.com/ruby/
informar fe de erratas • discutir
Machine Translated by Google
Ejecutando justo lo que necesita • 23
Volver a ejecutar todo lo que falló
El uso de un número de línea funciona bien cuando solo falla una especificación. Si tiene más de una
falla, puede ejecutarlas todas con el indicador onlyfailures . Esta bandera requiere un poco de
configuración, pero RSpec lo guiará a través del proceso de configuración:
$ rspec solo fallas
Para usar ̀onlyfailures`, primero debe configurar
`config.example_status_persistence_file_path`.
RSpec necesita un lugar para almacenar información sobre qué ejemplos están fallando para que sepa
qué volver a ejecutar. Usted proporciona un nombre de archivo a través del método RSpec.configure ,
que es comodín para muchas opciones diferentes de tiempo de ejecución.
Agregue las siguientes líneas a su archivo coffee_spec.rb entre la definición de la clase Coffee y las
especificaciones:
02runningspecs/06/spec/coffee_spec.rb
RSpec.configure do |config|
config.example_status_persistence_file_path = 'spec/examples.txt' end
Deberá volver a ejecutar RSpec una vez sin ningún indicador (para registrar el estado de aprobación/
reprobación):
$ rspec .F
« truncado »
2 ejemplos, 1 fracaso
Ejemplos fallidos:
rspec ./spec/coffee_spec.rb:29 # Una taza de café con leche cuesta $1.25
Ahora, puede usar la opción onlyfailures :
$ rspec solo fallas
Opciones de ejecución: incluye {:last_run_status=>"failed"}
F
« truncado »
1 ejemplo, 1 fracaso
Ejemplos fallidos:
rspec ./spec/coffee_spec.rb:29 # Una taza de café con leche cuesta $1.25
Veamos qué sucede cuando se soluciona el comportamiento y se cumplen las especificaciones. Intente
modificar la clase Coffee para aprobar ambos ejemplos. Aquí hay una posible implementación:
informar fe de erratas • discutir
Machine Translated by Google
Capítulo 2. Desde escribir especificaciones hasta ejecutarlas • 24
02runningspecs/06/spec/coffee_spec.rb
clase Café
def ingredientes
@ingredientes ||= [] fin
def añadir(ingrediente)
ingredientes << fin de ingrediente
precio def
1,00 + ingredientes.tamaño * 0,25 final
final
Con su implementación en su lugar, vuelva a ejecutar RSpec con la opción onlyfailures :
$ rspec solo fallas
Opciones de ejecución: incluye {:last_run_status=>"failed"}
.
Terminado en 0.00094 segundos (los archivos tardaron 0.09055 segundos en cargarse)
1 ejemplo, 0 fallas
RSpec vuelve a ejecutar el ejemplo anterior fallido y verifica que pasa. Si intentamos este proceso una vez
más, RSpec no tendrá ningún ejemplo fallido para ejecutar:
$ rspec solo fallas
Opciones de ejecución: incluye {:last_run_status=>"failed"}
Todos los ejemplos fueron filtrados
Terminado en 0.00031 segundos (los archivos tardaron 0.08117 segundos en cargarse)
0 ejemplos, 0 fallas
Otra opción de la línea de comandos, nextfailure, ofrece un giro a esta idea. Tendrá la oportunidad de
probarlo en el ejercicio al final de este capítulo.
Pasar opciones al comando rspec no es la única forma de ejecutar solo un subconjunto de sus ejemplos.
A veces, es más conveniente hacer anotaciones temporales en sus especificaciones.
Enfocar ejemplos específicos Si se
encuentra ejecutando el mismo subconjunto de especificaciones repetidamente, puede ahorrar tiempo al
marcarlas como enfocadas. Para hacerlo, simplemente agregue una f al comienzo del nombre del método
RSpec:
• el contexto se convierte en fcontext
• se pone en forma
• describir se convierte en fdescribe
informar fe de erratas • discutir
Machine Translated by Google
Ejecutando justo lo que necesita • 25
Veamos cómo se ve eso con el ejemplo “Una taza de café con leche cuesta $1.25”. En
coffee_spec.rb, reemplace context con fcontext (piense en ello como una forma abreviada de
contexto enfocado):
02runningspecs/07/spec/coffee_spec.rb
fcontext 'con leche' hacer
A continuación, debemos configurar RSpec para ejecutar solo los ejemplos enfocados. Edite
el bloque RSpec.configure en este archivo y agregue la siguiente línea resaltada:
02runningspecs/07/spec/coffee_spec.rb
RSpec.configure do |config|
config.filter_run_when_matching(focus: true)
config.example_status_persistence_file_path = 'spec/examples.txt' end
Ahora, cuando ejecuta solo rspec, solo ejecutará el ejemplo en el contexto enfocado:
$ especificación
Opciones de ejecución: incluye {:focus=>true}
.
Terminado en 0.00093 segundos (los archivos tardaron 0.07915 segundos en cargarse)
1 ejemplo, 0 fallas
Si no ha marcado ninguna especificación como enfocada, RSpec las ejecutará todas.
Nos gustaría mostrarle un aspecto más de las especificaciones enfocadas. Eche un vistazo a
la primera línea de la salida:
Opciones de ejecución: incluye {:focus=>true}
Vimos algo similar en la sección sobre ejecutar solo ejemplos fallidos:
Opciones de ejecución: incluye {:last_run_status=>"failed"}
Aunque estas dos formas de cortar y trocear sus especificaciones se sienten muy diferentes,
ambas se basan en la misma abstracción simple.
Filtrado de etiquetas
Anteriormente, cuando escribió la siguiente línea para centrarse en el contexto con leche :
02runningspecs/07/spec/coffee_spec.rb
fcontext 'con leche' hacer
… este código en realidad era solo una abreviatura de la siguiente expresión:
02runningspecs/08/spec/coffee_spec.rb
contexto 'con leche', enfoque: verdadero hacer
informar fe de erratas • discutir
Machine Translated by Google
Capítulo 2. Desde escribir especificaciones hasta ejecutarlas • 26
Cada vez que defina un ejemplo o grupo, es decir, cada vez que use RSpec.describe,
context o it, puede agregar un hash como la etiqueta focus: true que ve aquí. Este hash,
conocido como metadatos, puede contener claves y valores arbitrarios.
Detrás de escena, RSpec agregará sus propios metadatos, como last_run_status para
indicar si cada especificación pasó o falló la última vez que se ejecutó.
Puede filtrar ejemplos directamente desde la línea de comando usando la opción tag .
Por ejemplo, si RSpec no tuviera ya una opción de línea de comandos onlyfailures ,
podría haber obtenido el mismo comportamiento así:
$ rspec tag last_run_status:fallido
Opciones de ejecución: incluye {:last_run_status=>"failed"}
F
« truncado »
1 ejemplo, 1 fracaso
Ejemplos fallidos:
rspec ./spec/coffee_spec.rb:29 # Una taza de café con leche cuesta $1.25
De manera similar, podría pasar tag focus para ejecutar solo las especificaciones enfocadas, pero en su lugar,
configuramos RSpec para que lo haga de manera predeterminada.
Antes de continuar, no olvide volver a cambiar fcontext a context. La idea de enfocar es
filtrar las especificaciones temporalmente.
Marcar trabajo en curso
En BDD, generalmente trabaja para obtener solo una especificación a la vez para aprobar.
Tratar de abordar demasiadas funciones a la vez conduce a los tipos de diseños
complicados e imposibles de probar que BDD busca evitar.
Por otro lado, puede ser extremadamente productivo esbozar varios ejemplos en un lote.
Está pensando en todas las cosas que debe hacer un componente de software y desea
capturar las ideas para no olvidar nada.
RSpec admite este flujo de trabajo muy bien, a través de ejemplos pendientes .
Comenzando con la descripción
Mientras escribía las especificaciones del café anteriormente, es posible que haya estado
pensando en otras propiedades del café con leche: es de color más claro, más fresco, etc.
Si bien estos comportamientos están en su mente, continúe y agréguelos dentro del
contexto con leche como ejemplos vacíos:
02runningspecs/09/spec/coffee_spec.rb
' es de color claro' ' es más
frío que 200 grados Fahrenheit'
informar fe de erratas • discutir
Machine Translated by Google
Marcar Trabajo en Progreso • 27
No hay necesidad de completarlos; simplemente déjelos como están mientras trabaja en otras
especificaciones. Esto es lo que muestra RSpec en la consola cuando tiene ejemplos vacíos:
$ rspec ..**
Pendiente: (Se esperan las fallas enumeradas aquí y no afectan el estado de su suite )
1) Una taza de café con leche es de color claro # Aún no
implementado # ./spec/
coffee_spec.rb:34
2) Una taza de café con leche está a menos de 200 grados Fahrenheit
# Aún no implementado # ./
spec/coffee_spec.rb:35
Terminado en 0.00125 segundos (los archivos tardaron 0.07577 segundos en cargarse)
4 ejemplos, 0 fallas, 2 pendientes
Los dos ejemplos vacíos están marcados con asteriscos amarillos en la barra de progreso y
aparecen como "Pendiente'' en la salida.
Marcar trabajo incompleto
Cuando está esbozando el trabajo futuro, es posible que tenga una idea de cómo quiere que se
vea el cuerpo de la especificación. Sería bueno poder marcar algunas expectativas como trabajo
en progreso antes de comprometerse para que nunca confirme un conjunto de especificaciones
fallido.
RSpec proporciona el método pendiente para este propósito. Puede marcar una especificación
como pendiente agregando la palabra pendiente en cualquier lugar dentro del cuerpo de la
especificación, junto con una explicación de por qué la prueba aún no debería pasar. La ubicación
importa; se esperará que pasen todas las líneas antes de la llamada pendiente . Por lo general, lo
agregamos en la parte superior del ejemplo:
02runningspecs/10/spec/coffee_spec.rb
' es de color claro' hacer pendiente
'Color aún no implementado' esperar
(café.color).ser (:claro) fin
' es más frío que 200 grados Fahrenheit' hacer pendiente 'La
temperatura aún no se implementó' esperar
(café.temperatura) que sea < 200.0 fin
RSpec ejecutará el cuerpo de la especificación e imprimirá el error para que pueda verlo.
Pero no marcará la especificación, o su suite en general, como fallida:
informar fe de erratas • discutir
Machine Translated by Google
Capítulo 2. Desde escribir especificaciones hasta ejecutarlas • 28
$ rspec ..**
Pendiente: (Se esperan las fallas enumeradas aquí y no afectan el estado de su suite )
1) Una taza de café con leche es de color claro # Color aún no
implementado Falla/Error:
esperar(café.color) .ser(:claro)
Sin error de método:
método indefinido ̀color' para #<Café:0x007f83b1199a88
@ingredients=[:leche]> # ./
spec/coffee_spec.rb:36:in ̀bloque (3 niveles) en <superior (obligatorio)>'
2) Una taza de café con leche está a menos de 200 grados Fahrenheit
# Temperatura aún no implementada Falla/
Error: se espera que (temperatura del café) sea < 200.0
Sin error de método:
método indefinido 'temperatura' para #<Café:0x007f83b11984d0
@ingredients=[:leche]> # ./
spec/coffee_spec.rb:41:en 'bloque (3 niveles) en <superior (obligatorio)>'
Terminado en 0.00161 segundos (los archivos tardaron 0.07898 segundos en cargarse)
4 ejemplos, 0 fallas, 2 pendientes
Por supuesto, podría simplemente comentar los ejemplos en lugar de marcarlos como
pendientes. Pero a diferencia del código comentado, los ejemplos pendientes aún se
ejecutan e informan sus fallas, lo que significa que puede usar esta información para
impulsar su implementación.
Defina un método de inspección para una salida de prueba más clara
Algunos mensajes de error, como las excepciones NoMethodError
impresas para estas especificaciones de café pendientes, incluyen
representaciones de cadena de sus objetos. Ruby (y RSpec) generan esta
cadena llamando a inspeccionar en cada objeto.
Si una clase en particular no define un método de inspección , la cadena
resultante será algo como #Coffee:0x007f83b11984d0 @ingredi
ents=[:milk] . Para que la salida sea un poco más amigable para el
programador, recomendamos definir este método para imprimir una cadena
agradable y legible como #<Café (con leche)>.
Completar el trabajo en progreso Una
de las cosas buenas de marcar ejemplos como pendientes es que RSpec le avisará cuando
empiecen a pasar.
informar fe de erratas • discutir
Machine Translated by Google
Marcar Trabajo en Progreso • 29
Veamos cómo se ve eso. Implemente los métodos de color y temperatura que faltan dentro de la clase
Café :
02runningspecs/11/spec/coffee_spec.rb color
predeterminado
ingredientes.incluyen?(:leche) ? :claro : : fin oscuro
temperatura definida
ingredientes.incluyen?(:leche) ? 190.0 : 205.0 fin
Ahora, intente volver a ejecutar sus especificaciones:
$
rspec ..FF
Fallas:
1) Una taza de café con leche es de color claro CORREGIDO Se
esperaba que fallara el "Color aún no implementado" pendiente. No se generó ningún error.
# ./spec/café_spec.rb:42
2) Una taza de café con leche está a menos de 200 grados Fahrenheit SOLUCIONADO
Se esperaba que fallara la "Temperatura aún no implementada" pendiente. No se generó ningún
error .
# ./spec/café_spec.rb:47
Terminado en 0.00293 segundos (los archivos tardaron 0.08214 segundos en cargarse)
4 ejemplos, 2 fallas
Ejemplos fallidos:
rspec ./spec/coffee_spec.rb:42 # Una taza de café con leche es de color claro rspec ./spec/
coffee_spec.rb:47 # Una taza de café con leche es más fría que 200 grados Fahrenheit
RSpec ha marcado el conjunto de pruebas como fallido, porque tenemos ejemplos marcados como
pendientes que en realidad se implementaron ahora. Cuando elimine los bits pendientes , todo el
conjunto pasará.
Si realmente no desea que se ejecute el cuerpo de la especificación, puede usar omitir en lugar de
pendiente. O puede usar xit, que es una anotación temporal como fit , excepto que omite el ejemplo en
lugar de enfocarlo.
Usar pendiente para marcar errores en código de terceros
Si su especificación falla debido a un error en una dependencia, márquelo como
pendiente y el ID del ticket de su rastreador de errores; por ejemplo, pendiente
'esperando una solución para el error #42 de la Guía del autoestopista'. Cuando
actualice posteriormente a una versión que contenga la solución, RSpec se lo informará.
informar fe de erratas • discutir
Machine Translated by Google
Capítulo 2. Desde escribir especificaciones hasta ejecutarlas • 30
Tu turno
En este capítulo, practicó varios buenos hábitos de prueba relacionados con la ejecución de sus
especificaciones. El soporte de RSpec para estos hábitos lo distingue de otros marcos de prueba:
• Los potentes formateadores muestran el resultado de sus especificaciones de diversas formas.
• Filtrar sus ejemplos le permite concentrarse en un problema específico y ejecutar solo
las especificaciones que necesitas.
• El método pendiente lo ayuda a esbozar ejemplos antes de implementar
completamente el comportamiento.
Ahora, vas a experimentar un poco más con estas técnicas.
Ejercicio
En un nuevo directorio, cree un archivo llamado spec/tea_spec.rb con el siguiente contenido:
02runningspecs/exercises/spec/tea_spec.rb
clase Té
fin
RSpec.configure do |config|
config.example_status_persistence_file_path = 'spec/examples.txt' end
RSpec.describe Tea do
let(:té) { Té.nuevo }
' sabe como Earl Grey' espera
(tea.flavor) que sea :earl_grey end
hace 'calor' hacer
esperar (té.temperatura) para ser> 200.0 fin
fin
Ejecute bare rspec una vez para que pueda registrar el estado de los ejemplos; luego ejecute RSpec con el
indicador nextfailure y observe el resultado. ¿En qué se diferencia de la técnica onlyfailures que
analizamos en Cómo volver a ejecutar todo lo que falló, en la página 23?
Implemente el método de sabor de la clase Tea para que pase el primer ejemplo. Ahora, ejecute RSpec
nuevamente con el mismo indicador nextfailure . ¿Que ves?
informar fe de erratas • discutir
Machine Translated by Google
Tu turno • 31
Antes de terminar la implementación, pruebe los diferentes formateadores. Ejecute
rspec help para ver una lista de formatos de salida integrados. Pruebe los que no
hemos cubierto en este capítulo. Cuando llegue a HTML, abra la página en su
navegador y vea cómo RSpec representa las especificaciones aprobadas y fallidas.
Con la ayuda del indicador nextfailure , implemente el resto de la clase Tea . Ahora
sírvete una taza de tu bebida caliente favorita. ¡Te lo has ganado!
informar fe de erratas • discutir
Machine Translated by Google
En este capítulo, verá:
• Cómo sus especificaciones pueden brindarle confianza en su código •
Cómo un buen conjunto de pruebas hace posible la refactorización
• Cómo guiar su diseño utilizando sus especificaciones con el desarrollo
impulsado por el comportamiento (BDD)
CAPÍTULO 3
El estilo RSpec
En los últimos dos capítulos, ha llegado a conocer RSpec. Ha escrito sus primeros ejemplos y los ha
organizado en grupos. Ha visto cómo ejecutar solo un subconjunto filtrado de sus especificaciones y cómo
personalizar la salida.
Todas estas características de RSpec están diseñadas para facilitar ciertos hábitos:
• Escribir ejemplos que expliquen claramente el comportamiento esperado del código. • Separar el código
de configuración común de la lógica de prueba real.
Ninguno de estos hábitos tiene un costo:
• Escribir especificaciones lleva tiempo.
• Ejecutar suites grandes lleva tiempo (o requiere opciones de aprendizaje para reducirlas)
el conjunto).
• La lectura de especificaciones muy factorizadas requiere saltar entre la configuración y
código de prueba
No queremos que nadie dé por sentado que los hábitos que estamos formando son buenos. Nos gustaría
mostrarle que valen el costo. En este capítulo, lo guiaremos a través del enfoque de RSpec para el desarrollo
de software y lo que hace un buen conjunto de especificaciones.
Lo que sus especificaciones están haciendo por usted
Escribir especificaciones no es el objetivo de usar RSpec, son los beneficios que brindan esas especificaciones.
Hablemos ahora de esos beneficios; no todos son tan obvios como "las especificaciones detectan errores".
informar fe de erratas • discutir
Machine Translated by Google
Capítulo 3. El estilo RSpec • 34
Creando Confianza
Las especificaciones aumentan la confianza en su proyecto. No estamos hablando de la visión simplista de que
si sus especificaciones pasan, su programa está libre de errores (hemos estado programando en el mundo real
durante demasiado tiempo para creer ese cuento de hadas).
Cuando decimos confianza, queremos decir que una especificación bien escrita puede proporcionar evidencia
a favor de ciertas afirmaciones sobre su código. Por ejemplo:
• El "camino feliz" a través de un bit particular de código se comporta de la manera que usted
quiero que lo haga
• Un método detecta y reacciona a una condición de error que está anticipando.
• Esa última característica que agregó no rompió las existentes.
• Está logrando un progreso medible a través del proyecto.
Ninguna de estas declaraciones son absolutas. No vas a demostrar que son ciertos escribiendo suficientes
especificaciones. Pero una suite bien elaborada puede brindarle la confianza suficiente para saber cuándo
puede pasar a la siguiente pieza en la que está trabajando.
eliminando el miedo
Piense en un proyecto anterior en el que haya trabajado y en el que fue realmente aterrador trabajar en él. ¿Qué
lo hizo tan aterrador?
Estas son algunas de las cosas con las que nos hemos encontrado:
• Un cambio simple e inocuo rompería partes distantes del código que
parecía no tener relación.
• Los desarrolladores se sintieron paralizados por el miedo, incapaces de realizar cambios de forma segura en
el código en absoluto.
Trabajar en un proyecto con un buen conjunto de especificaciones es un cambio refrescante de esta situación.
Con una amplia cobertura de prueba, los desarrolladores descubren temprano si el nuevo código está rompiendo
las funciones existentes. Cuando sus especificaciones lo respaldan, puede refactorizar sin miedo fragmentos de
código complicados, asegurándose de que su base de código no se estanque.
Hablando de refactorización….
Habilitación de la refactorización
La verdad rara vez es permanente en los proyectos de software. Su comprensión del dominio del problema
mejorará a medida que descubra nuevos hechos. Su inicio puede pivotar. Las viejas suposiciones incrustadas
en el código pueden dificultar la implementación de nuevas características.
informar fe de erratas • discutir
Machine Translated by Google
Lo que sus especificaciones están haciendo por usted • 35
Para lidiar con este tipo de cambios, deberá refactorizar el código. Sin un buen conjunto de
especificaciones, la refactorización es una tarea abrumadora. Es imposible predecir cuánto código
necesitará volver a trabajar para cualquier cambio dado.
Nuestro desafío como desarrolladores es estructurar nuestros proyectos para que los grandes
cambios sean fáciles y predecibles. Como dice Kent Beck, “para cada cambio deseado, haga el
1
cambio fácil (advertencia: esto puede ser difícil), luego haga el cambio fácil”.
Sus especificaciones lo ayudarán a lograr este objetivo. Proporcionan una red de seguridad y
protegen contra las regresiones. También señalan lugares donde el código está demasiado acoplado,
donde deberá trabajar más duro para "facilitar el cambio".
Diseño guía
Si escribe sus especificaciones antes de su implementación, será su primer cliente. Obtendrá una
idea de cómo es usar las interfaces que está creando.
Un buen conjunto de ejemplos guiará el diseño inicial y respaldará la refactorización a medida que
evolucione su diseño.
Por contradictorio que parezca, uno de los propósitos de escribir especificaciones es causar dolor, o
más bien, hacer que el código mal diseñado sea doloroso. Al sacar a la superficie el dolor de un
problema de diseño temprano, las especificaciones le permiten solucionarlo mientras es barato y
fácil hacerlo.
Si te encuentras luchando con un montón de objetos desgarbados en su lugar solo para probar un
solo método, ¿cuáles son las probabilidades de que tu equipo pueda usar ese método correctamente
cada vez?
Sostenibilidad
Cuando maneja su código con RSpec, puede llevar un poco más de tiempo construir su primera
característica. Sin embargo, con cada nueva función que agregue, obtendrá una productividad
constante.
Sin RSpec o una herramienta similar en su arsenal, las funciones posteriores tienden a tardar mucho
más que las primeras. Estás luchando constantemente con el código existente para asegurarte de
que siga funcionando.
Este beneficio no se aplica a todos los proyectos. Si está escribiendo un proyecto descartable, o una
aplicación con un conjunto de funciones pequeño y congelado, probar exhaustivamente en cada
capa puede ser excesivo.
1. https://twitter.com/kentbeck/status/250733358307500032
informar fe de erratas • discutir
Machine Translated by Google
Capítulo 3. El estilo RSpec • 36
Documentando el Comportamiento
Las especificaciones bien escritas documentan cómo debe comportarse su sistema. A
diferencia de un archivo de documentación estático, sus especificaciones son ejecutables.
Descubrirá cuándo se han vuelto obsoletos, porque comenzará a ver fallas en la salida. Eso
significa que son mucho más fáciles de mantener actualizados que otras formas de documentación.
RSpec lo alienta a escribir ejemplos que hagan una excelente documentación. Su API
favorece las descripciones sencillas del comportamiento, como en Un sándwich ideal es delicioso.
Su rica biblioteca de expectativas lo ayuda a aclarar qué se supone que debe hacer el código
que está probando. Sus formateadores de salida pueden incluso organizar la salida de sus
especificaciones en un documento HTML coherente.
Transformando su flujo de trabajo
Considere un mundo sin marcos de prueba o BDD. Cada característica que escribiste sería
una apuesta. Encendías la aplicación y hurgabas con la esperanza de descubrir cualquier
problema obvio.
Conducir su diseño a partir de sus especificaciones transforma completamente su flujo de
trabajo. Ahora, cada ejecución de su suite es un experimento que ha diseñado para validar (o
refutar) una hipótesis sobre cómo se comporta el código. Obtiene comentarios rápidos y
frecuentes cuando algo no funciona, y puede cambiar de rumbo de inmediato.
¡Es divertido!
Finalmente, ¡dirigir su diseño a partir de sus especificaciones es divertido! Abordar un gran
problema de una vez es difícil y tiende a darnos un mal caso de "bloqueo del programador".
TDD nos alienta a dividir las cosas en pasos pequeños y alcanzables.
Es casi como un juego: primero, presentamos un ejemplo que expone un comportamiento que
el código aún no implementa. Luego, implementamos el comportamiento para que las
especificaciones pasen. Es un flujo constante de satisfacción.
Comparación de costos y beneficios
Esperamos que esté convencido de que BDD y RSpec pueden ayudarlo a crear buenos
diseños y construirlos rápidamente. Es importante reconocer los costos de lo que hacemos,
ya sea que estemos hablando de las minucias de un solo ejemplo o de un enfoque completo
para el desarrollo de software.
Escribir especificaciones
Cada especificación toma tiempo para escribir. Es por eso que muchos de los hábitos que ha estado practicando,
por ejemplo, esbozar varios ejemplos a la vez o crear código de ayuda reutilizable, giran en torno a ahorrar tiempo.
informar fe de erratas • discutir
Machine Translated by Google
Comparación de costos y beneficios • 37
Ejecución de toda la suite
Durante la vida útil de un proyecto BDD, sus especificaciones se ejecutarán con frecuencia,
quizás incluso miles o decenas de miles de veces. El tiempo que lleva ejecutar el conjunto de
especificaciones se multiplicará por esa gran cantidad.
Considere la diferencia entre un conjunto de pruebas que tarda 12 segundos y uno que tarda
10 minutos. Después de 1.000 carreras, el primero ha tardado 3 horas y 20 minutos.
Este último ha tardado acumulativamente casi 7 días. La velocidad del equipo cae en picada
cuando tienen que esperar una eternidad para saber si sus cambios rompieron algo.
A lo largo de este libro, le mostraremos prácticas para mantener sus especificaciones ágiles.
Obtener comentarios de un solo ejemplo
Hay una gran diferencia entre esperar menos de un segundo para que se ejecute un ejemplo
y esperar varios segundos o incluso varios minutos. No es solo un cambio cuantitativo. Una
vez que haya visto las especificaciones que le brindan una respuesta casi instantánea
mientras escribe, cualquier cosa más lenta se sentirá como una interrupción insoportable en
su línea de pensamiento.
Gary Bernhardt hace un uso efectivo de este estilo de desarrollo de retroalimentación rápida,
que demuestra en sus screencasts y publicaciones de blog de Destroy All Software :
Estas pruebas son lo suficientemente rápidas como para presionar enter (mi pulsación de tecla de ejecución
de prueba) y obtener una respuesta antes de tener tiempo para pensar. Significa que el flujo de mis
pensamientos nunca se rompe.2
Lidiar con el fracaso El
fracaso es algo bueno, de verdad que lo es. Una especificación fallida apunta al
comportamiento que rompió su cambio reciente. Sin embargo, cuesta tiempo y energía
rastrear la fuente de la falla.
Mucho peores son las fallas causadas por especificaciones frágiles , es decir, especificaciones
que fallan (quizás de forma intermitente) cuando el código está funcionando. Al escribir
expectativas RSpec precisas que describen exactamente el comportamiento que está
buscando, y nada más, evita que sus especificaciones se vuelvan quebradizas.
¡No te excedas!
Históricamente, los desarrolladores se han quejado de que sus proyectos carecían de
suficientes conjuntos de pruebas. Ahora que TDD se ha generalizado, estamos siendo
testigos del problema opuesto: proyectos que sufren de pruebas excesivas.
2. https://www.destroyallsoftware.com/blog/2014/tddstrawmenandrhetoric
informar fe de erratas • discutir
Machine Translated by Google
Capítulo 3. El estilo RSpec • 38
En proyectos sobre probados, incluso el cambio más simple lleva demasiado tiempo en completarse.
Las pruebas aparentemente no relacionadas comienzan a fallar o el conjunto de pruebas tarda demasiado en
ejecutarse para que los desarrolladores sean productivos.
No todas las pruebas valen el esfuerzo que implica escribirlas y mantenerlas. De hecho, algunas pruebas tienen
valor negativo. La respuesta de desbordamiento de pila de Kent Beck sobre este tema es instructiva aquí:
Me pagan por el código que funciona, no por las pruebas, por lo que mi filosofía es probar lo menos
3
posible para alcanzar un nivel de confianza determinado….
Recomendamos hacer un balance periódico de los proyectos de larga duración. ¿Los errores lógicos básicos
están entrando en producción? Considere reforzar esas áreas de su conjunto de pruebas. ¿Se interponen
errores falsos en el camino de los cambios de código? Intente modificar sus especificaciones para que sean
menos frágiles, o incluso elimine algunas de ellas.
A medida que cambia su arquitectura, las pruebas pueden volverse superfluas. Eliminar una prueba no significa
que el esfuerzo de escribirla se haya desperdiciado o malgastado. Simplemente significa que se ha quedado
más tiempo de lo esperado. Una prueba que no puede pagarse por sí misma ya no debería estar en su suite.
Decidir qué no probar
Cada comportamiento que especifica en una prueba es otro punto de acoplamiento entre sus pruebas y el
código de su proyecto. Eso significa que tendrá una cosa más que tendrá que arreglar si alguna vez necesita
cambiar el comportamiento de su implementación.
A veces, es mejor decidir intencionalmente no probar ciertas cosas. Por ejemplo, las interfaces de usuario
pueden cambiar rápidamente. Si acopla estrechamente sus pruebas automatizadas a los detalles incidentales
de su interfaz de usuario, aumenta el costo del cambio.
Puede sacarle más partido a las pruebas exploratorias manuales; consulte Explore It! de Elisabeth Hendrickson.
[Hen13] para obtener más información sobre este tema.
Si necesita controlar una interfaz de usuario a partir de pruebas automatizadas, intente probar en términos de
su dominio problemático ("iniciar sesión como administrador") en lugar de detalles de implementación ("escriba
[email protected] en el tercer campo de texto").
Otro lugar clave para mostrar moderación es el nivel de detalle en sus afirmaciones de prueba.
En lugar de afirmar que un mensaje de error coincide exactamente con una cadena en particular ("No se pudo
encontrar el usuario con ID 123"), considere usar subcadenas para que coincidan solo con las partes clave ("No
se pudo encontrar el usuario"). Asimismo, no especifique el orden exacto de una colección a menos que el
orden sea importante.
3. https://stackoverflow.com/questions/153234/qué tan profundas son sus pruebas unitarias/153565#153565
informar fe de erratas • discutir
Machine Translated by Google
Diferentes tipos de especificaciones • 39
Diferentes tipos de especificaciones
Nuestro objetivo como desarrolladores es escribir especificaciones que maximicen los valores que
hemos enumerado aquí (orientar el diseño, generar confianza, etc.) mientras se minimiza el tiempo
perdido para escribirlas, ejecutarlas y corregirlas.
Cada especificación tiene un trabajo que hacer. Estos trabajos se dividen en diferentes categorías:
capturar regresiones en una aplicación, guiar el diseño de una sola clase o método, etc.
La comunidad de desarrollo de software discute continuamente sobre cuántas de estas categorías
hay y sus definiciones exactas. Si bien es divertido reflexionar sobre estos interminables argumentos
y subcategorías, recomendamos centrarse solo en unos pocos tipos de especificaciones diferentes
y bien definidas. De esa manera, terminará eligiendo intencionalmente qué escribir en un momento
dado, según el beneficio que esté buscando.
Para este libro, vamos a considerar tres tipos de especificaciones descritas por
Steve Freeman y Nat Pryce en Desarrollo de software orientado a objetos, guiado por pruebas
[FP09]:
• Aceptación: ¿Funciona todo el sistema? • Unidad:
¿Nuestros objetos hacen lo correcto, son convenientes para trabajar con ellos? •
Integración: ¿Nuestro código funciona contra el código que no podemos cambiar?
Veamos cada uno de estos tipos de especificaciones uno por uno.
Especificaciones de aceptación
Las especificaciones de aceptación describen una característica en un estilo de caja negra de
extremo a extremo que ejercita todo el sistema. Estas especificaciones son difíciles de escribir,
comparativamente frágiles y lentas. Pero también brindan una gran confianza de que las partes del
sistema están trabajando juntas como un todo. También son sumamente útiles para la refactorización
a gran escala.
Especificaciones de la unidad
En el otro extremo del espectro, las especificaciones de unidades se centran en unidades de código
individuales, a menudo tan pequeñas como un único objeto o método. Comprueban el
comportamiento de un fragmento de código en relación con el entorno que se construye para él.
Las especificaciones de unidades bien escritas tienden a ejecutarse extremadamente rápido (¡a
menudo en unos pocos milisegundos o menos!) y, por lo tanto, tienden a costar menos que otros
tipos de especificaciones. Su naturaleza aislada y enfocada proporciona comentarios de diseño
útiles de inmediato. Su naturaleza independiente hace que sea menos probable que interfieran
entre sí en una suite grande.
informar fe de erratas • discutir
Machine Translated by Google
Capítulo 3. El estilo RSpec • 40
Por otro lado, las especificaciones de la unidad suelen ser de un nivel demasiado bajo para ser de mucha utilidad
durante la refactorización a gran escala. De hecho, es posible que incluso deba desechar algunas especificaciones
de la unidad durante dicha refactorización. Dado que están destinados a ser baratos de escribir y ejecutar, eso no
debería parecer un gran sacrificio.
Especificaciones de
integración Las especificaciones de integración se encuentran en algún lugar entre estos dos
extremos. El código que interactúa con un servicio externo, como una base de datos o una API
REST de terceros, debe tener una especificación de integración.
Hay una línea muy fina para dibujar aquí. Cualquier proyecto de software no trivial dependerá de otras bibliotecas.
Una interpretación estricta de estas definiciones requeriría que separe su unidad y las especificaciones de integración
de la siguiente manera:
• Las especificaciones de su unidad tendrían que aislar su código de cualquier tercero
dependencia.
• Se permitiría que sus especificaciones de integración (comparativamente lentas) accedan indirectamente al
código de terceros.
Recomendamos aplicar aquí el sentido común. Si su clase de Ruby depende de bibliotecas pequeñas, estables y
rápidas que no llegan a la red y no tienen efectos secundarios, probablemente esté bien llamarlas tal como están
según las especificaciones de su unidad.
Las especificaciones de integración suelen ser un orden de magnitud más lentas que las especificaciones de unidad.
En consecuencia, lo más probable es que no los ejecute constantemente, como lo haría con las especificaciones de
la unidad. Sin embargo, recomendamos ejecutar las especificaciones de integración cuando modifique el código que
cubren, o al menos antes de confirmar los cambios.
Además, las especificaciones de integración requieren más cuidado para evitar interferencias. Cuando su código
escribe un registro de la base de datos, su especificación deberá eliminar el registro (o revertir la transacción de la
base de datos) para no afectar los ejemplos posteriores. Este tipo de problemas de dependencia hacen que las
especificaciones de integración sean más difíciles de ejecutar en paralelo (algo que puede ahorrarle mucho tiempo)
que las especificaciones de unidad.
Recomendamos poner la menor lógica de bifurcación posible en las secciones de su código que se ocupan de las
dependencias. Cuanto más simples sean estas partes de su sistema, menos especificaciones de integración costosas
necesitará para verificarlas a fondo.
Pautas
Con estas definiciones en mente, hemos adoptado un conjunto de pautas para los proyectos de este libro. Son una
adaptación de principios que nos han servido bien en situaciones del mundo real. Dicho esto, es posible que no sean
aplicables para cada
informar fe de erratas • discutir
Machine Translated by Google
Directrices • 41
proyecto. Nuestro objetivo es brindarle las herramientas y el contexto para decidir qué es lo mejor para su propia
situación.
Las especificaciones de integración son más difíciles de escribir que las especificaciones de unidad y se ejecutan más lentamente.
Por lo tanto, preferimos escribir menos especificaciones de integración y más especificaciones de unidad.
Para hacer posible este arreglo, necesitamos mantener nuestras interfaces con recursos externos pequeñas y
bien definidas. De esa forma, solo tenemos que escribir algunas especificaciones de integración para estas
interfaces. Nuestras especificaciones de unidades más rápidas pueden sustituir fácilmente versiones falsas de
las interfaces.
Desafortunadamente, el consejo general para favorecer las especificaciones de la unidad no siempre es fácil de
llevar a cabo en el mundo real. Para muchos proyectos, las especificaciones de mayor valor son también las
que más cuestan.
Por ejemplo, la capacidad de refactorizar la lógica de su aplicación es extremadamente valiosa.
Las especificaciones de aceptación de extremo a extremo brindan el mejor soporte de refactorización. Debido a
que solo usan las interfaces públicas de su código, no dependen de los detalles de implementación.
Por supuesto, las especificaciones de la unidad ayudan con la refactorización de bajo nivel, como la
reimplementación de un método específico. Pero no admitirán esfuerzos de refactorización más grandes, como
eliminar una clase por completo y distribuir su lógica en otro lugar.
El soporte de refactorización de las especificaciones de aceptación tiene un precio. Son más difíciles de escribir,
más frágiles y más lentos que otras especificaciones. Pero brindan tanta confianza que es importante tenerlos:
solo escribimos muy pocos de ellos, nos enfocamos en el camino feliz y no los usamos para una cobertura
exhaustiva de ramas condicionales.
En la siguiente parte de este libro, escribirá una aplicación desde cero, utilizando estos tres tipos de
especificaciones en el camino. A medida que llegue a cada etapa del proyecto, piense en su objetivo para la
especificación que está a punto de escribir. Eso lo guiará naturalmente hacia qué tipo usar.
informar fe de erratas • discutir
Machine Translated by Google
Parte II
Creación de una aplicación con RSpec 3
¿Cómo construimos software que hace lo que nuestros
usuarios quieren? ¿Y cómo lo mantenemos funcionando
bien mientras lo escribimos?
Con el desarrollo de afuera hacia adentro, dibuja la aplicación
en su capa más externa (la interfaz de usuario o el protocolo
de red) y avanza hacia las clases y los métodos que
contienen la lógica detrás de la interfaz. Este enfoque lo
ayuda a asegurarse de que está utilizando todo lo que
construye.
RSpec 3 facilita el desarrollo de afuera hacia adentro. Esta
parte del libro lo transformará en una potencia de pruebas.
Al crear una aplicación y probarla con RSpec, agregará
varias herramientas a su caja de herramientas que lo harán
más efectivo.
Machine Translated by Google
En este capítulo, verá:
• Una descripción general del proyecto que construirá •
Configuración de RSpec para un proyecto
real • Cómo comenzar a escribir especificaciones de
aceptación • Cómo marcar el trabajo en progreso
CAPÍTULO 4
Comenzando desde el exterior: especificaciones de aceptación
Ha visto las partes básicas de RSpec: grupos de ejemplo, ejemplos y expectativas. Ahora,
va a juntar esas piezas mientras crea y prueba una aplicación real.
En este capítulo, elegiremos un problema para resolver y esbozaremos las piezas
principales de la solución. A medida que siga, creará un nuevo proyecto y comenzará a
probarlo con RSpec. Comenzará con las especificaciones de aceptación, que verifican el
comportamiento de la aplicación como un todo. Al final del capítulo, tendrá el esqueleto de
una aplicación en vivo y una especificación para probarla, además de algunas pistas sobre
dónde comenzar a completar los detalles.
Primeros pasos
Antes de comenzar, debemos decidir qué tipo de aplicación vamos a crear: un sistema
integrado, un motor de búsqueda, un clon de Twitter o lo que sea.
Tendremos que esbozar suficientes piezas para decidir qué tecnologías vamos a utilizar.
Luego, podemos configurar RSpec en nuestro nuevo directorio de proyectos. A lo largo del
proyecto, verá cómo el desarrollo de afuera hacia adentro lo ayuda a construir un mejor
sistema.
El proyecto: un rastreador de gastos
Necesitaremos un proyecto lo suficientemente grande como para contener algunos problemas del mundo real,
pero lo suficientemente pequeño como para trabajar en unos pocos capítulos. ¿Qué tal un servicio web para el
seguimiento de los gastos? Los clientes utilizarán algún tipo de software de cliente (una aplicación de línea de
comandos, una GUI o incluso una aplicación web) para realizar un seguimiento e informar sobre sus actividades diarias.
gastos.
informar fe de erratas • discutir
Machine Translated by Google
Capítulo 4. Comenzando desde el exterior: Especificaciones de aceptación • 46
Estas son las partes principales de la aplicación:
• Una aplicación web escrita en Sinatra que recibirá HTTP entrante
solicitudes (para agregar nuevos gastos o buscar los existentes)1 •
Una capa de base de datos que usa Sequel para almacenar gastos entre solicitudes2
• Un conjunto de objetos Ruby para representar los gastos y pegar las otras piezas
juntos
El siguiente diagrama muestra cómo encajan las piezas:
HTTP Código de enrutamiento
Pedido
HTTP (sus rutas de Sinatra)
Las especificaciones de
Lógica de gastos aceptación ejercen todas las
(tu código Ruby)
capas
Adaptador
(Continuación)
Base de datos
(SQLite)
Necesitamos probar todo esto de diferentes maneras. Comenzamos con las especificaciones de
aceptación que impulsan toda la aplicación desde la capa más externa, el ciclo de solicitud/respuesta HTTP.
¿Por qué no los rieles?
Podríamos haber usado Rails para construir este proyecto. Sin embargo, Rails tiene muchas funciones que
no necesitamos aquí, como correos, vistas orientadas al usuario, una canalización de activos y un sistema
de colas de trabajos. Además, Rails configura un arnés de prueba para usted. Eso es útil para proyectos
del mundo real, pero se interpone cuando está aprendiendo a configurar sus propias pruebas.
Por otro lado, las API JSON pequeñas como la que estamos construyendo aquí están justo en el punto
óptimo de Sinatra. Y es lo suficientemente simple como para que pueda conectarlo fácilmente a RSpec por
su cuenta. Todo lo que aprenda en este libro seguirá aplicándose a los proyectos de Rails.
1. http://www.sinatrarb.com/ 2.
http://sequel.jeremyevans.net/
informar fe de erratas • discutir
Machine Translated by Google
Primeros pasos • 47
Empezando
Ya ha instalado RSpec para algunos experimentos independientes. Pero este proyecto tiene más
dependencias que solo RSpec. Los clientes que implementen nuestra aplicación querrán usar
exactamente las mismas versiones de Ruby Gems con las que hemos probado.
Para esta aplicación, usaremos Bundler para catalogar e instalar todas las bibliotecas de las que
dependemos.3 Si nunca ha usado Bundler, no se preocupe; Si bien no lo explicaremos en
profundidad, no necesitará ninguna experiencia previa para seguirlo.
Cree un nuevo directorio llamado Expense_tracker. A partir de ahí, instale Bundler de la misma
manera que instalaría cualquier gema de Ruby y luego ejecute bundle init para configurar su
proyecto para usar Bundler:
$ gem install bundler
Bundler1.15.3 instalado con éxito 1 gema instalada
$ bundle init
Escribiendo un
nuevo Gemfile en ~/code/expense_tracker/Gemfile
Necesitaremos cuatro bibliotecas de Ruby para comenzar:
• RSpec para probar nuestro
proyecto • Coderay para una salida de falla resaltada en la sintaxis y fácil de leer
• Rack::Test para proporcionar una API para impulsar los servicios web a partir
de las pruebas • Sinatra para implementar la aplicación web; su huella ligera y simple
Las API son una buena opción para este proyecto
Para traer estas dependencias al proyecto, agregue las siguientes líneas al final del Gemfile recién
generado:
04specsdeaceptación/01/expense_tracker/
Gemfile gema 'rspec', '3.6.0'
gema 'coderay', '1.1.1'
gema 'racktest', '0.7.0' gema
'sinatra', '2.0.0'
Luego, dígale a Bundler que instale las bibliotecas requeridas y sus dependencias:
$ instalación del
paquete Obtención de metadatos de gemas de https://rubygems.org/.........
Obteniendo metadatos de la versión de https://rubygems.org/..
Resolviendo dependencias...
Usando bundler 1.15.3
Usando coderay 1.1.1
Usando difflcs 1.3
Obteniendo mustermann 1.0.0
3. http://bundler.io
informar fe de erratas • discutir
Machine Translated by Google
Capítulo 4. Comenzando desde el exterior: Especificaciones de aceptación • 48
Instalación de mustermann 1.0.0
Obtención de bastidor
2.0.3 Instalación de bastidor
2.0.3 Uso de rspecsupport 3.6.0
Obtención de inclinación
2.0.7 Instalación de
inclinación 2.0.7 Obtención de protección
de bastidor 2.0.0 Instalación de protección
de bastidor 2.0.0 Obtención de
prueba de bastidor 0.7.0 Instalación
de racktest 0.7.0 Uso de
rspeccore 3.6.0 Uso de rspec
expectations 3.6.0 Uso de
rspecmocks 3.6.0
Recuperación de sinatra 2.0.0
Instalación de sinatra
2.0.0 Uso de rspec 3.6.0 ¡Paquete completo! 4 dependencias Gemfile, 14 gemas ahora instaladas.
Use ̀bundle info [gemname]` para ver dónde está instalada una gema incluida.
Ahora, configure el proyecto para usar RSpec. Por ahora, siempre ejecutaremos rspec usando bundle exec,
para asegurarnos de que estamos usando las versiones de biblioteca exactas que esperamos.
En Bundler, en la página 293, hablaremos sobre otra forma más rápida de ejecutar nuestras especificaciones.
$ bundle exec rspec init
create .rspec create
spec/spec_helper.rb
Este comando generará dos archivos:
• .rspec, que contiene marcas de línea de comandos predeterminadas •
spec/spec_helper.rb, que contiene opciones de configuración
Los indicadores predeterminados en .rspec harán que RSpec cargue spec_helper.rb por nosotros antes de
cargar y ejecutar nuestros archivos de especificaciones.
Deberá agregar una línea en la parte superior de spec/spec_helper.rb:
04acceptancespecs/01/expense_tracker/spec/spec_helper.rb
ENV['RACK_ENV'] = 'prueba'
Ahora que todas las piezas están en su lugar, podemos escribir nuestro primer ejemplo.
Utilice el entorno de rack adecuado
Configuración de la variable de entorno RACK_ENV para probar los interruptores en el
comportamiento amigable de prueba en su marco web. Sinatra normalmente se traga las
excepciones y genera una respuesta de "Error interno del servidor 500". Con este
conjunto de variables, Sinatra permitirá que los errores aparezcan en su marco de prueba.
informar fe de erratas • discutir
Machine Translated by Google
Decidir qué probar primero • 49
Decidir qué probar primero
Incluso esta sencilla aplicación tiene varias piezas. Es fácil sentirse abrumado cuando estamos decidiendo
qué probar primero. ¿Donde empezamos?
Para impulsar el primer ejemplo, pregúntese: ¿cuál es el núcleo del proyecto? ¿Qué es lo único que
acordamos que debe hacer nuestra API? Debe guardar fielmente los gastos que registramos.
Codifiquemos la primera parte de ese comportamiento deseado en una especificación y luego
implementemos el comportamiento. Coloque el siguiente código en spec/acceptance/expense_tracker_api_spec.rb:
04acceptancespecs/01/expense_tracker/spec/acceptance/expense_tracker_api_spec.rb
requiere 'bastidor/prueba'
requiere 'json'
módulo ExpenseTracker
RSpec.describe 'Expense Tracker API' incluye
Rack::Prueba::Métodos
' registra los gastos enviados' do cafe =
{ 'beneficiario'
=> 'Starbucks', 'cantidad' =>
5.75, 'fecha' =>
'20170610'
}
publicar '/ gastos', JSON.generar (café) final final final
Tenga en cuenta que podemos anidar contextos RSpec dentro de módulos. En nuestro código base,
incluiremos tanto nuestra aplicación como nuestras especificaciones dentro del módulo ExpenseTracker
para que podamos acceder fácilmente a todas las clases definidas por nuestra aplicación.
Use un tipo de datos preciso para representar la moneda
Para simplificar nuestros ejemplos de código para que pueda concentrarse en aprender
RSpec, usamos números regulares de punto flotante de Ruby para representar montos
de gastos, aunque la aritmética de punto flotante no es lo suficientemente precisa para
manejar dinero.4
En un proyecto real, usaríamos la clase BigDecimal integrada en Ruby o una biblioteca
de divisas dedicada como Money gem.5,6
4. https://spin.atomicobject.com/2014/08/14/currencyroundingerrors/
5. https://rubydoc.org/stdlib2.4.1/libdoc/bigdecimal/rdoc/BigDecimal.
html 6. http://rubymoney.github.io/money/
informar fe de erratas • discutir
Machine Translated by Google
Capítulo 4. Comenzando desde el exterior: Especificaciones de aceptación • 50
No necesitamos diseñar toda la API por adelantado. Supongamos que publicaremos algunos pares clavevalor
en el punto final /expenses . Como hacen muchas API web, admitiremos el envío y la recepción de datos en
formato JSON.7 Debido a que los objetos JSON se convierten en hashes de Ruby con claves de cadena,
nuestros datos de ejemplo también tendrán claves de cadena. Por ejemplo, diremos { 'beneficiario' => 'Starbucks' }
en lugar de { beneficiario: 'Starbucks' }.
Una mirada rápida a las API HTTP
Nuestra API de seguimiento de gastos se basa en el Protocolo de transferencia de hipertexto (HTTP).
Este es el mismo protocolo que usa su navegador web para conectarse a sitios web, pero en nuestro
caso las solicitudes no necesariamente provendrán de un navegador. Un programa de línea de
comandos, una GUI de escritorio o un conjunto de ejemplos de RSpec pueden generar estas solicitudes.
Solo usaremos dos de las características más básicas de HTTP en estos ejemplos:
• Una solicitud GET lee datos de la aplicación. •
Una solicitud POST modifica los datos.
Hay mucho más en HTTP que solo esto. Para obtener más información, consulte uno de los muchos
tutoriales disponibles.a
a. https://code.tutsplus.com/tutorials/abeginnersguidetohttpandrestnet16340
Para obtener los datos dentro y fuera de nuestra aplicación, usaremos varios métodos auxiliares diferentes de
Rack::Test::Methods. Como puede ver, puede incluir módulos de Ruby en un contexto RSpec, tal como está
acostumbrado a hacer dentro de las clases de Ruby.
El primer ayudante de Rack::Test que usaremos es post. Esto simulará una solicitud HTTP POST, pero lo hará
llamando a nuestra aplicación directamente en lugar de generar y analizar paquetes HTTP.
Todavía no tenemos una aplicación, pero sigamos adelante y ejecutemos nuestras especificaciones de todos
modos. Esto nos dará una pista sobre qué implementar a continuación:
$ paquete ejecutivo rspec
F
Fallas:
1) Expense Tracker API registra los gastos enviados
Fallo/Error: publicar '/gastos', JSON.generar (café)
NameError:
variable local no definida o método ̀app' para
#<RSpec::ExampleGroups::ExpenseTrackerAPI:0x007fef0404f560>
« truncado »
7. http://json.org
informar fe de erratas • discutir
Machine Translated by Google
Decidir qué probar primero • 51
Este mensaje de error y la documentación de Rack::Test nos dicen que nuestro conjunto de pruebas
necesita definir un método de aplicación que devuelva un objeto que represente nuestra aplicación web.8
Todavía no hemos construido este objeto. Supongamos que será una clase llamada API en el módulo
ExpenseTracker . Agregue el siguiente código dentro de su contexto, justo encima de la línea it :
04acceptancespecs/02/expense_tracker/spec/acceptance/expense_tracker_api_spec.rb
def
aplicación
ExpenseTracker::API.nuevo final
Los contextos de RSpec son solo clases de Ruby, lo que significa que puede definir métodos auxiliares
como app, y estarán disponibles dentro de todos sus ejemplos.
El código que desearías tener
¿Parece que nos estamos adelantando al instanciar una clase inexistente?
Esta técnica puede ayudar a desarrollar su diseño.
Primero, escribes el código que deseas tener. Luego, completa la implementación.
Diseñar cosas desde la perspectiva de la persona que llama lo ayuda a escribir una API fácil de usar.
Vuelva a ejecutar sus especificaciones para ver dónde fallan:
$ paquete ejecutivo rspec
F
Fallas:
1) Expense Tracker API registra los gastos enviados Falla/Error:
ExpenseTracker::API.new
Error de nombre:
ExpenseTracker::API constante sin inicializar
« truncado »
Todavía no hemos definido esta clase. Hagámoslo.
A diferencia de Ruby on Rails, Sinatra no tiene una convención de nomenclatura de directorios establecida.
Sigamos la convención de Rails y coloquemos el código de nuestra aplicación en una carpeta llamada
app. Coloque el siguiente código en app/api.rb:
04acceptancespecs/02/expense_tracker/app/api.rb
requiere 'sinatra/base' requiere
'json'
8. https://github.com/racktest/racktest#examples
informar fe de erratas • discutir
Machine Translated by Google
Capítulo 4. Comenzando desde el exterior: Especificaciones de aceptación • 52
módulo ExpenseTracker
clase API < Sinatra::Base
fin
fin
Esta clase define el esqueleto más básico de una aplicación de Sinatra. Ahora, nuestras pruebas necesitan
cargarlo. De vuelta en su especificación, agregue la siguiente línea a la sección requerida en la parte superior:
04acceptancespecs/02/expense_tracker/spec/acceptance/
expense_tracker_api_spec.rb require_relative '../../app/api'
Ahora, sus especificaciones están pasando. Quizás se esté preguntando: “¿Realmente probamos algo? ¿No
deberíamos esperar o afirmar algo?”.
Llegaremos a eso. En este momento, solo estamos verificando que la solicitud POST se complete sin
bloquear la aplicación. A continuación, comprobaremos que obtuvimos una respuesta válida de la aplicación.
Comprobación de la respuesta
Rack::Test proporciona el método last_response para comprobar las respuestas HTTP. Agregue la siguiente
línea dentro de su especificación, justo después de la solicitud de publicación :
04acceptancespecs/03/expense_tracker/spec/acceptance/
expense_tracker_api_spec.rb expect(last_response.status).to eq(200)
Ha encontrado código como este antes, en Su primera especificación, en la página 5. Es una expectativa
que cumple el papel que desempeñaría un método de aserción en otros marcos de prueba.
Juntos, expect() y to() comprueban un resultado para señalar el éxito o el fracaso.
Comparan un valor, en este caso, el código de estado HTTP devuelto por last_response.status, utilizando un
comparador. Aquí, creamos un comparador usando el método eq , que indica si el valor envuelto por expect
es igual o no al argumento proporcionado de 200. Cuando pasamos el comparador de eq(200) como
argumento al método to() , Obtendrá un resultado de aprobación o reprobación.
Esto puede parecer un montón de partes móviles en comparación con un método de estilo de aserción
tradicional como assert_equal. Sin embargo, los emparejadores son más poderosos y más componibles que
las aserciones tradicionales. Veremos más sobre ellos en Explorando las expectativas de RSpec.
Veamos qué sucede cuando ejecutamos este código:
$ paquete ejecutivo rspec
F
Fallas:
1) Expense Tracker API registra los gastos enviados
informar fe de erratas • discutir
Machine Translated by Google
Completando el cuerpo de la respuesta • 53
Fallo/Error: expect(last_response.status).to eq(200)
esperado: 200
obtenido: 404
(comparado usando ==)
« truncado »
Nuestra aplicación devuelve un código de estado 404 (No encontrado). Eso no es sorprendente; aún no hemos
agregado ninguna ruta al código de Sinatra. Adelante, hazlo ahora:
04acceptancespecs/03/expense_tracker/app/api.rb
la publicación '/gastos'
finaliza
Cuando vuelva a ejecutar sus especificaciones, debería ver un resultado de aprobación.
Completar el cuerpo de la respuesta
Piense en cómo le gustaría que se vieran los datos de respuesta. Sería bueno recuperar una identificación única
para el gasto que acabamos de registrar para que podamos consultarlo más tarde. Devolvamos un objeto JSON
que se parece a esto:
{ "id_gastos": 42 }
La biblioteca JSON de Ruby puede analizar de manera segura un registro simple como este en un hash de Ruby:
>> requiere 'json' =>
verdadero
>> JSON.parse('{ "id_gastos": 42 }') =>
{"id_gastos"=>42}
En este punto, realmente no nos importa cuál es la ID específica, solo que los datos tengan esta estructura
general. Los emparejadores de RSpec facilitan la expresión de esta idea. Agregue las siguientes líneas resaltadas
dentro de su especificación, justo después de la verificación de respuesta HTTP:
04acceptancespecs/04/expense_tracker/spec/acceptance/expense_tracker_api_spec.rb
' registra los gastos enviados' do coffee =
{ 'beneficiario'
=> 'Starbucks', 'cantidad' =>
5.75, 'fecha' => '2017
0610'
}
publicar '/ gastos', JSON.generar (café) esperar
(última_respuesta.estado). a eq (200)
analizado = JSON.parse(last_response.body)
expect(analizado).to include('expense_id' => a_kind_of(Integer))
fin
informar fe de erratas • discutir
Machine Translated by Google
Capítulo 4. Comenzando desde el exterior: Especificaciones de aceptación • 54
Los emparejadores include y a_kind_of nos permiten explicar en términos generales lo que queremos: un
hash que contiene una clave de 'expense_id' y un valor entero. Al pasar un comparador a otro, los hemos
compuesto para crear uno nuevo que especifica el nivel correcto de detalle. Hablaremos más sobre los
emparejadores componibles en Componer emparejadores, en la página 176.
Cuando ejecutamos nuestra especificación actualizada, obtenemos otra falla:
$ paquete ejecutivo rspec
F
Fallas:
1) Expense Tracker API registra los gastos enviados
Falla/Error: analizado = JSON.parse(last_response.body)
JSON::ParserError:
''
743: token inesperado en
« truncado »
Nuestro bloque de publicación vacío simplemente envía una cadena vacía al cliente. En su lugar, debemos
enviar texto con formato JSON. Agregue la siguiente línea a su aplicación Sinatra:
04acceptancespecs/04/expense_tracker/app/
api.rb publicar '/gastos' hacer
JSON.generate('expense_id' => 42)
fin
Una vez que estemos generando texto que coincida con nuestro patrón esperado, nuestras especificaciones
volverán a pasar. Por supuesto, este ejemplo pasajero no nos dice mucho.
Hemos logrado engañarlo devolviendo algunos datos enlatados. Esta práctica de codificar los valores
devueltos solo para satisfacer una expectativa, conocida como simplificar la prueba, nos permite desarrollar
la especificación de principio a fin y luego volver más tarde para implementar el comportamiento
correctamente.9
Consultando los datos
Ahorrar gastos está muy bien, pero sería bueno recuperarlos. Queremos permitir que los usuarios
obtengan gastos por fecha, así que publiquemos algunos gastos con diferentes fechas y luego solicitemos
los gastos para una de esas fechas. Esperamos que la aplicación responda solo con los gastos registrados
en esa fecha.
Publicar un gasto tras otro se volverá muy viejo si tenemos que seguir repitiendo todo ese código.
Extraigamos esa lógica auxiliar en un método auxiliar post_expense dentro del bloque RSpec.describe :
9. https://www.youtube.com/watch?v=PhiXo5CWjYU
informar fe de erratas • discutir
Machine Translated by Google
Consultando los datos • 55
04acceptancespecs/05/expense_tracker/spec/acceptance/expense_tracker_api_spec.rb
def post_expense(gastos) post '/
gastos', JSON.generate(gastos)
expect(last_response.status).to eq(200)
analizado = JSON.parse(última_respuesta.cuerpo)
esperar(analizado).to include('expense_id' => a_kind_of(Integer)) gasto.merge('id' =>
analizado['gasto_id']) end
Este es básicamente el mismo código que antes, excepto que agregamos una llamada para fusionar
al final. Esta línea solo agrega una clave de identificación al hash, que contiene cualquier identificación
que se asigne automáticamente desde la base de datos. Hacerlo hará que las expectativas de
escritura sean más fáciles más adelante; podremos comparar la igualdad exacta.
Ahora, cambie el gasto de café al siguiente código más corto:
04aceptaciónespecificaciones/05/expense_tracker/spec/aceptación/
expense_tracker_api_spec.rb café = post_gasto(
'beneficiario' => 'Starbucks',
'cantidad' => 5,75,
'fecha' => '20170610'
)
Usando el mismo ayudante, registremos un gasto en la misma fecha y otro en una fecha diferente:
04acceptancespecs/05/expense_tracker/spec/acceptance/expense_tracker_api_spec.rb
zoo = post_expense(
'beneficiario' =>
'Zoológico', 'cantidad' =>
15.25, 'fecha' => '20170610'
)
comestibles = post_expense(
'beneficiario' => 'Whole Foods',
'cantidad' => 95.20,
'fecha' => '20170611'
)
Finalmente, puede consultar todos los gastos del 10 de junio y asegurarse de que los resultados
contengan solo los valores de esa fecha. Agregue las siguientes líneas resaltadas dentro de la misma
especificación en la que hemos estado trabajando, justo después de los gastos del zoológico y de
comestibles:
04acceptancespecs/05/expense_tracker/spec/acceptance/expense_tracker_api_spec.rb '
registra los gastos enviados' haga # POST
gastos de café, zoológico y comestibles aquí
obtener '/gastos/20170610' esperar
(última_respuesta.estado).to eq(200)
informar fe de erratas • discutir
Machine Translated by Google
Capítulo 4. Comenzando desde el exterior: Especificaciones de aceptación • 56
gastos = JSON.parse(última_respuesta.cuerpo)
esperar(gastos).contener_exactamente(café, zoológico)
fin
Estamos usando las mismas técnicas de antes: controlar la aplicación, obtener la última respuesta
de Rack::Test y ver los resultados. Hay muchas formas de comparar colecciones en RSpec. Aquí,
queremos verificar que la matriz contenga los dos gastos que queremos, y solo esos dos, sin
tener en cuenta el orden. El comparador container_exactly captura este requisito.
Si tuviéramos un requisito comercial específico de que los gastos estuvieran en una determinada
secuencia, podríamos comparar las colecciones con eq en su lugar, como en eq [café, zoológico].
Aquí, no nos importa el orden. El uso de un comparador más flexible que eq hace que nuestra
especificación sea más resistente, lo que nos da la libertad de cambiar el orden en el futuro sin
luchar contra una prueba rota.
Continúe y ejecute la última versión de su especificación:
$ paquete ejecutivo rspec
F
Fallas:
1) Expense Tracker API registra los gastos enviados
Fallo/Error: expect(last_response.status).to eq(200)
esperado: 200
obtenido: 404
(comparado usando ==)
« truncado »
Dado que aún no hemos definido una forma para que los clientes vuelvan a leer los datos,
estamos obteniendo otro código de estado 404. Agreguemos una ruta a la aplicación Sinatra que
devuelve una matriz JSON vacía:
04acceptancespecs/05/expense_tracker/app/api.rb
get '/expenses/:date' do
JSON.generate([]) end
Ahora, cuando volvemos a ejecutar nuestras especificaciones, obtenemos una respuesta incorrecta, en lugar de una
Error HTTP:
$ paquete ejecutivo rspec
F
Fallas:
1) Expense Tracker API registra los gastos enviados
Fracaso/Error: esperar (gastos) para contener_exactamente (café, zoológico)
informar fe de erratas • discutir
Machine Translated by Google
Guardar su progreso: especificaciones pendientes • 57
la colección esperada contenía: [{"payee"=>"Starbucks", "amount"=>5.75,
"date"=>"20170610", "id"=>42}, {"payee"=>" Zoo", "cantidad"=>15,25, "fecha"=>"20170610",
"id"=>42}] colección real contenida: los elementos que faltaban
eran: "cantidad"=>5,75, "fecha []
"=>"20170610", "id"=>42}, [{"beneficiario"=>"Starbucks",
{"beneficiario"=>"Zoo", "cantidad"=>15.25, "fecha"=>"20170610", "id"=>42}]
« truncado »
No tiene mucho sentido posponer lo inevitable. Vamos a tener que escribir algo de código para
ahorrar y cargar gastos.
Guardando su progreso: especificaciones pendientes
Antes de desviarse hacia la implementación de bajo nivel, es una buena idea guardar su trabajo
hasta el momento. Sin embargo, no recomendamos dejar las especificaciones en un estado
defectuoso. Así que marquemos este como en progreso. Agregue la siguiente línea resaltada en la
parte superior de su especificación:
04acceptancespecs/06/expense_tracker/spec/acceptance/expense_tracker_api_spec.rb
' registra los gastos enviados' do pendiente
'Necesidad de mantener los gastos'
Ahora, cuando ejecute sus especificaciones, recibirá un recordatorio de en qué estaba trabajando:
$ paquete ejecutivo rspec
*
Pendiente: (Se esperan las fallas enumeradas aquí y no afectan el estado de su suite )
1) Expense Tracker API registra los gastos enviados
# Necesidad de mantener los gastos
Fracaso/Error: esperar (gastos) para contener_exactamente (café, zoológico)
la colección esperada contenía: [{"payee"=>"Starbucks", "amount"=>5.75,
"date"=>"20170610", "id"=>42}, {"payee"=>" Zoo", "amount"=>15.25, "date"=>"20170610",
"id"=>42}] la colección real contenía:
[]
los elementos que faltaban eran: [{"beneficiario"=>"Starbucks",
"cantidad"=>5,75, "fecha"=>"20170610", "id"=>42}, {"beneficiario"=>"Zoo", "cantidad"= >
15,25 , "fecha"=>"20170610", "id"=>42}]
# ./spec/acceptance/expense_tracker_api_spec.rb:46:in ̀bloque (2 niveles) en
<módulo:ExpenseTracker>'
Finalizó en 0,03437 segundos (los archivos tardaron 0,1271 segundos en cargarse) 1
ejemplo, 0 fallas, 1 pendiente
informar fe de erratas • discutir
Machine Translated by Google
Capítulo 4. Comenzando desde el exterior: Especificaciones de aceptación • 58
Una vez que implemente ese comportamiento, eliminará la línea pendiente . RSpec fallará en sus
especificaciones si lo olvida; en efecto, está diciendo: "¡Oye, dijiste que esto aún no funcionaba!"
Antes de continuar, conectemos nuestra aplicación a un servidor web para que podamos verla
funcionando. Rack, el kit de herramientas HTTP sobre el que está construido Sinatra, viene con una
herramienta llamada rackup que facilita la ejecución de cualquier aplicación de Rack (incluidas las
aplicaciones creadas con Sinatra). Solo necesitamos definir un archivo de configuración rackup
llamado config.ru con los siguientes contenidos:
04acceptancespecs/06/expense_tracker/config.ru
require_relative 'app/api' ejecutar
ExpenseTracker::API.new
Suficientemente simple. Solo estamos cargando nuestra aplicación y diciéndole a Rack que la ejecute.
Con eso en su lugar, podemos iniciar nuestra aplicación ejecutando rackup:
$ bundle exec rackup
[20170613 13:34:10] INFO WEBrick 1.3.1 [20170613
13:34:10] INFO ruby 2.4.1 (20170322) [x86_64darwin15]
[20170613 13:34:10] INFORMACIÓN WEBrick::HTTPServer#inicio: pid=45203 puerto=9292
Mientras se ejecuta, podemos usar una herramienta de línea de comandos como curl en otra ventana
de terminal para enviar solicitudes a nuestra aplicación . al servidor local: 9292:
$ curl localhost:9292/gastos/20170610 w "\n" []
¡Funciona! Como era de esperar, nuestra aplicación responde con un JSON vacío
formación.
Antes de continuar, envíe el código a su sistema de control de revisiones favorito.
De esa manera, podrá continuar fácilmente donde lo dejó. Tome una taza de café, pruebe un par de
ejercicios y encuéntrenos en el próximo capítulo.
Tu turno
En este capítulo, repasamos las piezas principales del software que estamos construyendo.
Usó Bundler para administrar todas las dependencias de su proyecto, incluido RSpec. Escribió su
primera especificación para impulsar la aplicación desde su capa más externa, la interfaz HTTP, luego
escribió suficiente código Ruby para tener una idea de cómo debe ser la lógica comercial.
10. https://curl.haxx.se/
informar fe de erratas • discutir
Machine Translated by Google
Tu turno • 59
A continuación, limitaremos nuestro enfoque a la lógica de enrutamiento HTTP y la probaremos
aisladamente del resto del sistema.
Ejercicios
1. Hojee la documentación introductoria de Sinatra para tener una idea de
cómo se estructuran las aplicaciones.11
2. Lea “Prueba de Sinatra con Rack::Test” para conocer el enfoque de prueba preferido por el
equipo de Sinatra.12
3. ¿Recuerda el archivo spec_helper.rb que RSpec generó para usted? Lea algunos de los
comentarios e intente habilitar algunas de las configuraciones comentadas.
11. http://www.sinatrarb.com/intro.html
12. http://www.sinatrarb.com/testing.html
informar fe de erratas • discutir
Machine Translated by Google
En este capítulo, verá:
• La diferencia entre las especificaciones de aceptación y las especificaciones
de unidad • Cómo usar la inyección de dependencia para escribir código flexible y
comprobable • El uso de dobles de prueba/objetos simulados para reemplazar los
reales • Cómo refactorizar sus especificaciones para mantenerlas limpias y legibles
CAPÍTULO 5
Pruebas aisladas: especificaciones de la unidad
Terminaste el último capítulo con una especificación de aceptación funcional. Esta especificación
informa (correctamente) que la lógica subyacente aún no está implementada. Comenzará a
completar el esqueleto de la aplicación con una implementación funcional ahora, retomando donde
lo dejó: la capa de enrutamiento HTTP.
Dado que probará la lógica de enrutamiento de forma aislada (sin un servidor de aplicaciones en
vivo o una base de datos real), utilizará las especificaciones de la unidad para impulsar el
comportamiento en esta capa:
Pedido
HTTP (sus rutas de Sinatra) de la unidad ejercen una capa
de forma aislada
Lógica de gastos
(tu código Ruby)
Adaptador
(Continuación)
Base de datos
(SQLite)
Cuando termine este capítulo y los ejercicios, tendrá un conjunto completo de especificaciones de
unidades de aprobación para su capa HTTP.
informar fe de erratas • discutir
Machine Translated by Google
Capítulo 5. Pruebas aisladas: Especificaciones de la unidad • 62
De las especificaciones de aceptación a las especificaciones de la unidad
Deberíamos tomarnos un momento para revisar lo que queremos decir con "especificaciones de la unidad". El
término común prueba unitaria significa diferentes cosas para diferentes personas, o incluso para la misma
persona en diferentes proyectos. Las pruebas unitarias generalmente implican aislar una clase o método del
resto del código. El resultado son pruebas más rápidas y fáciles de
encontrar errores.
Donde los enfoques de pruebas unitarias difieren es en el grado de aislamiento; es decir, si eliminar todas las
dependencias posibles o probar juntos un grupo relacionado de objetos que colaboran. En este libro, usaremos
las especificaciones de la unidad para referirnos al conjunto de pruebas más rápidas y aisladas para un proyecto
en particular. Para obtener más información, consulte el artículo de Martin Fowler sobre el tema.1
Con las pruebas unitarias de este capítulo, no llamará directamente a los métodos de la clase API . En su lugar,
seguirá simulando solicitudes HTTP a través de la interfaz Rack::Test . Por lo general, prueba una clase a
través de su interfaz pública, y esta no es una excepción. La interfaz HTTP es la interfaz pública.
Impulse la API pública de cada capa
Sus pruebas para cualquier capa en particular, desde el código orientado al cliente hasta
las clases de modelos de bajo nivel, deben impulsar la API pública de esa capa. Se encontrará
tomando decisiones más cuidadosas sobre lo que se incluye o no en la API. Además, sus
pruebas serán menos frágiles y le darán mayor libertad para refactorizar su código.
Sin embargo, estará aislando la API pública del motor de almacenamiento subyacente. En efecto, está probando
una capa de la aplicación a la vez. En este capítulo, eso significa impulsar el comportamiento de la clase API
que enruta las solicitudes entrantes al motor de almacenamiento. En el próximo capítulo, probará y diseñará el
propio motor de almacenamiento.
Para obtener más información sobre los matices entre los diferentes tipos de especificaciones, consulte el
2
artículo de Xavier Shay, "Cómo pruebo las aplicaciones Rails".
Una mejor experiencia de prueba
Antes de saltar a las pruebas, tomemos un momento para configurar RSpec para la tarea en cuestión. La
configuración predeterminada de RSpec es mínima por diseño. Pero el marco ofrece una serie de
configuraciones sugeridas que son fáciles de activar.
1. https://martinfowler.com/bliki/UnitTest.html
2. https://rhnh.net/2012/12/20/howitestrailsapplications/
informar fe de erratas • discutir
Machine Translated by Google
De especificaciones de aceptación a especificaciones de unidad • 63
Aquí hay algunas cosas que la configuración sugerida de RSpec hará por usted:
• Ejecute RSpec sin ningún cambio en las clases principales de Ruby (sin parches mono)
modo)
• Use el formateador de documentación más detallado cuando esté ejecutando solo
un archivo de especificaciones
• Ejecute sus especificaciones en orden aleatorio
Cuando inicia un proyecto con el comando rspec init (como hicimos en Primeros pasos, en la página 45),
RSpec agrega estas sugerencias a spec/spec_helper.rb pero las deja comentadas. Para habilitarlos, busque
y elimine las dos líneas marcadas =begin y =end.
Aunque todas estas configuraciones recomendadas son útiles, en realidad hemos desactivado dos de ellas
para generar una salida un poco más corta para este libro. La primera es la configuración de advertencias
para información de diagnóstico adicional: excelente cuando escribe una gema, pero un poco hablador
cuando usa una gema como Sequel que genera muchas advertencias de Ruby.
05unitspecs/01/expense_tracker/spec/spec_helper.rb
# config.warnings = verdadero
También desactivamos profile_examples, una característica que ya encontró en
Identificación de ejemplos lentos, en la página 20:
05unitspecs/01/expense_tracker/spec/spec_helper.rb
# config.profile_examples = 10
Obtener una lista de ejemplos lentos es útil para conjuntos de pruebas grandes, pero aún no hemos llegado
al final de este proyecto.
Mientras estamos en este archivo, agregue la siguiente línea dentro del bloque RSpec.configure :
05unitspecs/01/expense_tracker/spec/spec_helper.rb
RSpec.configure do |config|
config.filter_gems_from_backtrace 'rack', 'racktest', 'sequel', 'sinatra'
Cuando falla una especificación, el seguimiento impreso puede contener docenas de líneas de código de
marco de trabajo, oscureciendo el código de la aplicación que está buscando. RSpec ya filtra su propio
código del backtrace, y también puede filtrar fácilmente otras gemas.3 Tendrá menos para leer y encontrará
errores más rápidamente.
3. http://rspec.info/documentation/3.6/rspeccore/RSpec/Core/Configuration.html#filter_gems_from_backtrace
método_instancia
informar fe de erratas • discutir
Machine Translated by Google
Capítulo 5. Pruebas aisladas: Especificaciones de la unidad • 64
Si alguna vez necesita ver el backtrace completo, aún puede hacerlo; simplemente pase el indicador backtrace
o b a RSpec.
Tómese un tiempo para leer los comentarios dentro de spec_helper.rb. Explican cómo afectarán las nuevas
opciones de configuración a sus pruebas. Una vez que haya terminado, estará listo para escribir algunas
pruebas unitarias.
Dibujar el comportamiento
El comportamiento que está probando se divide en un par de categorías amplias.
Querrá ver qué sucede cuando una llamada a la API tiene éxito y cuando falla.
Utilice las especificaciones de
unidades para los casos extremos La velocidad y la simplicidad de las pruebas unitarias las
convierten en el lugar perfecto para probar todas sus ramas condicionales y casos extremos.
Cubrir exhaustivamente todos los casos en una integración más lenta o una prueba de
aceptación tiende a ser demasiado ineficiente.
Comience esbozando el caso de éxito. Cree un nuevo archivo llamado spec/unit/app/api_spec.rb con el
siguiente contenido:
05unitspecs/02/expense_tracker/spec/unit/app/
api_spec.rb require_relative '../../../app/api'
módulo Rastreador de gastos
RSpec.describe API
describe 'POST/gastos' contextualiza
' cuando el gasto se registra correctamente'
' devuelve el id de gasto' ' responde
con un final de 200 (OK)'
# ... el siguiente contexto irá aquí... fin
fin
fin
El bloque de contexto agrupa estas dos especificaciones relacionadas con el éxito y le permite compartir un
código de configuración común entre ellas.
Ahora, agregue un segundo contexto. Este se encargará del caso de falla:
05unitspecs/02/expense_tracker/spec/unit/app/
api_spec.rb context 'cuando el gasto falla en la validación'
hazlo 'devuelve un mensaje de error'
' responde con un 422 (entidad no procesable)' end
Este es suficiente contenido para empezar. Ejecute su suite ahora y RSpec informará estos casos de prueba
como pendientes:
informar fe de erratas • discutir
Machine Translated by Google
Llenar la primera especificación • 65
$ paquete exec rspec spec/unidad/aplicación/api_spec.rb
Aleatorizado con semilla 34086
ExpenseTracker::API
POST /gastos
cuando la validación del gasto falla devuelve
un mensaje de error (PENDIENTE: Aún no implementado) responde con un
422 (Entidad no procesable) (PENDIENTE: Aún no implementado) cuando el gasto
se registra
correctamente
responde con un 200 (OK) (PENDIENTE: Aún no implementado) devuelve
el id de gasto (PENDIENTE: Aún no implementado)
« truncado »
Terminado en 0.00107 segundos (los archivos tardaron 0.15832 segundos en cargarse)
4 ejemplos, 0 fallas, 4 pendientes
Aleatorizado con semilla 34086
Es hora de completar el comportamiento.
Completando la primera especificación
Todavía no tiene ninguna clase o método para indicar si el registro de un gasto tuvo éxito o no.
Tomemos un momento para esbozar cómo se vería ese código.
Conexión al almacenamiento
En primer lugar, necesitará algún tipo de motor de almacenamiento que mantenga el historial de
gastos; llámalo Libro mayor. El enfoque más simple sería que la clase API creara una instancia de
Ledger directamente:
05unitspecs/03/expense_tracker/api_snippets.rb
clase API < Sinatra::Base
def initialize
@ledger = Ledger.new
super() # resto de la inicialización desde el final de Sinatra
fin
# Más tarde, las personas que llaman
hacen esto: app = API.new
Pero este estilo limita la flexibilidad y la capacidad de prueba del código, ya que no le permite usar
un libro mayor sustituto para el comportamiento personalizado. En su lugar, considere estructurar
el código para que las personas que llaman pasen un objeto que cumple el rol de Ledger al
inicializador de la API :
informar fe de erratas • discutir
Machine Translated by Google
Capítulo 5. Pruebas aisladas: Especificaciones de la unidad • 66
05unitspecs/03/expense_tracker/api_snippets.rb
clase API < Sinatra::Base
def initialize(libro mayor:) @libro
mayor = libro mayor
super()
end
fin
# Más tarde, las personas que llaman
hacen esto: app = API.new(ledger: Ledger.new)
Esta técnica, pasar objetos de colaboración en lugar de codificarlos de forma rígida, se conoce
como inyección de dependencia (DI, por sus siglas en inglés). Esta frase evoca pesadillas de
marcos Java detallados y archivos XML incomprensibles para algunas personas. Pero como
muestra el fragmento anterior, DI en Ruby es tan simple como pasar un argumento a un método. Y
con él, obtienes varias ventajas:
• Dependencias explícitas: están documentadas allí mismo en la firma de
inicializar
• Código sobre el que es más fácil razonar (sin estado global)
• Bibliotecas que son más fáciles de colocar en otro proyecto
• Código más comprobable
Una desventaja de la forma en que hemos esbozado el código aquí es que las personas que llaman
siempre tienen que pasar un objeto para registrar los gastos. Nos gustaría que las personas que
llamen puedan decir API.new en el caso común. Afortunadamente, podemos tener nuestro pastel y
comérnoslo también. Todo lo que tenemos que hacer es darle al parámetro un valor predeterminado.
Agrega el siguiente código a app/api.rb, justo dentro de tu clase de API :
05unitspecs/03/expense_tracker/app/api.rb
def initialize(libro mayor: Libro mayor.nuevo)
@ledger = final del
libro
mayor()
Cuando llega la solicitud HTTP POST, la clase API le indicará al Ledger que registre() el gasto. El
valor de retorno de record() debe indicar el estado y la información de error:
05unitspecs/03/expense_tracker/api_snippets.rb
# Pseudocódigo de lo que sucede dentro de la clase API: # result =
@ledger.record({ 'some' => 'data' }) result.success?
# => un booleano
result.expense_id # => un número
result.error_message # => una cadena o cero
informar fe de erratas • discutir
Machine Translated by Google
Llenar la primera especificación • 67
Aún no es hora de escribir la clase Ledger . No estás probando su comportamiento aquí;
estás probando la clase API . En su lugar, necesitará algo que sustituya a una instancia de
Ledger . Específicamente, necesitarás un doble de prueba.
Dobles de prueba: Mocks, Stubs y otros
Un doble de prueba es un objeto que reemplaza a otro durante una prueba. Los probadores
tienden a referirse a ellos como simulacros, stubs, falsificaciones o espías, dependiendo de
cómo se utilicen. RSpec admite todos estos usos bajo el término general de dobles.
Explicaremos las diferencias en Comprensión de los dobles de prueba, o puede leer el
artículo de Martin Fowler "Dobles de prueba" para obtener un resumen rápido.4
Para crear un sustituto para una instancia de una clase en particular, utilizará el método
instance_double de RSpec y le pasará el nombre de la clase que está imitando (esta clase
no necesita existir todavía). Dado que necesitará acceder a esta instancia falsa de Ledger
desde todas sus especificaciones, la definirá mediante una construcción let , tal como lo hizo
con el objeto sándwich en el primer capítulo.
Hay un par de otras adiciones para hacer a su especificación, que hemos destacado para
usted. Modifique api_spec.rb a la siguiente estructura:
05unitspecs/03/expense_tracker/spec/unit/app/
api_spec.rb require_relative '../../../app/api'
require 'rack/test'
módulo Rastreador de gastos
RecordResult = Struct.new(:¿éxito?, :expense_id, :error_message)
RSpec.describe API do
include Rack::Test::Methods def app
API.new(ledger:
ledger)
end
let(:ledger) { instance_double('ExpenseTracker::Ledger') }
describir 'POST/gastos' hacer
contexto 'cuando el gasto se registra con éxito' hacer # ... las
especificaciones van aquí ...
fin
context 'cuando el gasto falla en la validación' do
# ... las especificaciones van
aquí ... fin
fin
fin
fin
4. https://martinfowler.com/bliki/TestDouble.html
informar fe de erratas • discutir
Machine Translated by Google
Capítulo 5. Pruebas aisladas: Especificaciones de la unidad • 68
Al igual que con las especificaciones de aceptación, utilizará Rack::Test para enrutar las
solicitudes HTTP a la clase API . El otro gran cambio es empaquetar la información de estado en
una clase RecordResult simple . Eventualmente, moveremos esta definición al código de la
aplicación. Pero eso puede esperar hasta que hayamos definido Ledger.
Usar objetos de valor en los límites de capa
La costura entre capas es donde se esconden los errores de integración. El uso de
un objeto de valor simple como RecordResult o Struct entre capas facilita aislar el
código y confiar en sus pruebas. Consulte la excelente charla “Boundaries” de Gary
Bernhardt para obtener más detalles.5
Ahora, está listo para completar el cuerpo del primer ejemplo. Dentro del primer contexto, busque
la especificación vacía 'devuelve el id. de gastos' que esbozó anteriormente.
Cámbialo por el siguiente código:
05unitspecs/03/expense_tracker/spec/unit/app/
api_spec.rb ' devuelve la identificación del gasto '
gasto = { 'algunos' => 'datos' }
permitir (libro mayor). recibir (: registro) . con
(gasto) . y_devolver
(Resultado del registro. nuevo (verdadero, 417, cero))
publicar '/ gastos', JSON.generar (gastos)
analizado = JSON.parse(last_response.body)
expect(analizado).to include('expense_id' => 417) end
En las líneas resaltadas, llamamos al método allow desde rspecmocks.
Este método configura el comportamiento del doble de prueba: cuando la persona que llama (la
clase API ) invoca el registro, el doble devolverá una nueva instancia de RecordResult que indica
una publicación exitosa.
Otra cosa a tener en cuenta: el hash de gastos que estamos pasando no se parece en nada a
datos válidos. En la aplicación en vivo, los datos entrantes se parecerán más a { 'beneficiario' ...,
=> 'cantidad' ...,
=>'fecha' => ... }. Esto está bien; el punto central de la prueba de Ledger doble es
que devolverá una respuesta de éxito o falla enlatada, sin importar la entrada.
Tener datos que parecen obviamente falsos puede ser de gran ayuda. Nunca lo confundirá con
lo real en el resultado de su prueba y perderá el tiempo preguntándose: "¿Cómo resultó este
gasto en ese informe?"
Ejecute sus especificaciones; deberían fallar, porque el comportamiento de la API aún no está
implementado:
5. https://www.destroyallsoftware.com/talks/boundaries
informar fe de erratas • discutir
Machine Translated by Google
Manejo del éxito • 69
$ bundle exec rspec spec/unit/app/api_spec.rb « truncado »
Fallas:
1) ExpenseTracker::API POST /expenses cuando el gasto se registra correctamente devuelve la
identificación del gasto
Fallo/Error: esperar (analizado). para incluir ('expense_id' => 417)
esperado {"expense_id" => 42} para incluir {"expense_id" => 417}
diferencia:
@@ 1,2 +1,2 @@
"id_gastos" => 417,
+"id_gastos" => 42,
# ./spec/unit/app/api_spec.rb:30:in ̀bloque (4 niveles) en
<módulo:ExpenseTracker>'
Terminado en 0.03784 segundos (los archivos tardaron 0.16365 segundos en
cargarse) 4 ejemplos, 1 falla, 3 pendientes
Ejemplos fallidos:
rspec ./spec/unit/app/api_spec.rb:20 # ExpenseTracker::API POST /expenses cuando el gasto se
registra correctamente devuelve la identificación del gasto
Aleatorizado con semilla 56373
Una vez que tenga una especificación fallida, es hora de completar la implementación.
Manejo del éxito
Para aprobar esta especificación, la ruta /expenses de nuestra API debe hacer tres cosas:
• Analizar un gasto del cuerpo de la solicitud
• Usar su libro mayor (ya sea uno real basado en una base de datos o uno falso para realizar pruebas)
para registrar el gasto
• Devolver un documento JSON que contenga el ID de gasto resultante
Cambie su ruta /expenses dentro de app/api.rb al siguiente código:
05unitspecs/03/expense_tracker/app/
api.rb publicar '/gastos' hacer
gasto = JSON.parse(solicitud.cuerpo.leer) resultado
= @ledger.record(gasto)
JSON.generate('expense_id' => resultado.expense_id) end
Ahora, vuelva a ejecutar las especificaciones:
$ bundle exec rspec spec/unit/app/api_spec.rb « truncado »
informar fe de erratas • discutir
Machine Translated by Google
Capítulo 5. Pruebas aisladas: Especificaciones de la unidad • 70
Terminado en 0.02645 segundos (los archivos tardaron 0.14491 segundos en
cargarse) 4 ejemplos, 0 fallas, 3 pendientes
Aleatorizado con semilla 23924
La API ahora registra correctamente el gasto y devuelve el resultado que esperamos.
Ahora que sabemos que la aplicación devuelve el ID de gasto correctamente, pasemos al
siguiente comportamiento: mostrar el código de estado HTTP correcto. Complete el cuerpo de
la especificación 'responde con un 200 (OK)' de la siguiente manera:
05unitspecs/04/expense_tracker/spec/unit/app/api_spec.rb
' responde con un 200 (OK)' hacer gastos
= { 'algunos' => 'datos' }
permitir (libro mayor). recibir (: registro) .
con (gasto) .
y_devolver (Resultado del registro. nuevo (verdadero, 417, cero))
publicar '/gastos', JSON.generar(gastos)
esperar(última_respuesta.estado).to eq(200) fin
Continúe y ejecute este archivo de especificaciones:
$ paquete exec rspec spec/unidad/aplicación/api_spec.rb
Aleatorizado con semilla 55289
ExpenseTracker::API
POST /gastos
cuando el gasto se registra con éxito
devuelve la identificación del
gasto responde con un 200
(OK) cuando el gasto falla en la validación
responde con un 422 (entidad no procesable) (PENDIENTE: aún no
implementado)
devuelve un mensaje de error (PENDIENTE: aún no implementado)
« truncado »
Terminado en 0.02565 segundos (los archivos tardaron 0.12232 segundos en
cargarse) 4 ejemplos, 0 fallas, 2 pendientes
Aleatorizado con semilla 55289
Pasa, porque Sinatra devuelve un código de estado HTTP 200 a menos que ocurra un error o
establezca uno explícitamente. Eso debería hacerle preguntarse si la prueba realmente
funciona o no. Rompamos temporalmente el código de la aplicación para asegurarnos de que
la prueba lo detecte:
informar fe de erratas • discutir
Machine Translated by Google
Manejo del éxito • 71
Siempre vea sus especificaciones fallar
Las pruebas, como el código de implementación, pueden contener errores, ¡pero no tenemos
pruebas para nuestras pruebas! Por lo tanto, verifique cada prueba haciéndola ponerse roja,
confirmando que falla de la manera esperada y haciéndola pasar.
05unitspecs/04/expense_tracker/app/
api.rb publicar '/gastos'
hacer estado 404
gasto = JSON.parse(solicitud.cuerpo.leer) resultado
= @ledger.record(gasto)
JSON.generate('expense_id' => resultado.expense_id) end
Ahora, volver a ejecutar la especificación falla como se esperaba:
$ bundle exec rspec spec/unit/app/api_spec.rb « truncado
»
Fallas:
1) ExpenseTracker::API POST /gastos cuando el gasto se registra con éxito responde con un
200 (OK)
Fallo/Error: expect(last_response.status).to eq(200)
esperado: 200
obtenido: 404
(comparado usando ==)
# ./spec/unit/app/api_spec.rb:41:in ̀bloque (4 niveles) en
<módulo:ExpenseTracker>'
Terminado en 0.03479 segundos (los archivos tardaron 0.14115 segundos en
cargarse) 4 ejemplos, 1 falla, 2 pendientes
Ejemplos fallidos:
rspec ./spec/unit/app/api_spec.rb:33 # ExpenseTracker::API POST /expenses cuando el gasto se
registra correctamente responde con un 200 (OK)
Aleatorizado con semilla 32399
Asegúrate de deshacer la aplicación antes de continuar. Una vez que haga eso, volverá a tener dos especificaciones
aprobadas. Ahora, podemos centrar nuestra atención en probar la mantenibilidad. Hay mucho código duplicado en
estos dos casos de prueba. Antes de pasar a la siguiente sección, considere cómo puede hacer que el código sea un
poco menos repetitivo.
informar fe de erratas • discutir
Machine Translated by Google
Capítulo 5. Pruebas aisladas: Especificaciones de la unidad • 72
Refactorizar mientras está verde
Es tentador comenzar a factorizar el código duplicado mientras aún está escribiendo
sus especificaciones. Evite esa tentación: primero apruebe sus especificaciones y luego
refactorice. De esa manera, puede usar sus especificaciones para verificar su
refactorización.
refactorización
Ambos casos de prueba tienen expresiones idénticas configurando el doble de prueba del libro mayor .
Puede eliminar la duplicación moviéndolos a un enlace anterior común .
Coloque el siguiente código justo dentro del primer bloque de contexto :
05unitspecs/05/expense_tracker/spec/unit/app/
api_spec.rb let(:gastos) { { 'algunos' => 'datos' } }
antes de hacer
permitir (libro mayor). recibir (: registro) .
con (gasto) .
y_devolver (Resultado del registro. nuevo (verdadero, 417, cero))
fin
Ahora, elimine el código de configuración de ambos ejemplos:
05unitspecs/ 05 /expense_tracker/spec/unit/app/
api_spec.rb ' devuelve el id de gasto'
publicar '/gastos', JSON.generate(gastos)
analizado = JSON.parse(last_response.body)
expect(analizado).to include('expense_id' => 417) end
' responde con un 200 (OK)' publicar '/
gastos', JSON.generar (gastos) esperar
(última_respuesta.estado) .to eq (200 ) fin
Estas especificaciones refactorizadas informan "solo los hechos" del comportamiento esperado. Cuando
llega una solicitud POST, el cuerpo debe contener el ID de gasto devuelto por nuestro libro mayor y el
código de respuesta debe ser 200.
Los ejemplos son un poco más SECOS ahora ("Don't Repeat Yourself", un enfoque explicado en el
libro The Pragmatic Programmer [HT00]). Puede ser tentador SECARlos aún más moviendo también
las líneas posteriores '/gastos'... al gancho anterior . Sin embargo, tal movimiento sería excesivo.
Poner la publicación en el anzuelo anterior probablemente se interpondrá en el camino de las
especificaciones futuras para este contexto. Por ejemplo, agregar soporte para clientes XML requeriría
un encabezado HTTP diferente. Si envía la solicitud HTTP desde su código de configuración, no podrá
modificar los encabezados solo para sus ejemplos XML.
informar fe de erratas • discutir
Machine Translated by Google
Manejo de fallas • 73
Mantenga la configuración y el código de prueba separados
Considere los tres pasos tradicionales de las pruebas: organizar, actuar y afirmar. Mover solo el
paso de organización (configuración) a un anzuelo anterior deja en claro qué es y qué no es
parte del comportamiento que está probando.
Todavía conserva la flexibilidad de agregar un código de configuración adicional para las
especificaciones individuales que lo necesitan.
Fallo de manejo
Ahora que sus especificaciones están probando el "camino feliz" de un gasto exitoso, centremos nuestra atención
en el caso de falla. Puede aplicar el conocimiento ganado con tanto esfuerzo de las secciones anteriores y
comenzar con un gancho anterior ya factorizado . Aquí, el código de configuración llenará el objeto de valor
RecordResult con un estado de éxito falso y un mensaje de error:
05unitspecs/06/expense_tracker/spec/unit/app/
api_spec.rb context 'cuando el gasto falla en la validación'
do let(:expense) { { 'algunos' => 'datos' } }
antes de hacer
allow(libro mayor).para
recibir(:registro) .con(gasto) .and_return(RecordResult.new(false, 417, 'Gasto incompleto'))
fin
' devuelve un mensaje de error' publique
' / gastos', JSON.generar (gastos)
analizado = JSON.parse(last_response.body)
expect(analizado).to include('error' => 'Gasto incompleto') end
' responde con un 422 (entidad no procesable)' publique '/gastos',
JSON.generar (gastos) esperar
(última_respuesta.estado) .to eq (422 ) fin
fin
Las expectativas en los ejemplos también son diferentes; están buscando un mensaje de error en el cuerpo y un
código de error HTTP.
Estas nuevas especificaciones fallarán cuando las ejecute, ya que el comportamiento aún no está implementado:
$ bundle exec rspec spec/unit/app/api_spec.rb « truncado
»
Fallas:
1) ExpenseTracker::API POST /expenses cuando el gasto falla en la validación devuelve un
mensaje de error
informar fe de erratas • discutir
Machine Translated by Google
Capítulo 5. Pruebas aisladas: Especificaciones de la unidad • 74
Incumplimiento/Error: esperar (analizado). para incluir ('error' => 'Gasto
incompleto')
esperado {"expense_id" => 417} para incluir {"error" => "Gasto incompleto"}
diferencia:
@@ 1,2 +1,2 @@
"error" => "Gasto incompleto",
+"expense_id" => 417,
# ./spec/unit/app/api_spec.rb:52:in ̀bloque (4 niveles) en
<módulo:ExpenseTracker>'
2) ExpenseTracker::API POST /gastos cuando el gasto falla en la validación responde con un
422 (entidad no procesable)
Fallo/Error: expect(last_response.status).to eq(422)
esperado: 422
obtenido: 200
(comparado usando ==)
# ./spec/unit/app/api_spec.rb:57:in ̀bloque (4 niveles) en
<módulo:ExpenseTracker>'
Terminado en 0.03552 segundos (los archivos tardaron 0.13746 segundos en
cargarse) 4 ejemplos, 2 fallas
Ejemplos fallidos:
rspec ./spec/unit/app/api_spec.rb:48 # ExpenseTracker::API POST /expenses cuando la validación
del gasto falla devuelve un mensaje de error rspec ./spec/unit/app/
api_spec.rb:55 # ExpenseTracker:: API POST /gastos cuando el gasto falla en la validación
responde con un 422 (entidad no procesable)
Aleatorizado con semilla 8222
¿La ruta POST necesita verificar el éxito? indicador de RecordResult y establezca
el estado HTTP y el cuerpo en consecuencia. Abre app/api.rb y cambia la ruta de
publicación '/gastos' a lo siguiente:
05unitspecs/07/expense_tracker/app/
api.rb publicar '/gastos' hacer
gasto = JSON.parse(solicitud.cuerpo.leer) resultado
= @ledger.record(gasto)
si resultado.éxito?
JSON.generate('expense_id' => result.expense_id) else
estado 422
JSON.generate('error' => resultado.mensaje_error) end
fin
informar fe de erratas • discutir
Machine Translated by Google
Definición del libro mayor • 75
Vuelva a ejecutar sus especificaciones y asegúrese de que pasen. Luego, regrese y eche un
vistazo a la última iteración de las especificaciones de su unidad. Observe cómo los dobles
de prueba definen la interfaz que debe proporcionar la clase Ledger . Antes de terminar este
capítulo, esbocemos la clase Ledger .
Definición del libro mayor
Comenzaremos con una clase Ledger vacía . En el directorio de su proyecto, cree un nuevo
archivo llamado app/ledger.rb con los siguientes contenidos:
05unitspecs/08/expense_tracker/app/
ledger.rb módulo ExpenseTracker
RecordResult = Struct.new(:¿éxito?, :expense_id, :error_message)
final del libro mayor de
clase
fin
Tenga en cuenta que también hemos movido la definición de estructura temporal RecordResult
desde antes a su hogar permanente aquí. No olvide eliminar la versión anterior de
RecordResult de spec/unit/app/api_spec.rb.
Deberá solicitar este nuevo archivo de app/api.rb:
05unitspecs/08/expense_tracker/app/
api.rb require_relative 'libro mayor'
Las especificaciones que tiene hasta ahora todavía están usando el libro mayor falso. Pasaron
sin una clase Ledger real definida, por lo que es de esperar que sigan pasando ahora que
tenemos una. Sigue adelante e inténtalo:
$ bundle exec rspec spec/unit/app/api_spec.rb « truncado
»
Fallas:
1) ExpenseTracker::API POST /expenses cuando el gasto se registra correctamente devuelve
la identificación del gasto
Falla/Error:
permitir (libro mayor). recibir (: registro) .
con (gasto) .
y_retorno (Resultado del registro. nuevo (verdadero, 417, cero))
la clase ExpenseTracker::Ledger no implementa el método de instancia: registro
# ./spec/unit/app/api_spec.rb:19:in ̀bloque (4 niveles) en
<módulo:ExpenseTracker>'
« truncado »
Terminado en 0.00783 segundos (los archivos tardaron 0.13142 segundos en
cargarse) 4 ejemplos, 4 fallas
informar fe de erratas • discutir
Machine Translated by Google
Capítulo 5. Pruebas aisladas: Especificaciones de la unidad • 76
Ejemplos fallidos:
rspec ./spec/unit/app/api_spec.rb:24 # ExpenseTracker::API POST /expenses cuando el gasto se
registra correctamente devuelve el ID de gasto rspec ./spec/unit/app/api_spec.rb:31
# ExpenseTracker: :API POST /expenses cuando el gasto se registra correctamente responde con
un 200 (OK) rspec ./spec/unit/app/api_spec.rb:53 # ExpenseTracker::API POST /
expenses cuando el gasto falla en la validación responde con un 422 (entidad no procesable)
rspec ./spec/unit/app/api_spec.rb:46 # ExpenseTracker::API POST /expenses cuando el gasto
falla en la validación devuelve un mensaje de error
Aleatorizado con semilla 40684
Observe la queja: la clase ExpenseTracker::Ledger no implementa el método de instancia: registro.
Las especificaciones están fallando porque la clase Ledger real no se parece lo suficiente a la falsa.
Acaba de encontrar una característica de RSpec llamada verificación de dobles. Ayudan a evitar
simulacros frágiles, un problema en el que las especificaciones pasan cuando deberían estar fallando.
Recuerde que los dobles de prueba imitan la interfaz de un objeto real. Cuando los nombres de los
métodos o los parámetros del objeto real cambian, un doble de prueba tradicional seguirá
respondiendo a los métodos antiguos. Es fácil olvidarse de actualizar sus especificaciones cuando esto sucede.
Los dobles verificadores de RSpec en realidad inspeccionan el objeto real que están representando
y fallan la prueba si las firmas del método no coinciden. Aprenderá más sobre ellos en Verificación
de dobles, en la página 243.
Sigamos la guía del mensaje de error y agreguemos un método de registro vacío a Ledger:
05unitspecs/09/expense_tracker/app/ledger.rb
registro de definición
fin
Ahora, vuelva a ejecutar la especificación:
$ bundle exec rspec spec/unit/app/api_spec.rb « truncado
»
Fallas:
1) ExpenseTracker::API POST /gastos cuando el gasto se registra con éxito responde con un
200 (OK)
Falla/Error:
permitir (libro mayor). recibir (: registro) .
con (gasto) .
y_retorno (Resultado del registro. nuevo (verdadero, 417, cero))
Número incorrecto de argumentos. Se esperaba 0, se
obtuvo 1. # ./spec/unit/app/api_spec.rb:19:in ̀bloque (4 niveles) en
<module:ExpenseTracker>'
informar fe de erratas • discutir
Machine Translated by Google
Tu turno • 77
« truncado »
Terminado en 0.00762 segundos (los archivos tardaron 0.12669 segundos en
cargarse) 4 ejemplos, 4 fallas
Ejemplos fallidos:
rspec ./spec/unit/app/api_spec.rb:31 # ExpenseTracker::API POST /expenses cuando el gasto se
registra correctamente responde con 200 (OK) rspec ./spec/unit/app/api_spec.rb:24 #
ExpenseTracker::API POST /expenses cuando el gasto se registra correctamente devuelve el id
del gasto rspec ./spec/unit/app/api_spec.rb:53 # ExpenseTracker::API POST /
expenses cuando el gasto falla en la validación responde con un 422 (entidad no procesable)
rspec ./spec/unit/app/api_spec.rb:46 # ExpenseTracker::API POST /expenses cuando el gasto
falla en la validación devuelve un mensaje de error
Aleatorizado con semilla 8060
El mensaje de error ha cambiado a Número incorrecto de argumentos. Esperaba 0, obtuve 1.
RSpec ve el nuevo método de registro , pero señala que no toma un argumento como lo hace el
doble de prueba.
Haga que las firmas del método coincidan agregando un parámetro:
05unitspecs/10/expense_tracker/app/
ledger.rb def registro
(gasto) fin
Cuando vuelva a ejecutar las especificaciones, deberían pasar.
$ bundle exec rspec spec/unit/app/api_spec.rb « truncado
»
Terminado en 0.0266 segundos (los archivos tardaron 0.13459 segundos en
cargarse) 4 ejemplos, 0 fallas
Aleatorizado con semilla 13686
Un objeto simulado que guía la interfaz por uno real. ¿Qué tal eso para la vida imitando el arte?
Ahora que tiene especificaciones ecológicas, comprométase con su trabajo y tómese un
merecido descanso. En el próximo capítulo, completará el comportamiento detrás de la interfaz.
Tu turno
En este capítulo, ha aclarado exactamente cómo se supone que debe comportarse su API.
Ha explicado en detalle lo que sucede cuando el almacenamiento de un registro de gastos tiene
éxito o falla, usando un doble de prueba para reemplazar la capa de persistencia no escrita.
Ha refactorizado sus especificaciones para que expliquen exactamente qué comportamiento está
probando. Finalmente, usó el diseño sugerido por sus pruebas para guiar la interfaz de un objeto
real.
informar fe de erratas • discutir
Machine Translated by Google
Capítulo 5. Pruebas aisladas: Especificaciones de la unidad • 78
Ahora es el momento de poner a prueba estas nuevas habilidades. No te saltes este; usted se
basará en este trabajo en el próximo capítulo.
Ejercicios
En estos ejercicios, implementará otra pieza clave de su aplicación en la capa de enrutamiento.
En el camino, buscará oportunidades para compartir código.
Reducción de la
búsqueda de duplicación a través de las especificaciones de su unidad para JSON.parse; verá
varios fragmentos que se ven así:
05unitspecs/11/expense_tracker/spec/unit/api_example_spec.rb
parsed = JSON.parse(last_response.body)
expect(parsed).to do_something
Tomamos repetidamente last_response de Rack::Test y luego lo analizamos desde JSON.
Encuentre una manera de reducir esta lógica duplicada.
Implementación de la ruta
GET En este capítulo, creamos juntos una especificación para la ruta POST. Ahora
es el momento de construir uno para la ruta GET y hacerlo pasar. Esto será mucho
más fácil que la versión POST; todas las piezas ya están en su lugar.
El objetivo es poder acceder a una URL que contenga una fecha:
obtener '/gastos/20170612'
…y recuperar los datos JSON que contienen los gastos registrados ese día.
Primero, agregue el siguiente esquema a spec/unit/app/api_spec.rb, dentro del bloque API
RSpec.describe :
05unitspecs/11/expense_tracker/spec/unit/app/api_spec.rb
describe 'GET /expenses/:date' do
context 'cuando existen gastos en la fecha dada' ' devuelve los
registros de gastos como JSON' ' responde con un
200 (OK)' end
context 'cuando no hay gastos en la fecha dada' hazlo 'devuelve una matriz
vacía como JSON' ' responde con un final de
200 (OK)'
fin
informar fe de erratas • discutir
Machine Translated by Google
Tu Turno • 79
Complete el cuerpo de cada ejemplo uno por uno. Deberá considerar las siguientes
preguntas:
• ¿Qué dobles de prueba u otros objetos necesitará configurar?
• ¿Cómo será el JSON esperado cuando haya gastos registrados?
en la fecha indicada?
• ¿Qué datos subyacentes necesitarán proporcionar sus duplicados de prueba para que
pueda devolver el JSON correcto?
• ¿Cómo cambian las respuestas a las dos preguntas anteriores cuando hay
no hay gastos en el libro mayor para esa fecha?
Sugerencia: su clase Libro mayor necesitará un método que devuelva los gastos en una
fecha específica. Sugerimos el nombre expensas_on para este método; los ejemplos en el
próximo capítulo usarán ese nombre. (No necesitará completar el cuerpo de expensas_en
hasta el próximo capítulo).
Mire el trabajo que ya ha hecho en este capítulo para obtener ideas sobre cómo estructurar
su código de configuración y sus expectativas. Si se queda atascado, eche un vistazo al
código fuente de este capítulo. Pero haz tu mejor esfuerzo para terminar este ejercicio. El
siguiente capítulo se basará en la implementación de la ruta GET que escribió para este
ejercicio.
informar fe de erratas • discutir
Machine Translated by Google
En este capítulo, verá:
• Cómo configurar una base de datos para realizar pruebas, sin dañar su
datos reales
• Técnicas para organizar código de configuración compartido y global • Cómo usar
metadatos para controlar cómo RSpec ejecuta ciertas especificaciones • Cómo
diagnosticar una dependencia de orden entre sus especificaciones
CAPÍTULO 6
Seamos realistas: especificaciones de integración
A estas alturas, tiene una sólida capa de enrutamiento HTTP diseñada con la ayuda de las
especificaciones de la unidad. Para escribir estas especificaciones de la unidad, aisló el código bajo
prueba. Sus especificaciones asumieron que las dependencias subyacentes eventualmente se
implementarían y proporcionaron dobles de prueba: versiones falsas para la prueba.
Ahora es el momento de escribir esas dependencias de verdad. En este capítulo, implementará la
clase Ledger como la capa inferior de la aplicación. Escribirá código para almacenar registros de
gastos en una base de datos. Creará poderosas especificaciones de integración para asegurarse de
que los datos realmente se almacenen:
HTTP Código de enrutamiento
Pedido
HTTP (sus rutas de Sinatra)
Lógica de gastos
(tu código Ruby) Las especificaciones
de integración
ejercitan capas junto
con sus dependencias
Adaptador
(Continuación)
Base de datos
(SQLite)
Al final del capítulo, no solo funcionarán sus especificaciones de integración, sino también las
especificaciones de aceptación de extremo a extremo con las que comenzó este proyecto.
informar fe de erratas • discutir
Machine Translated by Google
Capítulo 6. Realización: Especificaciones de integración • 82
Conexión de la base de datos
Hemos aplazado la implementación de la capa de la base de datos todo lo que hemos
podido. Pero con el resto de las capas circundantes definidas, es todo lo que queda.
Conociendo Secuela
Para este ejercicio, utilizará una biblioteca de base de datos de Ruby llamada Sequel.
Sequel le permite crear tablas, agregar datos, etc., sin vincular su código a ningún producto
de base de datos específico. Sin embargo, aún tendrá que elegir una base de datos y, para
este proyecto, la biblioteca SQLite de bajo mantenimiento funcionará bien.1
Continúe y agregue las siguientes dos líneas a su Gemfile:
06integrationspecs/01/expense_tracker/Gemfile
gema 'secuela', '4.48.0' gema
'sqlite3', '1.3.13'
Ahora, vuelva a ejecutar Bundler para instalar las nuevas bibliotecas:
$ instalación del
paquete Obtención de metadatos de gemas de https://rubygems.org/.........
Obteniendo metadatos de la versión de https://rubygems.org/..
Resolviendo dependencias...
Uso de bundler 1.15.3 Uso
de coderay 1.1.1 Uso de
difflcs 1.3 Uso de
mustermann 1.0.0 Uso de
rack 2.0.3 Uso de
rspecsupport 3.6.0 Obtención de
sequel 4.48.0 Instalación de
sequel 4.48.0 Uso de tilt 2.0.7
Obtención de sqlite3
1.3 .13 Instalación de sqlite3
1.3.13 con extensiones nativas Uso de rackprotection 2.0.0 Uso
de racktest 0.7.0 Uso de rspeccore
3.6.0 Uso de rspec
expectations 3.6.0 Uso de
rspecmocks 3.6.0 Uso de sinatra 2.0.0
Usando rspec 3.6.0 ¡Paquete
completo! 6 dependencias
Gemfile, 16 gemas
ahora instaladas.
Use ̀bundle info [gemname]` para ver dónde está instalada una gema incluida.
Una vez que Sequel y SQLite estén instalados, estará listo para usarlos. En nuestra
aplicación, eventualmente hará lo siguiente:
1. https://sqlite.org
informar fe de erratas • discutir
Machine Translated by Google
Conexión de la base de datos • 83
• Cargar la biblioteca de Sequel
• Crear una conexión de base de datos
• Crear una tabla de gastos para que tengamos un lugar para almacenar nuestros
registros • Insertar registros en la tabla de
gastos • Consultar registros de la tabla de gastos
Esa es una gran lista de tareas, pero las API para realizar estos pasos son simples. De hecho,
el primer ejemplo de la página de inicio de Sequel proporciona un fragmento de código corto
que muestra cómo hacer todo esto .
>> requiere 'secuela'
=> cierto
>> DB = Sequel.sqlite =>
#<Sequel::SQLite::Base de datos: {:adapter=>:sqlite}> >>
DB.create_table(:gems) { String :name } => nil
>> DB[:gemas].insertar(nombre: 'rspec') => 1
>> DB[:gemas].insertar(nombre: 'sinatra') => 2
>> DB[:gemas].all =>
[{:nombre=>"rspec"}, {:nombre=>"sinatra"}]
Ahora que está familiarizado con las API, está listo para crear algunas bases de datos.
Crear una base de datos
Querrás crear bases de datos SQLite separadas para pruebas, desarrollo y producción (para
que no golpees tus datos reales durante las pruebas).
Una variable de entorno es la forma más fácil de configurar bases de datos separadas para
esta aplicación. En Primeros pasos, en la página 47, usó la variable RACK_ENV para indicar
en qué entorno se estaba ejecutando el código. Aprovechemos ese trabajo. Cree un nuevo
archivo llamado config/sequel.rb con el siguiente código:
06integrationspecs/03/expense_tracker/config/sequel.rb
requiere 'secuela'
DB = Sequel.sqlite("./db/#{ENV.fetch('RACK_ENV', 'desarrollo')}.db")
Esta configuración creará un archivo de base de datos como db/test.db o db/production.db
según la variable de entorno RACK_ENV . Tenga en cuenta que está asignando la conexión
de la base de datos a una constante de base de datos de nivel superior; esta es la convención
de Sequel cuando solo hay una base de datos global.3
2. http://sequel.jeremyevans.net
3. http://sequel.jeremyevans.net/rdoc/files/doc/opening_databases_rdoc.html
informar fe de erratas • discutir
Machine Translated by Google
Capítulo 6. Realización: Especificaciones de integración • 84
Con esta configuración en su lugar, no tiene que preocuparse por sobrescribir accidentalmente sus datos
de producción durante la prueba.
Ahora es el momento de pensar en la estructura de sus datos. Cada elemento de gasto necesitará varios
datos:
• Una identificación
única • Nombre del
beneficiario • Cantidad
• Fecha
Utilizará una migración de Sequel para crear la estructura de la tabla que contiene esta información.4
Puede colocar los archivos de migración en cualquier lugar, pero una convención común es mantenerlos
en db/migrations. Agregue el siguiente código a db/migrations/0001_create_expenses.rb:
06integrationspecs/03/expense_tracker / db/migrations/0001_create_expenses.rb
Sequel.migration hacer
cambios
crear_tabla :gastos hacer
clave_principal :id
Cadena :beneficiario Flotante :cantidad Fecha :fecha
fin
fin
fin
Para aplicar esta migración a su base de datos, deberá indicarle a Sequel que la ejecute.
En un minuto, configurará RSpec para que lo haga automáticamente cada vez que ejecute sus pruebas de
integración. Por ahora, intente ejecutar esta migración en la base de datos de desarrollo. Para hacerlo,
puede usar el comando sequel que se incluye con la biblioteca Sequel:
$ bundle exec sequel m ./db/migrations sqlite://db/development.db echo « truncado »
I, [20170613T13:34:25.536511 #14630] INFO : Terminé de aplicar la versión de
migración 1, dirección: arriba, tomó 0.001514 segundos
Ahora que configuró Sequel, es hora de escribir algunas especificaciones.
Prueba del comportamiento del libro mayor
Hemos visto cómo ejecutar las migraciones de Sequel manualmente desde la línea de comandos.
Deberá configurar RSpec para ejecutarlos automáticamente, de modo que la estructura de la base de datos
esté lista antes de que se ejecute la primera especificación de integración.
4. http://sequel.jeremyevans.net/rdoc/files/doc/migration_rdoc.html
informar fe de erratas • discutir
Machine Translated by Google
Prueba del comportamiento del libro mayor • 85
El siguiente código se asegurará de que la estructura de la base de datos esté configurada y vacía, lista para que sus
especificaciones le agreguen datos:
Sequel.extension :migration
Sequel::Migrator.run(DB, 'db/migrations')
DB[:gastos].truncar
Primero, ejecutamos todos los archivos de migración para asegurarnos de que todas las tablas de la base de datos existan
con su esquema actual. Luego, eliminamos cualquier dato de prueba sobrante de la tabla usando el método de truncado . De
esa forma, cada ejecución del paquete de especificaciones comienza con una base de datos limpia, sin importar lo que haya
sucedido antes.
El único problema es que no es obvio dónde poner estas líneas. Hasta este punto, ha tendido a mantener rutinas de
configuración como esta en uno de dos lugares:
• La parte superior de un único archivo de
especificaciones • El archivo global spec_helper.rb
Las migraciones de bases de datos se encuentran en algún lugar entre estos dos extremos. Queremos cargarlos para
cualquier especificación que toque la base de datos, pero no para las especificaciones de la unidad; esas deben permanecer
rápidas, incluso cuando las migraciones de nuestra base de datos crecen durante la vida útil de la aplicación.
La convención RSpec para este tipo de código "parcialmente compartido" es ponerlo en una carpeta llamada spec/support;
luego podemos cargarlo desde cualquier archivo de especificaciones que lo necesite.
Cree un nuevo archivo llamado spec/support/db.rb con el siguiente contenido:
06integrationspecs/03/expense_tracker/spec/support/
db.rb RSpec.configure do |
c| c.before(:suite) do
Sequel.extension :migration
Sequel::Migrator.run(DB, 'db/migrations')
DB[:gastos].truncate end end
Este fragmento define un gancho a nivel de suite. Nos encontramos por primera vez antes de los ganchos en Hooks, en la
página 9. Un gancho típico se ejecutará antes de cada ejemplo. Este se ejecutará solo una vez: después de que se hayan
cargado todas las especificaciones, pero antes de que se ejecute el primero . Para eso están los hooks before(:suite) .
Arranque su entorno para pruebas sencillas
Su conjunto de especificaciones debería configurar la base de datos de prueba por usted, en lugar de
requerir que ejecute una tarea de configuración separada. Las personas que prueban su código
(¡incluido usted!) pueden olvidarse fácilmente de ejecutar el paso adicional, o es posible que ni siquiera
sepan que lo necesitan.
informar fe de erratas • discutir
Machine Translated by Google
Capítulo 6. Realización: Especificaciones de integración • 86
Ahora que hemos definido nuestro enlace, estamos listos para definir nuestra especificación y
cargar el archivo de soporte. Cree un archivo llamado spec/integration/app/ledger_spec.rb con
el siguiente código:
06integrationspecs/03/expense_tracker/spec/integration/app/ledger_spec.rb
require_relative '../../../app/ledger' require_relative
'../../../config/sequel' require_relative '../../soporte/db'
módulo ExpenseTracker
RSpec.describe Ledger do
let(:libro mayor) { Libro
mayor.nuevo } let(:gastos) hacer
{
'beneficiario' => 'Starbucks',
'cantidad' => 5,75,
'fecha' => '20170610'
} fin
describir '#record' do # ...
contextos ir aquí ... fin
fin
fin
La configuración de :ledger y :expense será la misma para cada ejemplo, así que hemos usado
let para inicializar estos datos.
Ahora es el momento de explicar el comportamiento que queremos en el primer ejemplo.
Queremos decirle al libro mayor que ahorre el gasto, y luego leer la base de datos del disco y
asegurarnos de que el gasto realmente se ahorró:
06integrationspecs/03/expense_tracker/spec/integration/app/
ledger_spec.rb contexto 'con un gasto válido' hacer
' guarda con éxito el gasto en la base de datos' do result =
ledger.record(expense)
esperar ( resultado). ser_éxito esperar
( DB [ : gastos ] . todos) . 10') )] fin
fin
Estamos usando un par de nuevos emparejadores aquí. El primero, be_success, simplemente
verifica ese resultado. es verdad. Este comparador está integrado en RSpec; aprenderá más al
respecto en Predicados dinámicos, en la página 194.
informar fe de erratas • discutir
Machine Translated by Google
Comprobación del comportamiento del libro mayor • 87
El segundo comparador, emparejar [a_hash_incluyendo(...)], espera que nuestra aplicación
devuelva datos que coincidan con una determinada estructura; en este caso, una matriz de
hashes de un elemento con ciertas claves y valores. Esta expresión es otro uso de los
emparejadores componibles de RSpec ; aquí, estás pasando el comparador a_hash_incluyendo al partido uno.
Nos estamos desviando un poco de la práctica general de TDD en este fragmento. Normalmente,
cada ejemplo solo tendría una expectativa; de lo contrario, una falla puede enmascarar otra.
Aquí, tenemos dos expectativas en el mismo ejemplo.
Hay un poco de compensación a considerar aquí. Cualquier especificación que toque la base de
datos será más lenta, particularmente en sus pasos de configuración y desmontaje. Si seguimos
"una expectativa por ejemplo" demasiado rigurosamente, vamos a estar repitiendo esa
configuración y desmontaje muchas veces. Al combinar juiciosamente un par de afirmaciones,
mantenemos nuestra suite rápida.
Veamos a qué estamos renunciando para obtener este aumento de rendimiento. Siga adelante y
ejecuta tus especificaciones:
$ paquete exec rspec spec/integration/app/ledger_spec.rb « truncado »
Fallas:
1) ExpenseTracker::Ledger#record con un gasto válido guarda con éxito el gasto en la base de
datos
Fallo/Error: expect(result).to be_success esperado nil
para responder a ̀success?`
# ./spec/integration/app/ledger_spec.rb:23:in ̀bloque (4 niveles) en
<módulo:ExpenseTracker>'
Terminado en 0,02211 segundos (los archivos tardaron 0,15418 segundos en
cargarse) 1 ejemplo, 1 error
Ejemplos fallidos:
rspec ./spec/integration/app/ledger_spec.rb:20 #
ExpenseTracker::Ledger#record con un gasto válido guarda correctamente el gasto en la base de
datos
Aleatorizado con semilla 27984
Como era de esperar, la especificación falló en la primera afirmación. Ni siquiera vemos la
segunda aserción porque, de forma predeterminada, RSpec aborta la prueba en el primer error.
Sería bueno registrar el primer fracaso, pero seguir intentando la segunda expectativa. ¡Buenas
noticias! La etiqueta :aggregate_failures hace precisamente eso. Cambie su declaración de
ejemplo a lo siguiente:
06integrationspecs/04/expense_tracker/spec/integration/app/
ledger_spec.rb ' guarda con éxito el gasto en la base de datos', :aggregate_failures do
informar fe de erratas • discutir
Machine Translated by Google
Capítulo 6. Realización: Especificaciones de integración • 88
Ahora, la salida de RSpec muestra ambas fallas debajo de una descripción del ejemplo:
$ paquete exec rspec spec/integration/app/ledger_spec.rb « truncado »
Fallas:
1) ExpenseTracker::Ledger#record con un gasto válido guarda con éxito el gasto en la base de
datos
Obtuve 1 falla y 1 otro error:
1.1) Fracaso/Error: esperar (resultado). ser_éxito
se esperaba que nada respondiera a ̀success?`
# ./spec/integration/app/ledger_spec.rb:23:in ̀bloque (4 niveles) en
<módulo:ExpenseTracker>'
1.2) Falla/Error: id: result.expense_id,
Sin error de método:
método indefinido 'expense_id' para nil:NilClass
# ./spec/integration/app/ledger_spec.rb:25:in ̀bloque (4 niveles) en
<módulo:ExpenseTracker>'
Finalizó en 0,02142 segundos (los archivos tardaron 0,15952 segundos en cargarse)
1 ejemplo, 1 error
Ejemplos fallidos:
rspec ./spec/integration/app/ledger_spec.rb:20 #
ExpenseTracker::Ledger#record con un gasto válido guarda correctamente el gasto en la base de
datos
Aleatorizado con semilla 41929
Es probable que queramos que nuestras otras pruebas de integración obtengan este beneficio.
Mueva la propiedad :aggregate_failures un nivel hacia arriba del ejemplo al grupo:
06integrationspecs/05/expense_tracker/spec/integration/app/
ledger_spec.rb RSpec.describe Ledger, :aggregate_failures do
RSpec se refiere a estas propiedades de las especificaciones como metadatos. Cuando define
metadatos (símbolos arbitrarios o hashes) en un grupo de ejemplo, todos los ejemplos de ese
grupo lo heredan, al igual que cualquier grupo anidado. Anteriormente vimos un ejemplo de
metadatos especificados como un hash en Filtrado de etiquetas, en la página 25. Aquí estamos
definiendo nuestros metadatos usando solo un símbolo como acceso directo.
Internamente, RSpec expande esto en un hash como {agreged_failures: true }.
Ahora, es el momento de completar el comportamiento. El método de registro de la clase Ledger
necesita almacenar el gasto en la base de datos y luego devolver un RecordResult que indique
lo que sucedió:
06integrationspecs/06/expense_tracker/app/
ledger.rb def registro (gastos)
informar fe de erratas • discutir
Machine Translated by Google
Prueba del caso no válido • 89
DB[:gastos].insertar(gastos) id =
DB[:gastos].max(:id)
RecordResult.new(verdadero, id, nil) fin
Cuando vuelva a ejecutar RSpec, su especificación de integración debería pasar ahora.
Prueba del caso no válido
Hasta ahora, nuestra especificación de integración verifica solo el "camino feliz" de ahorrar un gasto válido.
Agreguemos una segunda especificación que pruebe un gasto al que le falta un beneficiario:
06integrationspecs/07/expense_tracker/spec/integration/app/ledger_spec.rb
context 'cuando el gasto no tiene un beneficiario' hazlo
'rechaza el gasto como no válido' haz
gasto.delete('beneficiario')
resultado = libro mayor.record(gasto)
expect(result).not_to be_success
expect(result.expense_id).to eq(nil)
expect(result.error_message).to include('`beneficiario` es obligatorio')
expect(DB[:gastos].cuenta).to eq(0) end
fin
Aquí, la instancia de Ledger debe devolver el mensaje y el estado de falla correctos, y la base de datos no debe
tener gastos.
Dado que este comportamiento no está implementado, queremos que esta nueva especificación falle:
$ paquete exec rspec spec/integration/app/ledger_spec.rb « truncado »
Fallas:
1) ExpenseTracker::Ledger#record cuando el gasto carece de un beneficiario rechaza el gasto
como no válido
« truncado »
2) ExpenseTracker::Ledger#record con un gasto válido guarda con éxito el gasto en la base de
datos
« truncado »
Terminado en 0.02597 segundos (los archivos tardaron 0.18213 segundos en
cargarse) 2 ejemplos, 2 fallas
Ejemplos fallidos:
rspec ./spec/integration/app/ledger_spec.rb:34 #
ExpenseTracker::Ledger#record cuando al gasto le falta un beneficiario rechaza el gasto como no
válido
informar fe de erratas • discutir
Machine Translated by Google
Capítulo 6. Realización: Especificaciones de integración • 90
rspec ./spec/integration/app/ledger_spec.rb:20 #
ExpenseTracker::Ledger#record con un gasto válido guarda correctamente el gasto en la base de datos
Aleatorizado con semilla 57045
Eso es extraño. Ambas especificaciones fallaron. ¿Rompimos alguna otra especificación además
de las dos que estamos ejecutando aquí? Intente volver a ejecutar todo el paquete varias veces.
Su salida exacta diferirá de la nuestra; es posible que vea dos fallas o solo una. Aquí hay una
prueba con dos fallas:
$ paquete exec rspec «
truncado »
Terminado en 0.06926 segundos (los archivos tardaron 0.21812 segundos en cargarse)
11 ejemplos, 2 fallas, 1 pendiente
Ejemplos fallidos:
rspec ./spec/integration/app/ledger_spec.rb:20 #
ExpenseTracker::Ledger#record con un gasto válido guarda correctamente el gasto en la base de datos
rspec ./spec/integration/
app/ledger_spec.rb:34 # ExpenseTracker: :Ledger#record cuando
al gasto le falta un beneficiario rechaza el gasto como inválido
Aleatorizado con semilla 32043
El resultado es ligeramente diferente cada vez que lo ejecuta, porque RSpec ejecuta las
especificaciones en orden aleatorio. Esta técnica, habilitada a través de la línea config.order
= :random en spec_helper.rb, es útil para encontrar dependencias de orden; es decir,
especificaciones cuyo comportamiento depende de cuál se ejecute primero.
Prueba en orden aleatorio para encontrar dependencias de orden
Si sus especificaciones se ejecutan en el mismo orden cada vez, es posible que
tenga una que solo está pasando porque una anterior, rota, dejó algún estado atrás.
Utilice la opción de ordenación aleatoria de su conjunto de pruebas para mostrar
estas dependencias.
Una semilla aleatoria le da un orden de prueba específico y repetible. Puede reproducir cualquier
secuencia de este tipo pasando la opción seed a RSpec junto con el número de semilla informado
en la salida.
Por ejemplo, la salida de RSpec nos dice que la ejecución anterior estaba usando la semilla 32043.
Puede reproducir ese orden de prueba específico en cualquier momento que desee:
$ bundle exec rspec seed 32043 «
truncado »
Terminado en 0.05941 segundos (los archivos tardaron 0.22746 segundos en cargarse)
11 ejemplos, 2 fallas, 1 pendiente
informar fe de erratas • discutir
Machine Translated by Google
Prueba del caso no válido • 91
Ejemplos fallidos:
rspec ./spec/integration/app/ledger_spec.rb:20 #
ExpenseTracker::Ledger#record con un gasto válido guarda correctamente el gasto en la base de
datos rspec ./spec/
integration/app/ledger_spec.rb:34 # ExpenseTracker: :Ledger#record
cuando al gasto le falta un beneficiario rechaza el gasto como inválido
Aleatorizado con semilla 32043
Si bien RSpec no puede decirle por qué ocurre la dependencia de pedidos, sin duda puede ayudarlo a identificar
qué especificaciones necesita ejecutar para reproducirlas.
Con la opción bisect , RSpec ejecutará sistemáticamente diferentes partes de su suite hasta que encuentre el
conjunto más pequeño que desencadene una falla:
$ bundle exec rspec bisect seed 32043 Bisect
comenzó a usar opciones: "seed 32043"
Ejecutando suite para encontrar fallas... (0.45293 segundos)
Comenzando bisect con 2 ejemplos fallidos y 9 ejemplos no fallidos.
Comprobando que las fallas dependen del pedido... la falla parece depender del pedido
Ronda 1: dividir en dos los ejemplos que no fallan 19. ignorando los ejemplos 15 (0.45132
segundos)
Ronda 2: dividir en dos los ejemplos que no fallan 69 . ignorando ejemplos 67 (0.43739 segundos)
Ronda 3: dividir en dos los ejemplos que no fallan 89 . ignorando el ejemplo 8 (0.43102
segundos)
¡Bisección completa! Se redujeron los ejemplos necesarios que no fallan de 9 a 1 en 1,64 segundos.
El comando de reproducción mínima es:
rspec './spec/aceptación/expense_tracker_api_spec.rb[1:1]' './spec/integration/
app/ledger_spec.rb[1:1:1:1,1:1:2:1]' seed 32043
RSpec nos ha dado un conjunto mínimo de especificaciones que podemos ejecutar en cualquier momento para
ver la falla:
$ bundle exec rspec './spec/acceptance/expense_tracker_api_spec.rb[1:1]' './spec/integration/app/
ledger_spec.rb[1:1:1:1,1:1:2:1] ' seed 32043 Opciones de ejecución: incluir {:ids=>{"./spec/
acceptance/
expense_tracker_api_spec.rb"=>["1:1"], "./spec/integration/app/ledger_spec.rb"=
>["1:1:1:1", "1:1:2:1"]}}
Aleatorizado con semilla 32043
*FF
« truncado »
Terminado en 0.0485 segundos (los archivos tardaron 0.21859 segundos en
cargarse) 3 ejemplos, 2 fallas, 1 pendiente
informar fe de erratas • discutir
Machine Translated by Google
Capítulo 6. Realización: Especificaciones de integración • 92
Ejemplos fallidos:
rspec ./spec/integration/app/ledger_spec.rb:20 #
ExpenseTracker::Ledger#record con un gasto válido guarda correctamente el gasto en la base de
datos rspec ./spec/
integration/app/ledger_spec.rb:34 #
ExpenseTracker: :Ledger#record cuando al gasto le falta un beneficiario rechaza el gasto como
inválido
Aleatorizado con semilla 32043
Los números entre corchetes se denominan ID de ejemplo; indican la posición de cada ejemplo en su
archivo, en relación con otros ejemplos y grupos anidados.
Por ejemplo, some_spec.rb[2:3] significaría "el tercer ejemplo dentro del segundo grupo en some_spec.rb".
Puede pegar estos valores en su terminal tal como lo hizo con los números de línea en Ejecución de fallas
específicas, en la página 21.
Con este conjunto de especificaciones, podemos ver que la nueva especificación hace que la especificación
anterior falle si la nueva se ejecuta primero. Las escrituras de nuestra base de datos se filtran entre pruebas.
Esté atento a la interacción de prueba en las especificaciones de integración
Sus especificaciones de integración interactúan con recursos externos: el sistema de
archivos, la base de datos, la red, etc. Debido a que estos recursos se comparten, debe
tener especial cuidado para restaurar el sistema a un borrón y cuenta nueva después de
cada especificación.
Aislamiento de sus especificaciones mediante transacciones de base de datos
Para resolver este problema, envolveremos cada especificación en una transacción de base de datos.
Después de que se ejecute cada ejemplo, queremos que RSpec revierta la transacción, cancelando cualquier
escritura que haya ocurrido y dejando la base de datos en un estado limpio. Sequel proporciona un método
fácil de probar para envolver el código en las transacciones.5
Un gancho RSpec alrededor sería perfecto para esta tarea. Ya tiene un archivo spec/support/db.rb para el
código de soporte de la base de datos, así que agréguelo allí, dentro del bloque RSpec.configure :
06integrationspecs/07/expense_tracker/spec/support/
db.rb c.around(:ejemplo, :db) do |ejemplo|
DB.transaction(rollback: :siempre) { ejemplo.ejecutar } fin
La secuencia de llamadas en el nuevo gancho es un poco retorcida, así que tenga paciencia con nosotros
por un segundo. Para cada ejemplo marcado como que requiere la base de datos (a través de la etiqueta :db ;
verá cómo usar esto en un momento), suceden los siguientes eventos:
5. http://sequel.jeremyevans.net/rdoc/files/doc/testing_rdoc.html
informar fe de erratas • discutir
Machine Translated by Google
Aislamiento de sus especificaciones usando transacciones de base de datos • 93
1. RSpec llama a nuestro anzuelo alrededor , pasándole el ejemplo que estamos ejecutando.
2. Dentro del gancho, le decimos a Sequel que inicie una nueva transacción en la base de datos.
3. Sequel llama al bloque interno, en el que le decimos a RSpec que ejecute el ejemplo.
4. El cuerpo del ejemplo termina de ejecutarse.
5. Sequel revierte la transacción, eliminando cualquier cambio que hayamos hecho en
la base de datos.
6. El gancho alrededor termina y RSpec pasa al siguiente ejemplo.
Solo queremos pasar tiempo configurando la base de datos cuando realmente la usamos.
Aunque iniciar y revertir una transacción toma menos de una décima de segundo, eso empequeñece el
tiempo de ejecución de nuestras especificaciones de unidades rápidas y enfocadas.
Aquí es donde entra en juego la noción de etiquetado . Una etiqueta es una pieza de metadatos
(información personalizada) adjunta a un ejemplo o grupo. Aquí, utilizará un nuevo símbolo, :db, para
indicar que un ejemplo toca la base de datos:
06integrationspecs/07/expense_tracker/spec/integration/an_integration_spec.rb
require_relative '../support/db'
RSpec.describe 'Una especificación de integración', :db do
# ...
fin
Tenga en cuenta que hemos tenido que hacer dos cosas para asegurarnos de que esta especificación se ejecute dentro de
una transacción de base de datos:
• Cargue explícitamente el código de configuración desde
support/db • Etiquete el grupo de ejemplo con :db
Si tiene varios archivos de especificaciones que tocan la base de datos, es fácil olvidarse de hacer una
u otra de estas cosas. El resultado pueden ser especificaciones que se aprueban o fallan de manera
inconsistente (dependiendo de cuándo y cómo las ejecute).
Sería bueno poder etiquetar los grupos de ejemplo relacionados con la base de datos con :db y confiar
en que el código de soporte se cargará según sea necesario. Afortunadamente, RSpec tiene una opción
que admite exactamente este uso. En su spec_helper.rb, agregue el siguiente fragmento dentro del
bloque RSpec.configure :
06integrationspecs/07/expense_tracker/spec/spec_helper.rb
RSpec.configure do |config|
config.when_first_matching_example_defined(:db) do require_relative
'support/db' end
informar fe de erratas • discutir
Machine Translated by Google
Capítulo 6. Realización: Especificaciones de integración • 94
Con ese enlace en su lugar, RSpec cargará condicionalmente spec /support/db.rb si (y solo si) se
cargan ejemplos que tienen una etiqueta :db .
Ahora, deberá agregar la etiqueta a sus especificaciones. Tanto sus especificaciones de
aceptación como de integración lo necesitan al final de la línea RSpec.describe , justo antes de la
palabra clave do . Primero, spec/acceptance/expense_tracker_api_spec.rb:
06integrationspecs/08/expense_tracker/spec/acceptance/expense_tracker_api_spec.rb
RSpec.describe 'Expense Tracker API', :db do
A continuación, spec/integration/app/ledger_spec.rb:
06integrationspecs/08/expense_tracker/spec/integration/app/ledger_spec.rb
RSpec.describe Ledger, :aggregate_failures, :db do
Mientras lo hace, también puede eliminar la línea require_relative '../../support/db' de ledger_spec.rb.
Por fin, puede ejecutar RSpec con la misma semilla aleatoria y obtener solo el único error que
esperábamos:
$ bundle exec rspec seed 32043 «
truncado »
Terminado en 0.05786 segundos (los archivos tardaron 0.22818 segundos en cargarse)
11 ejemplos, 1 error, 1 pendiente
Ejemplos fallidos:
rspec ./spec/integration/app/ledger_spec.rb:33 #
ExpenseTracker::Ledger#record cuando al gasto le falta un beneficiario rechaza el gasto como no válido
Aleatorizado con semilla 32043
Volvemos a una sola especificación que falla de manera predecible. Es hora de continuar con el
ciclo Rojo/Verde/Refactorizar agregando solo el comportamiento suficiente a su objeto para que
la especificación pase.
Completando el comportamiento
La especificación que falla espera que el método de registro de la clase Ledger devuelva
información de error si pasamos un gasto no válido (uno sin beneficiario definido). Agregue las
siguientes líneas resaltadas a la definición de registro en app/ledger.rb:
06integrationspecs/08/expense_tracker/app/ledger.rb
módulo ExpenseTracker
RecordResult = Struct.new(:¿éxito?, :expense_id, :error_message)
class Ledger
def record(gasto) a menos
que gasto.clave?('beneficiario')
mensaje = 'Gasto no válido: se requiere 'beneficiario''
informar fe de erratas • discutir
Machine Translated by Google
Consulta de Gastos • 95
return RecordResult.new(falso, nil, mensaje) end
DB[:gastos].insertar(gastos) id =
DB[:gastos].max(:id)
RecordResult.new(verdadero, id, nil) fin
def gastos_en(fecha) fin
fin
fin
Vuelva a ejecutar sus especificaciones; deberían pasar todos. ¡Bien hecho! Ha implementado el
ahorro de un solo gasto en todas las capas de la aplicación, de arriba a abajo.
$ paquete exec rspec spec/integration/app/ledger_spec.rb « truncado »
Terminado en 0.01205 segundos (los archivos tardaron 0.15597 segundos en cargarse) 2
ejemplos, 0 fallas
Aleatorizado con semilla 38450
Ahora que domina el proceso y ha instalado todos los datos y la infraestructura de enrutamiento, la
siguiente parte del comportamiento de Ledger será mucho más fácil de implementar.
Consulta de Gastos
Volvamos nuestra atención a la pieza final del rompecabezas: consultar los gastos de la base de
datos. Primero, necesitará una especificación fallida que registre algunos gastos en el libro mayor,
con algunos de los gastos en la misma fecha.
La consulta de esa fecha debe devolver solo los gastos coincidentes.
Agregue las siguientes especificaciones dentro del bloque RSpec.describe en spec/integration/app/
ledger_spec.rb:
06integrationspecs/09/expense_tracker/spec/integration/app/ledger_spec.rb
describe '#expenses_on' hazlo
'devuelve todos los gastos para la fecha proporcionada' haz
resultado_1 = libro mayor.record(gastos.merge('fecha' => '20170610')) resultado_2 =
libro mayor.record(gastos.merge('fecha' => '20170610')) resultado_3 = libro
mayor.record(gastos.merge('fecha' => '20170611'))
esperar (libro mayor. gastos_en ('20170610')). para contener_exactamente (
un_hash_incluido(id: resultado_1.id_de_gastos),
un_hash_incluido(id: resultado_2.id_de_gastos)
) fin
informar fe de erratas • discutir
Machine Translated by Google
Capítulo 6. Realización: Especificaciones de integración • 96
' devuelve una matriz en blanco cuando no hay gastos coincidentes' do
expect(ledger.expenses_on('20170610')).to eq([]) fin
fin
El flujo general (agregar tres gastos y luego buscar dos de ellos por fecha) es similar a lo que
hicimos en la especificación de aceptación en Consulta de datos, en la página 54. Aquí, está
probando a nivel del objeto Libro mayor individual , en lugar de toda la aplicación.
Estamos usando otro comparador componible aquí para combinar dos emparejadores que ha
usado antes (contain_exactly y a_hash_incluye) en un nuevo comparador que describe una
estructura de datos anidados.
Ejecute el archivo de especificaciones para asegurarse de que fallan ambas especificaciones:
$ paquete exec rspec spec/integration/app/ledger_spec.rb
Aleatorizado con semilla 3824
ExpenseTracker::Ledger
#registro
con un gasto válido
guarda con éxito el gasto en la base de datos
cuando el gasto carece de un beneficiario
rechaza el gasto como no válido
#expenses_on
devuelve una matriz en blanco cuando no hay gastos coincidentes (FALLO 1) devuelve
todos los gastos para la fecha proporcionada (FALLO 2)
Fallas:
1) ExpenseTracker::Ledger#expenses_on devuelve una matriz en blanco cuando no hay gastos
coincidentes
Falla/Error: expect(ledger.expenses_on('20170610')).to eq([])
esperado: []
obtenido: cero
(comparado con ==) # ./
spec/integration/app/ledger_spec.rb:59:in ̀bloque (3 niveles) en
<module:ExpenseTracker>' # ./
spec/support/db.rb:9:in ̀ bloque (3 niveles) en <superior (requerido)>' # ./spec/support/
db.rb:9:in ̀bloque (2 niveles) en <superior (requerido)>'
2) ExpenseTracker::Ledger#expenses_on devuelve todos los gastos para la fecha
proporcionada
Falla/Error:
esperar (libro mayor. gastos_en ('20170610')). para contener_exactamente (
un_hash_incluido(id: resultado_1.id_de_gastos),
un_hash_incluido(id: resultado_2.id_de_gastos)
)
informar fe de erratas • discutir
Machine Translated by Google
Consulta de Gastos • 97
esperaba una colección que se puede convertir en una matriz con ̀#to_ary` o
`#to_a`, pero obtuvo cero
# ./spec/integration/app/ledger_spec.rb:52:in ̀bloque (3 niveles) en
<módulo:ExpenseTracker>' # ./
spec/support/db.rb:9:in ̀bloque (3 niveles) en <superior (obligatorio)>' # ./spec/support/
db.rb:9:in ̀bloque (2 niveles) en <superior (obligatorio)>'
Terminado en 0.02766 segundos (los archivos tardaron 0.16616 segundos en
cargarse) 4 ejemplos, 2 fallas
Ejemplos fallidos:
rspec ./spec/integration/app/ledger_spec.rb:58 #
ExpenseTracker::Ledger#expenses_on devuelve una matriz en blanco cuando no hay gastos
coincidentes rspec ./
spec/integration/app/ledger_spec.rb:47 # ExpenseTracker::
Ledger#expenses_on devuelve todos los gastos para la fecha proporcionada
Aleatorizado con semilla 3824
Ahora, está listo para implementar la lógica para hacer que la especificación pase. La clase
Ledger necesitará consultar la tabla de gastos de la base de datos para filas que tengan una
fecha coincidente. Puede usar el método where de Sequel para filtrar por el campo de fecha .
En los ejercicios al final del capítulo anterior, definió un método de gastos vacío para la clase
Ledger . Complete su cuerpo con el siguiente código:
06integrationspecs/09/expense_tracker/app/
ledger.rb def expensas_en(fecha)
DB[:gastos].where(fecha: fecha).todo final
Tus especificaciones de integración deberían aprobarse ahora:
$ paquete exec rspec spec/integration/app/ledger_spec.rb
Aleatorizado con semilla 22267
ExpenseTracker::Ledger
#registro
con un gasto válido
guarda con éxito el gasto en la base de datos
cuando el gasto carece de un beneficiario
rechaza el gasto como no válido
#expenses_on
devuelve todos los gastos para la fecha proporcionada
devuelve una matriz en blanco cuando no hay gastos coincidentes
Terminado en 0.01832 segundos (los archivos tardaron 0.16797 segundos en
cargarse) 4 ejemplos, 0 fallas
Aleatorizado con semilla 22267
informar fe de erratas • discutir
Machine Translated by Google
Capítulo 6. Realización: Especificaciones de integración • 98
En este punto, toda la lógica y las especificaciones se implementan en cada capa de la
aplicación. Es hora de ver si nuestra especificación de aceptación más externa ya pasa.
Ejecutemos toda la suite:
$ paquete ejecutivo rspec
Aleatorizado con semilla 21580
F............
Fallas:
1) La API Expense Tracker registra los gastos FIJOS
Se esperaba que fallara la "Necesidad de mantener los gastos" pendiente. No se generó
ningún error.
# ./spec/aceptación/expense_tracker_api_spec.rb:22
Terminado en 0.04844 segundos (los archivos tardaron 0.22185 segundos en
cargarse) 13 ejemplos, 1 falla
Ejemplos fallidos:
rspec ./spec/acceptance/expense_tracker_api_spec.rb:22 # Expense Tracker API registra los gastos
enviados
Aleatorizado con semilla 21580
RSpec enumera una falla, pero dice que la especificación se ha solucionado. En Guardar su
progreso: Especificaciones pendientes, en la página 57, marcó la especificación de
aceptación como pendiente porque aún no estaba aprobada. Eso significa que pasará tan
pronto como elimine la línea pendiente .
Busque dentro de spec/acceptance/expense_tracker_api_spec.rb la línea que contiene la
palabra pendiente y elimine toda la línea. Una vez hecho esto, también pasarán sus
especificaciones de aceptación:
$ paquete ejecutivo rspec
Aleatorizado con semilla 14629
.............
Terminado en 0.04986 segundos (los archivos tardaron 0.21623 segundos en
cargarse) 13 ejemplos, 0 fallas
Aleatorizado con semilla 14629
¡Lindo! A lo largo de unos pocos capítulos, ha implementado una API JSON funcional para
realizar un seguimiento de los gastos. En cada paso del proceso, usó RSpec para guiar su
diseño, detectar regresiones y crear su aplicación con confianza.
Garantizar que la aplicación funcione de verdad
Hay una última cosa que probar antes de dar por terminado el día. Sus especificaciones
brindan evidencia de que la aplicación funciona, pero ciertamente no brindan prueba. Intentemos
informar fe de erratas • discutir
Machine Translated by Google
Garantizar que la aplicación funcione de verdad • 99
ejecutando la aplicación a mano (como hicimos al final de Guardar su progreso: especificaciones pendientes)
para ver por nosotros mismos si realmente funciona o no. Primero, inicie la aplicación usando el comando
rackup :
$ bundle exec rackup
[20170613 13:34:47] INFO WEBrick 1.3.1 [20170613
13:34:47] INFO ruby 2.4.1 (20170322) [x86_64darwin15]
[20170613 13:34:47] INFORMACIÓN WEBrick::HTTPServer#inicio: pid=45899 puerto=9292
Con su servidor API ejecutándose, intente acceder al punto final HTTP de la aplicación desde otra terminal:
$ curl localhost:9292/gastos/20170610 w "\n"
NameError: Constante no inicializada ExpenseTracker::Ledger::DB ~/code/
expense_tracker/app/ledger.rb:17:in ̀expenses_on' ~/code/expense_tracker/
app/api.rb:25:in ̀block in <class: API>'
« truncado »
¡Oh, no, un fracaso! La constante DB no está definida. Cuando estaba configurando la base de datos por
primera vez, mantuvo la configuración, incluida la definición de DB, en config/sequel.rb. Puede usar una
herramienta de búsqueda de texto como grep para encontrar dónde se está cargando este archivo en la
aplicación.6 El siguiente comando buscará en el directorio actual (.) recursivamente (r) para la configuración/
secuencia de texto:
$ grep config/sequel r. excludedir=.git ./spec/integration/
app/ledger_spec.rb:require_relative '../../../config /sequel'
En este momento, el único código que carga este archivo es la especificación de integración de Ledger .
Cualquier cosa que intente usar la base de datos solo funcionará cuando se cargue este archivo de
especificaciones. Eso está causando la falla que vemos cuando intentamos usar la aplicación de verdad.
También evitará que ejecutemos con éxito las especificaciones de aceptación en spec/acceptance/
expense_tracker_spec.rb por sí mismas.
Es importante poder ejecutar cualquier archivo de especificaciones de forma aislada. A menudo necesitará
hacerlo cuando esté depurando fallas en las especificaciones, o cuando esté saltando entre escribir una
clase y trabajar con sus especificaciones de unidad.
Puede usar un poco de magia de línea de comandos para intentar ejecutar cada uno de sus archivos de
especificaciones individualmente. Aquí hay un ejemplo usando bash, el shell predeterminado en la mayoría
de los sistemas similares a Unix (incluidas las herramientas Cygwin y MinGW disponibles para Windows):
$ (para f en ̀find spec iname '*_spec.rb'`; haz echo "$f:" bundle
exec rspec $f
fp || exit 1 done)
6. https://en.wikipedia.org/wiki/Grep
informar fe de erratas • discutir
Machine Translated by Google
Capítulo 6. Realización: especificaciones de integración • 100
especificación/aceptación/expense_tracker_api_spec.rb:
Aleatorizado con semilla 24954
Ocurrió un error en un enlace ̀before(:suite)`.
Falla/Error: Sequel.extension :migration
Error de nombre:
constante sin inicializar Sequel # ./spec/
support/db.rb:3:in ̀bloque (2 niveles) en <superior (obligatorio)>'
Terminado en 0.01902 segundos (los archivos tardaron 0.16858 segundos en cargarse)
0 ejemplos, 0 fallas, 1 error ocurrió fuera de los ejemplos
Aleatorizado con semilla 24954
Necesitamos encontrar un lugar mejor para cargar config/sequel.rb. En general, es una buena
idea cargar las dependencias justo donde se usan. En este caso, ledger.rb tiene una
dependencia directa de su configuración de Sequel. Carguemos la configuración desde la parte
superior de ese archivo:
06integrationspecs/09/expense_tracker/app/ledger.rb
require_relative '../config/sequel'
Con esa línea en su lugar, puede eliminar la llamada require_relative de spec/integration/app/
ledger_spec.rb. Ahora, cada uno de sus archivos de especificaciones pasará cuando lo ejecute
individualmente:
$ (para f en ̀find spec iname '*_spec.rb'`; haz echo "$f:"
bundle exec
rspec $f fp || exit 1 done)
especificación/aceptación/expense_tracker_api_spec.rb:
Aleatorizado con semilla 64689
.
Terminado en 0.02758 segundos (los archivos tardaron 0.20933 segundos en cargarse)
1 ejemplo, 0 fallas
Aleatorizado con semilla 64689
especificación/integración/aplicación/ledger_spec.rb:
Aleatorizado con semilla 7247
....
Terminado en 0.01689 segundos (los archivos tardaron 0.15956 segundos en cargarse)
4 ejemplos, 0 fallas
Aleatorizado con semilla 7247
especificación/unidad/aplicación/api_spec.rb:
Aleatorizado con semilla 21495
........
informar fe de erratas • discutir
Machine Translated by Google
Tu turno • 101
Terminado en 0.02619 segundos (los archivos tardaron 0.21264 segundos en
cargarse) 8 ejemplos, 0 fallas
Aleatorizado con semilla 21495
Intente iniciar la aplicación con rackup nuevamente y use curl para hacer algunas solicitudes:
$ curl localhost:9292/expenses data '{"beneficiario":"Zoo", "cantidad":10,
"fecha":"20170610"}' w
"\n" {"expense_id" :
1} $ curl localhost:9292/gastos data '{"beneficiario":"Starbucks", "cantidad":7.5, "fecha":"20170610"}'
w "\n" {" id_gastos":2} $ curl
localhost:9292/gastos/
20170610 w "\n" [{"id":1,"beneficiario":"Zoológico","cantidad":10.0,"fecha":
"20170610"},{"id":2,"beneficiario":"St arbucks","cantidad":7.5,"fecha":"20170610"}]
¡Funciona! Cómprese un vaso de su bebida favorita, comprométase con su trabajo y luego terminemos
en la siguiente sección. Simplemente no se olvide de registrar el gasto.
Tu turno
En este capítulo, implementó la pieza final de la aplicación: la capa de almacenamiento que escribe en
una base de datos real. Escribiste especificaciones de integración para probar esta capa.
Debido a que estas especificaciones realizaron cambios en el mismo estado global (la base de datos),
podrían interferir entre sí. Usó el orden de prueba aleatorio de RSpec y la capacidad bisect para
descubrir estas dependencias. Los arregló con un gancho limpio y mantuvo el código de transacción de
la base de datos ruidoso fuera de sus especificaciones de integración.
Una vez que pasaron sus especificaciones de integración, descubrió que sus especificaciones de
extremo a extremo también eran verdes. Ha completado la primera pieza importante de una aplicación real.
Durante este proyecto, ha llegado a conocer bastante bien RSpec. Ha aprendido a probar métodos
individuales usando expectativas y probar dobles. Creó grupos de ejemplo y compartió datos de prueba
para mantener sus especificaciones enfocadas en lo que se supone que debe hacer el código. Ha
utilizado el ejecutor de especificaciones de RSpec para descubrir y solucionar problemas en su código
de prueba.
En las próximas partes del libro, profundizaremos en cada uno de estos aspectos de RSpec. Pero
primero, intente hacer uno o dos ejercicios.
Ejercicios
En estos ejercicios, desarrollará sus especificaciones para que verifiquen el comportamiento de su
código más de cerca. Luego, utilizará el desarrollo de afuera hacia adentro para agregar una nueva
característica (específicamente, soporte para un nuevo formato de datos) a su rastreador de gastos.
informar fe de erratas • discutir
Machine Translated by Google
Capítulo 6. Realización: Especificaciones de integración • 102
Más Validaciones
Hasta ahora, la clase Ledger solo valida una propiedad de los gastos entrantes: que tengan un
beneficiario. ¿Qué otras cosas debe verificar el método de registro antes de guardar un gasto?
Escriba las especificaciones de integración para estas comprobaciones e implemente el
comportamiento.
Formato de datos
La versión actual de la aplicación asume que todas las entradas son JSON. Podría publicar
XML, y el código aún intentaría analizarlo como JSON:
$ curl data 'algunos xml aquí' \ header
"Tipo de contenido: texto/xml" \ http://
localhost:9292/gastos
Para este ejercicio, agregará la capacidad para que el rastreador de gastos lea y escriba XML
además de JSON. Primero, debe decidir sobre un formato XML.
Si usa algo como la biblioteca Ox, obtendrá un formato, lector y escritor gratis.7
Lo siguiente a considerar es cómo las personas que llaman seleccionarán el formato de datos.
Afortunadamente, HTTP proporciona un medio para hacer esto con encabezados, que muchos
clientes y servidores HTTP ya entienden:8
• El extremo POST de gastos miraría el encabezado HTTP ContentType para los tipos MIME
estándar para estos formatos, application/json o text/xml, para saber cómo analizar los
datos entrantes.9
• El extremo GET gastos/:fecha leería el encabezado Aceptar , similar a
ContentType y decida cómo dar formato a los datos salientes.
En el camino, terminará decidiendo qué hacer cuando la persona que llama solicite un formato
no compatible o cuando los datos entrantes no coincidan con el formato anunciado. Las
especificaciones de la unidad para su capa de enrutamiento son un buen lugar para verificar
todos estos casos extremos.
Rack::Test y Sinatra proporcionan métodos auxiliares para que pueda leer y escribir los distintos
encabezados HTTP:
• En Rack::Test, llame al encabezado para configurar sus encabezados de solicitud antes de llamar a obtener
10
o publicar.
7. http://www.ohler.com/ox/
8. https://en.wikipedia.org/wiki/Content_negotiation
9. https://en.wikipedia.org/wiki/MIME#ContentType
10. http://www.rubydoc.info/gems/racktest/0.6.3/Rack/Test/Session#getinstance_method
informar fe de erratas • discutir
Machine Translated by Google
Tu turno • 103
• En Sinatra, llame a request.accept o request.media_type para leer los encabezados particulares
necesarios para este ejercicio.11
• También en Sinatra, escriba sus encabezados de respuesta en el hash de encabezados antes
de regresar de su código de enrutamiento.12
Vaya a su capa de enrutamiento, luego especifique e implemente la lógica para leer y escribir
XML, incluidos los casos extremos de entrada no válida que pensó al planificar esta función.
¿Que sigue?
Ahora está armado con las herramientas para crear una característica importante de la aplicación
desde la especificación de aceptación hasta la implementación. Podríamos guiarlo a través de
una característica principal más (o podría crear una por su cuenta). Hacerlo sería una buena
práctica, pero no mostraría todas las formas en que RSpec puede ayudarlo a realizar pruebas de
manera más efectiva.
En cambio, analizaremos de cerca cada aspecto de cómo usará RSpec en su vida diaria.
Comenzaremos con los componentes básicos de RSpec, como la interfaz de línea de comandos.
11. http://www.sinatrarb.com/intro.html#Accessing%20the%20Request%20Object
12. http://www.sinatrarb.com/intro.html#Setting%20Body,%20Status%20Code%20and% 20Encabezados
informar fe de erratas • discutir
Machine Translated by Google
Parte III
Núcleo RSpec
RSpec 3 proporciona muchas formas útiles de ayudarlo a
probar su código, pero no es un trato de todo o nada.
Puede elegir qué aspectos de RSpec funcionarán mejor
para su proyecto.
En esta parte, veremos rspeccore, que ejecuta sus
especificaciones. Verá cómo organizar sus
especificaciones, cómo compartir código de manera
efectiva, cómo aplicar funciones a conjuntos arbitrarios
de ejemplos y cómo configurar las opciones más utilizadas de RSpec 3.
Machine Translated by Google
En este capítulo, verá:
• Cómo organizar sus especificaciones en grupos significativos •
Cómo "acertar con las palabras" con los nombres de sus grupos • Dónde
colocar el código de configuración y desmontaje compartido
CAPÍTULO 7
Ejemplos de código de estructuración
Ahora que usó RSpec para diseñar y construir los inicios de una aplicación, tiene un
modelo mental de "dónde van las cosas". Ha escrito ejemplos breves y claros que
explican exactamente cuál es el comportamiento esperado del código. Dispuso estos
ejemplos en grupos lógicos, no solo para compartir el código de configuración, sino
también para mantener juntas las especificaciones relacionadas.
Al final de este capítulo, será un experto en organización, al menos para ejemplos de
RSpec. Utilizará el lenguaje flexible de RSpec para comunicar su intención claramente
cuando organice sus especificaciones en grupos. También conocerá los diversos lugares
donde puede colocar el código de configuración compartido y cuáles son las ventajas y desventajas.
No le estamos pidiendo que memorice cada RSpec API posible para la organización del
código. ¡Ciertamente no lo hemos hecho! Pero cuando esté inmerso en un gran proyecto
y busque hacer que las pruebas sean más fáciles de leer y mantener, esperamos que
piense: “¡Ajá! Podría usar una de esas técnicas”, y vuelva a consultar este capítulo.
Las especificaciones bien estructuradas son algo más que orden. A veces, debe adjuntar
un comportamiento especial a ciertos ejemplos o grupos, como configurar una base de
datos o agregar un manejo de errores personalizado. El mecanismo de este
comportamiento, los metadatos, se basa en una buena agrupación, así que hablemos
primero de los grupos de ejemplo.
Conseguir las palabras correctas
Las especificaciones no pueden existir por sí solas. Desde la primera especificación que escribió
mientras leía este libro, cada especificación ha sido parte de un grupo de ejemplo. El grupo de
ejemplo cumple el rol de una clase de caso de prueba en otros marcos de prueba. tiene múltiples
propósitos:
• Brinda una estructura lógica para comprender cómo se relacionan los ejemplos individuales
a otro
informar fe de erratas • discutir
Machine Translated by Google
Capítulo 7. Ejemplos de código de estructuración • 108
• Describe el contexto, como una clase, un método o una situación en particular, de
lo que estás probando
• Proporciona una clase de Ruby para que actúe como un ámbito para su lógica compartida, como
ganchos, definiciones let y métodos auxiliares
• Ejecuta código común de instalación y desmontaje compartido por varios ejemplos
Veamos algunas formas diferentes de crear estos grupos.
Los basicos
Vimos las formas básicas de organizar las especificaciones en Grupos, Ejemplos y Expectativas, en
la página 5:
• describe crea un grupo de ejemplo. • crea un
solo ejemplo.
Antes de pasar a otras formas de organizar las especificaciones, analicemos los puntos más finos
de estos componentes básicos.
describir
Todas las diferentes variaciones de describe equivalen a lo mismo: dices qué es lo que estás
probando. La descripción puede ser una cadena:
07structuringcodeexamples/01/getting_the_words_right.rb
RSpec.describe 'Mi increíble API de jardinería' termina
…o cualquier clase, módulo u objeto de Ruby:
07structuringcodeexamples/01/getting_the_words_right.rb
RSpec.describe Perennials::Rhubarb do end
RSpec.describe Las plantas perennes
terminan
RSpec.describe my_favorite_broccoli terminan
Puede combinar estos dos enfoques y pasar una clase/módulo/objeto de Ruby seguido de una
cadena:
07structuringcodeexamples/01/getting_the_words_right.rb
RSpec.describe Garden, 'in winter' do end
Las diferencias pueden parecer sutiles, pero pasar un nombre de clase tiene algunas ventajas.
Requiere que la clase exista y esté escrita correctamente, lo que le ayudará a detectar errores.
También permite a los autores de la extensión RSpec proporcionar
informar fe de erratas • discutir
Machine Translated by Google
Obtener las palabras correctas • 109
comodidades para usted. Por ejemplo, la biblioteca rspecrails puede indicar a partir de sus
especificaciones qué controlador está probando.1
Todas estas variantes funcionan bien con los metadatos que usó en Tag
Filtrado, en la página 25 para etiquetar ejemplos con información adicional:
07ejemplosdecódigodeestructuración/01/
obtener_las_palabras_correctas.rb RSpec.describe WeatherStation, 'actualizaciones de radar',
uses_network: true do end
Hablaremos más sobre los metadatos en Especificaciones de corte y troceado con metadatos.
él
Dentro de un grupo de ejemplo, crea un ejemplo. Pasas una descripción del comportamiento que estás
especificando. Al igual que con describe, también puede pasar metadatos personalizados para ayudar
a RSpec a ejecutar ejemplos específicos de manera diferente. Así es como puede indicar que las
especificaciones de un rociador de césped controlado por computadora necesitan acceso a un bus en serie:
07structuringcodeexamples/01/getting_the_words_right.rb
RSpec.describe Sprinkler hazlo 'riega el
jardín', uses_serial_bus: verdadero fin
Estos dos componentes básicos son suficientes para tener un gran comienzo con BDD. Pero una parte
crucial de BDD es "tener las palabras correctas". Algunos conceptos no encajan bien en frases con
describe y it. Afortunadamente, RSpec proporciona diferentes formas de redactar sus especificaciones.
Otras formas de obtener las palabras correctas
El uso de describe tiene más sentido cuando todos los ejemplos de un grupo describen una sola clase,
método o módulo. Sin embargo, no todo encaja en una plantilla tan sencilla de "sujeto hace acción".
contexto En lugar de describir
A veces, desea agrupar ejemplos porque están relacionados con una situación o condición compartida.
Podrías hacerlo usando describe:
07structuringcodeexamples/01/getting_the_words_right.rb
RSpec.describe 'Un hervidor de agua' describe ' cuando
está hirviendo' hazlo 'puede hacer té'
'puede hacer café'
final
final
1. https://relishapp.com/rspec/rspecrails/v/36/docs/controllerspecs
informar fe de erratas • discutir
Machine Translated by Google
Capítulo 7. Ejemplos de código de estructuración • 110
Sin embargo, ¿ves qué torpemente se lee ese bloque interno? "¿Describir cuando hierve?"
Para grupos como estos, RSpec proporciona contexto, un alias para describir.
07structuringcodeexamples/01/getting_the_words_right.rb
RSpec.describe 'Un hervidor de agua' do
context 'al hervir' do it 'puede hacer té'
it 'puede hacer café'
end end
Puede parecer quisquilloso preocuparse por la redacción, pero las especificaciones legibles
son cruciales para comunicar sus ideas y para el mantenimiento a largo plazo. Muestran la
intención detrás del código.
ejemplo En lugar de
it , el método it de RSpec también tiene alias útiles. La mayoría de las veces, lo que estás
describiendo es el sujeto de una oración, como "Un hervidor de agua puede hacer té".
Aquí, el pronombre it tiene mucho sentido, ya que sustituye al sujeto.
En otras ocasiones, proporciona varios ejemplos de datos en lugar de varias oraciones
sobre un tema. Por ejemplo, podría estar diseñando un analizador de números de teléfono
y desea demostrar que funciona con múltiples formatos de números de teléfono:
07structuringcodeexamples/01/getting_the_words_right.rb
RSpec.describe PhoneNumberParser, 'analiza números de teléfono' hazlo
'en formato xxxxxxxxxx' it 'en
formato (xxx) xxxxxxx' end
Aquí, no tiene sentido expresar cada ejemplo como una oración que comienza con él. En
su lugar, puede usar el ejemplo, que funciona igual pero se lee mucho más claramente:
07structuringcodeexamples/01/getting_the_words_right.rb
RSpec.describe PhoneNumberParser, 'analiza números de teléfono' do
ejemplo 'en formato xxxxxxxxxx' ejemplo
'en formato (xxx) xxxxxxx'
fin
Esto corrige la redacción incómoda, sin que tengamos que repetir la frase analiza los
números de teléfono para cada línea .
especificar En lugar de eso
Como un cajón de sastre para los momentos en que ni él ni el ejemplo se leen bien, RSpec proporciona el
alias de especificación :
informar fe de erratas • discutir
Machine Translated by Google
Obtener las palabras correctas • 111
07structuringcodeexamples/01/getting_the_words_right.rb
RSpec.describe 'Deprecations' do
especifique 'MyGem.config está en desuso en favor de MyGem.configure'
especifique 'MyGem.run está en desuso en favor de MyGem.start' end
Hemos agrupado estos ejemplos a lo largo de una preocupación transversal
(desvalorización de bibliotecas) en lugar de un tema común. especifique funciona bien
para casos como estos, donde cada ejemplo tiene su propio tema.
Definición de sus propios nombres
No está limitado a estos alias. RSpec le permite definir sus propios nombres para describirlo .
Por ejemplo, suponga que está buscando un error relacionado con la base de datos y
desea pausar la ejecución después de cada ejemplo en un grupo para inspeccionar los datos.
La gema Pry admite este estilo de depuración.2 Con Pry cargado, puede pausar la
ejecución llamando a binding.pry dentro de cada bloque it .
Sin embargo, sería más fácil y menos propenso a errores tener un método que actúe como
describe pero agregue el comportamiento Pry por usted. ¿Recuerda los prefijos x y f del
Capítulo 2, Desde escribir especificaciones hasta ejecutarlas, en la página 15? Estos le
permiten omitir o enfocar un ejemplo o grupo. Usemos la misma técnica para definir nuevos
alias, pdescribe y pit. Así es como cambiaría uno de sus ejemplos de seguimiento de
gastos de it to pit:
07estructuracióncódigoejemplos/02/expense_tracker/spec/integration/app/ledger_spec.rb
pit 'guarda con éxito el gasto en la base de datos' do result =
ledger.record(gasto)
esperar ( resultado). ser_éxito
esperar ( DB [ : gastos ] . todos) . 10') )] fin
Para definir estos dos alias, agregaría el siguiente código a un bloque RSpec.configure :
07ejemplosdecódigodeestructuración/02/expense_tracker/spec/
spec_helper.rb RSpec.configure do |rspec|
rspec.alias_example_group_to :pdescribe, palanca: true
rspec.alias_example_to :pit, palanca: true
2. http://pryrepl.org
informar fe de erratas • discutir
Machine Translated by Google
Capítulo 7. Ejemplos de código de estructuración • 112
rspec.after(:ejemplo, pry: true) do |ex|
requiere enlace
' palanca'.pry
end
end
Cada uno de estos alias agregará pry: true metadata a su respectivo grupo de ejemplo o
ejemplo único. El gancho posterior llama a binding.pry solo después de los ejemplos que tienen
definida la metatada :pry .
Ahora, puede activar o desactivar rápidamente el comportamiento de palanca agregando o
eliminando una p al comienzo de describe o it.
Cada técnica que ha visto en esta sección se trata de claridad, específicamente, de organizar
sus ejemplos para que se lean lógicamente. Vamos a continuar con este tema de la claridad
en la siguiente sección. Verá cómo evitar que las rutinas de configuración comunes abarroten
el contenido de sus especificaciones.
Compartir lógica común
En los últimos capítulos, ha utilizado varias técnicas diferentes para compartir una lógica de
configuración común.
Las tres principales herramientas de organización son definiciones let , ganchos y métodos
auxiliares. Aquí hay un fragmento que contiene estos tres elementos uno al lado del otro.
Es una versión simplificada de las especificaciones de la API que escribió para Testing in
Isolation: Unit Specs, incluida una refactorización menor de los ejercicios.
07structuringcodeexamples/03/expense_tracker/spec/api_spec.rb
RSpec.describe 'POST un gasto exitoso' do
# dejar definiciones
let(:libro mayor) { instance_double('ExpenseTracker::Ledger') } let(:gasto) { { 'algunos' =>
'datos' } }
# gancho
antes de
permitir (libro mayor). recibir (: registro) . con (gasto ) .
fin
# método auxiliar def
parsed_last_response
JSON.parse(last_response.body) end
fin
A medida que ha trabajado con los ejemplos de este libro, ha utilizado definiciones let varias
veces en sus especificaciones. Son geniales para configurar cualquier cosa que pueda ser
informar fe de erratas • discutir
Machine Translated by Google
Compartiendo Lógica Común • 113
inicializados en una línea o dos de código, y le brindan una evaluación perezosa de forma gratuita (RSpec
nunca ejecuta el bloque hasta que realmente se necesita).
Has visto todo lo que hay para alquilar. Pero nos gustaría mostrarle un poco más sobre las otras dos
técnicas.
Manos
Los ganchos son para situaciones en las que un bloque let simplemente no funciona. Por ejemplo, es
posible que su código de configuración compartido deba tener efectos secundarios, como modificar la
configuración global o escribir en un archivo. O puede estar implementando preocupaciones transversales
como transacciones de bases de datos.
Escribir un gancho implica dos conceptos. El tipo de gancho controla cuándo se ejecuta en relación con
sus ejemplos. El alcance controla la frecuencia con la que se ejecuta el gancho.
Veamos estos dos conceptos a la vez.
Tipo
Hay tres tipos de ganchos en RSpec, llamados así cuando se ejecutan:
• antes
• después
• alrededor
Los anzuelos antes y después están relacionados, así que profundicemos en esos primero.
antes y después de
Como su nombre lo indica, sus anzuelos anteriores se ejecutarán antes que sus ejemplos.
Se garantiza que los ganchos posteriores se ejecutarán después de sus ejemplos, incluso si el ejemplo
falla o el gancho anterior genera una excepción. Estos ganchos están destinados a limpiar después de la
lógica y las especificaciones de su configuración.
Aquí hay un ejemplo que oculta el hash ENV que contiene las variables de entorno de su aplicación (para
que sus especificaciones puedan modificarlas de manera segura sin afectar otras especificaciones), luego
restaura el hash después:
07estructuracióncódigoejemplos/04/
before_and_after_hooks_spec.rb RSpec.describe MyApp::Configuration do
before(:ejemplo) do
@original_env = ENV.to_hash end
after(:ejemplo) hacer
ENV.replace(@original_env) end
fin
informar fe de erratas • discutir
Machine Translated by Google
Capítulo 7. Ejemplos de código de estructuración • 114
Este estilo de gancho es fácil de leer, pero divide la lógica de configuración y desmontaje en
dos mitades que debemos seguir. Si desea mantener unido todo este código relacionado, puede
usar un enlace en su lugar.
Favorecer antes de después de la limpieza de datos
Cuando la lógica de limpieza de su base de datos no encaja perfectamente en
un enlace transaccional , querrá considerar un enlace anterior o posterior . En
estas situaciones, recomendamos usar un enlace anterior por las siguientes
razones:
• Si olvida agregar el gancho anterior a una especificación en particular, la
falla ocurrirá en ese ejemplo en lugar de uno posterior.
• Cuando ejecuta un solo ejemplo para diagnosticar una falla, los registros
permanecerán en la base de datos para que pueda investigarlos.
alrededor
Los ganchos around son un poco más complejos de lo que hemos visto hasta ahora: intercalan
su código de especificación dentro de su gancho, por lo que parte del gancho se ejecuta antes
que el ejemplo y parte se ejecuta después. Aquí está la lógica de configuración y desmontaje
del fragmento anterior, envuelto en un gancho alrededor :
07ejemplosdecódigodeestructuración/04/
around_hooks_spec.rb RSpec.describe
MyApp::Configuration do around(:ejemplo) do |ex|
original_env = ENV.to_hash
ex.ejecutar
ENV.reemplazar (original_env)
final
fin
El comportamiento de estos dos fragmentos es el mismo; es solo una cuestión de cuál se lee
mejor para su aplicación.
Antes de pasar al concepto de alcance, echemos un vistazo rápido a uno de los puntos más
finos de los ganchos de escritura: dónde colocar el código.
Hooks de
configuración Hasta ahora en este capítulo, hemos puesto nuestros hooks before, after y around
dentro de grupos de ejemplo. Si solo necesita que sus ganchos se ejecuten para un conjunto
de ejemplos, entonces este enfoque funciona bien. Pero si necesita que sus ganchos se
ejecuten para varios grupos, se cansará de copiar y pegar todo ese código en cada grupo.
informar fe de erratas • discutir
Machine Translated by Google
Compartiendo Lógica Común • 115
En estas situaciones, puede definir los ganchos una vez para toda su suite, en un bloque
RSpec.configure (normalmente en spec/spec_helper.rb o en algún lugar de spec/support):
07ejemplosdecódigodeestructuración/04/spec/
spec_helper.rb RSpec.configure do |config|
config.around(:ejemplo) hacer |ex|
original_env = ENV.to_hash
ex.ejecutar
ENV.reemplazar (original_env) final
fin
Ha definido estos ganchos de configuración en un solo lugar, pero se ejecutarán para cada ejemplo
en su conjunto de pruebas. Tenga en cuenta las compensaciones aquí:
• Los ganchos globales reducen la duplicación, pero pueden generar sorprendentes efectos de
"acción a distancia" en sus especificaciones.3
• Los enlaces dentro de los grupos de ejemplo son más fáciles de seguir, pero es fácil omitir un
enlace importante por error al crear un nuevo archivo de especificaciones.
Usar ganchos de configuración para detalles incidentales
Le recomendamos que solo use enlaces de configuración para cosas que no son
esenciales para comprender cómo funcionan sus especificaciones. Los fragmentos de
lógica que aíslan cada ejemplo, como las transacciones de la base de datos o el
sandboxing del entorno, son los principales candidatos.
Preferimos mantener las cosas simples y ejecutar nuestros ganchos incondicionalmente. Sin
embargo, si nuestros ganchos de configuración solo son necesarios para un subconjunto de
ejemplos, y en particular si son lentos, usaremos metadatos para asegurarnos de que se ejecuten
solo para el subconjunto que los necesita. Ya usó esta técnica en Aislamiento de sus especificaciones
mediante transacciones de bases de datos, en la página 92.
Alcance
La mayoría de los ganchos que ha escrito están destinados a ejecutarse una vez por ejemplo. Después
de todo, desea asegurarse de que sus ejemplos puedan ejecutarse en cualquier orden y que cualquier
ejemplo individual pueda ejecutarse por sí solo. Dado que este comportamiento es la norma, RSpec
establece el alcance en : ejemplo si no proporciona uno.
A veces, sin embargo, un enlace necesita realizar una operación que requiere mucho tiempo, como
crear un montón de tablas de base de datos o iniciar un navegador web en vivo. Ejecutar el gancho
una vez por especificación tendría un costo prohibitivo.
3. https://en.wikipedia.org/wiki/Action_at_a_distance_(programación_computadora)
informar fe de erratas • discutir
Machine Translated by Google
Capítulo 7. Ejemplos de código de estructuración • 116
Para estos casos, puede ejecutar el gancho solo una vez para todo el conjunto de especificaciones o
una vez por grupo de ejemplo. Los ganchos toman un argumento :suite o :context para modificar el
alcance.
En el siguiente fragmento, usamos un enlace anterior (: contexto) para iniciar un navegador web solo
una vez para un grupo de ejemplo:
07structuringcodeexamples/04/before_and_after_hooks_spec.rb
RSpec.describe 'Interfaz web para mi termostato' do
before(:context ) finaliza
WebBrowser.launch
después (: contexto) hacer
Fin de WebBrowser.shutdown
fin
Use :context Hooks con cautela
Solo consideramos usar ganchos :context para efectos secundarios, como iniciar
un navegador web, que satisfagan las dos condiciones siguientes:
• No interactúa con cosas que tienen un ciclo de vida por ejemplo • Es notablemente
lento para ejecutarse
Cuando usa un gancho :context , es responsable de limpiar cualquier estado
resultante; de lo contrario, puede hacer que otras especificaciones pasen o fallen
incorrectamente.
Este es un problema particularmente común con el código de la base de datos.
Cualquier registro creado en un gancho anterior (: contexto) no se ejecutará en sus
transacciones de base de datos por ejemplo. Los registros permanecerán después de
que se complete el grupo de ejemplo, lo que podría afectar las especificaciones posteriores.
A veces, necesita una forma de ejecutar un código de configuración solo una vez, antes de que
comience el primer ejemplo. Para eso están los ganchos :suite :
07estructuracióncódigoejemplos/04/spec/
spec_helper.rb requiere 'fileutils'
RSpec.configure do |config|
config.before(:suite) do # Eliminar
archivos temporales sobrantes
FileUtils.rm_rf('tmp') end
fin
informar fe de erratas • discutir
Machine Translated by Google
Compartiendo Lógica Común • 117
Tenga en cuenta que hemos escrito este código como un gancho de configuración. De hecho,
RSpec.configure es el único lugar donde se permiten ganchos :suite , porque existen independientemente
de cualquier ejemplo o grupo.
Puede usar anzuelos antes y después con cualquiera de los tres visores. Mientras escribimos este capítulo,
alrededor de los ganchos solo se admite :example scope.
¿Qué hay de before(:each) y before(:all)?
Ocasionalmente puede encontrarse con algunas especificaciones que definen sus ganchos usando :each y :all para el
argumento de alcance . Estos son los términos que RSpec usó originalmente en lugar de :example y :context. Sin
embargo, encontramos que :all era confuso cuando se usaba para definir ganchos de configuración:
RSpec.configure do |config|
config.before(:all) do # ...
final
final
En este archivo de configuración, la palabra "todos" sugiere que el enlace se ejecutará antes que todos los ejemplos en
todo el conjunto, pero ese no es el caso. Este enlace se ejecutará una vez por grupo de ejemplo de nivel superior. Para
ejecutar una vez para la suite, usaría before(:suite) en su lugar.
RSpec 3 corrigió la redacción confusa al cambiar el nombre de :each a :example y :all a :context.
Los nombres antiguos :each y :all siguen estando allí por motivos de compatibilidad con versiones anteriores de las
especificaciones existentes, pero recomendamos usar solo los términos más nuevos.
Una última cosa a tener en cuenta sobre los ganchos: si tiene un grupo de ejemplo anidado dentro de otro,
sus ganchos se ejecutarán en el siguiente orden:
• antes de que los ganchos corran de afuera hacia adentro.
• Los ganchos posteriores corren de adentro hacia afuera.
alrededor de los ganchos se comportan de manera similar. El comienzo de cada anzuelo alrededor , los
bits antes de la llamada a ejemplo.ejecutar, se ejecutará de afuera hacia adentro, como un anzuelo anterior .
El extremo de cada gancho circular se extenderá de adentro hacia afuera.
Cuándo usar ganchos
Los ganchos que has escrito han tenido dos propósitos:
• Eliminar detalles duplicados o incidentales que distraerían a los lectores
desde el punto de tu ejemplo
• Expresar las descripciones en inglés de sus grupos de ejemplo como exe
código cortable
informar fe de erratas • discutir
Machine Translated by Google
Capítulo 7. Ejemplos de código de estructuración • 118
El código de transacción de la base de datos que agregó en Aislar sus especificaciones usando
Transacciones de base de datos, en la página 92 es un ejemplo de la primera situación:
07ejemplosdecódigodeestructuración/05/expense_tracker/spec/
support/db.rb RSpec.configure
do |c| #...
c.alrededor(:ejemplo, :db) do |ejemplo|
DB.transaction(rollback: :siempre) { ejemplo.ejecutar } fin
fin
Al poner la reversión de su transacción en un gancho , evitó ensuciar cada especificación dependiente
de la base de datos con esta lógica de transacción.
En Handling Failure, en la página 73, escribió el segundo tipo de gancho:
07structuringcodeexamples/05/expense_tracker/spec/unit/app/
api_spec.rb context 'cuando el gasto falla en la validación' do
#...
antes de hacer
allow(libro mayor).para
recibir(:registro) .con(gasto) .and_return(RecordResult.new(false, 417, 'Gasto incompleto'))
fin
#...
fin
Su anzuelo anterior traduce la descripción 'cuando el gasto falla en la validación' en código Ruby.
Dentro de su gancho, configura el doble de prueba del libro mayor para que no registre el gasto. Al
hacerlo, se asegura de que la descripción sea válida para todos los ejemplos dentro del contexto.
También facilita que un lector vea lo que significa la descripción en inglés en términos de su modelo de
dominio.
Un gancho debería hacer que sea más fácil seguir tus ejemplos. Abusar de los ganchos RSpec hará
que salte todo el directorio de especificaciones para rastrear el flujo del programa.
En la siguiente sección, le mostraremos cómo reconocer un anzuelo malo y tratarlo usando otra
herramienta organizativa: los métodos auxiliares.
Métodos auxiliares
A veces, podemos ser demasiado inteligentes para nuestro propio bien y hacer un mal uso de estas
construcciones en un esfuerzo por eliminar hasta la última repetición de nuestras especificaciones.
Eche un vistazo a las siguientes especificaciones. Es bastante complejo, así que lo analizaremos en
detalle más adelante.
07structuringcodeexamples/06/transit/spec/berlin_transit_ticket_spec.rb
Línea 1 RSpec.describe BerlinTransitTicket do
informar fe de erratas • discutir
Machine Translated by Google
Compartiendo Lógica Común • 119
let(:billete) { BerlinTransitTicket.nuevo }
antes de hacer
5 # Estos valores dependen de las definiciones ̀let`
# definido en los contextos anidados a continuación.
#
ticket.starting_station = partida_estacion
ticket.estación_final = estación_final
10 fin
let(:tarifa) { billete.tarifa }
contexto 'al comenzar en la zona A' hacer
15 let(:starting_station) { 'Bundestag' }
context 'y terminando en la zona B' do
let(:final_estación) { 'Leopoldplatz' }
20 cuesta _ € 2.70' hacer
esperar(tarifa).to eq 2.7
fin
25 fin
context 'y terminando en la zona C' do
let(:final_estación) { 'Birkenwerder' }
30 cuesta _ € 3.30' hacer
esperar(tarifa).to eq 3.3
fin
35 fin
fin
fin
Rastreemos lo que sucede cuando RSpec ejecuta el primer ejemplo:
1. El enlace anterior de la línea 4 comienza a ejecutarse.
2. Nos referimos a ticket en la línea 8, lo que nos lleva a la definición let en la línea 2
para crear ese objeto.
3. Hacemos referencia a la estación_inicial en la línea 8, que salta a la definición let en
línea 15 y viceversa.
4. En la línea 9, hacemos referencia a ending_station, que salta a la definición let interna
en la línea 18 y viceversa.
5. El enlace anterior se completa, por lo que ahora comenzamos a ejecutar el ejemplo en la línea 20.
6. El ejemplo hace referencia a la tarifa, que salta a la definición let en la línea 12
y vuelta
7. Se ejecuta la expectativa y se completa el ejemplo.
informar fe de erratas • discutir
Machine Translated by Google
Capítulo 7. Ejemplos de código de estructuración • 120
¡Y eso es solo para el primer ejemplo! Ahora, imagine que este archivo de especificaciones ha
aumentado una cantidad de ejemplos adicionales a lo largo del tiempo. Algunas de estas
definiciones ni siquiera serán visibles en su pantalla cuando esté viendo un ejemplo fallido.
Considere a la persona que tiene que leer este código en seis meses cuando algo se rompa
(¡podría ser usted!). La primera reacción del lector al ver esas especificaciones de una sola
línea resaltadas es: “¿Qué pasó? ¿Qué comportamiento estamos probando aquí? La comunidad
de TDD llama a esta separación de causa y efecto un invitado misterioso.
4
El cálculo de la tarifa es lo principal que estamos probando. Debe estar al frente y al centro en
las especificaciones.
Sin embargo, eso no significa que debamos repetir todo ese código de configuración. Recuerde
que un grupo de ejemplo de RSpec es solo una clase de Ruby. Eso significa que podemos
definir métodos de ayuda en él, tal como lo haríamos con cualquier otra clase:
07estructuracióncódigoejemplos/06/transit/spec/berlin_transit_ticket_refactored_spec.rb
RSpec.describe BerlinTransitTicket do def
fare_for(starting_station, end_station) ticket = BerlinTransitTicket.new
ticket.starting_station = partida_estación ticket.ending_station
= final_estación ticket.tarifa
fin
contexto 'al comenzar en la zona A y terminar en la zona B' hacer
cuesta _ € 2.70' hacer
expect(fare_for('Bundestag', 'Leopoldplatz')).to eq 2.7 end
fin
contexto 'al comenzar en la zona A y terminar en la zona C' hacer
cuesta _ € 3.30' hacer
expect(fare_for('Bundestag', 'Birkenwerder')).to eq 3.3 end
fin
fin
Ahora, es explícito exactamente qué comportamiento estamos probando, sin necesidad de
repetir los detalles de la API de emisión de boletos. Alguien que lea su especificación no
necesitará adivinar el comportamiento implícito; verán todo explicado en blanco y negro.
Además, hemos ganado un poco de flexibilidad. Con el anzuelo anterior , se requerían todas
nuestras especificaciones para ejecutar el anzuelo tal como estaba escrito. Con un método
auxiliar, controlamos el tiempo y el contenido de la configuración.
4. https://robots.thoughtbot.com/mysteryguest
informar fe de erratas • discutir
Machine Translated by Google
Compartiendo Lógica Común • 121
Poner a sus ayudantes en un módulo Al
igual que con cualquier otra clase de Ruby, también puede definir métodos auxiliares en un módulo
separado e incluirlos en sus grupos de ejemplo. Por ejemplo, este es el resumen de las especificaciones
de aceptación que escribió en Empezar desde afuera: Especificaciones de aceptación:
07structuringcodeexamples/07/expense_tracker/spec/acceptance/expense_tracker_api_spec.rb
RSpec.describe 'Expense Tracker API', :db incluye
Rack::Prueba::Métodos
def app
ExpenseTracker::API.nuevo
final
#...
fin
A medida que defina las especificaciones en los nuevos archivos que impulsan la API, se encontrará
utilizando la prueba de rack en varios lugares. Si coloca este código de pegamento en un módulo,
puede incluirlo fácilmente en todas sus especificaciones de aceptación:
07structuringcodeexamples/08/expense_tracker/spec/acceptance/expense_tracker_api_spec.rb
module APIHelpers
incluye Rack::Prueba::Métodos
def app
ExpenseTracker::API.nuevo
fin
fin
RSpec.describe 'Expense Tracker API', :db incluye APIHelpers
#...
fin
Debido a la capa adicional de direccionamiento indirecto, recomendamos usar un módulo solo si
necesita usar los mismos métodos auxiliares en más de un grupo de ejemplo.
Incluir Módulos Automáticamente Incluso
una sola línea como include APIHelpers es fácil de olvidar. Si necesita incluir el mismo módulo auxiliar
en muchas o todas sus especificaciones, puede llamar a config.include en un bloque RSpec.configure :
07estructuracióncódigoejemplos/08/expense_tracker/spec/acceptance/
expense_tracker_api_spec.rb
RSpec.configure do |config|
config.include APIHelpers fin
informar fe de erratas • discutir
Machine Translated by Google
Capítulo 7. Ejemplos de código de estructuración • 122
Este ejemplo incluirá el módulo APIHelpers en cada grupo de ejemplo. La mayoría de las veces,
probablemente desee cargar este módulo solo en los grupos que lo necesitan. En Compartir código
condicionalmente, en la página 140, le mostraremos cómo hacerlo.
Al igual que con todas las configuraciones globales que están ocultas en un archivo de soporte, tenga
cuidado de no ocultar detalles importantes detrás de una llamada a config.include.
Compartir grupos de ejemplo
Como hemos visto, los módulos antiguos de Ruby funcionan muy bien para compartir métodos auxiliares
entre grupos de ejemplo. Pero eso es todo lo que pueden compartir. Si desea reutilizar un ejemplo, una
construcción let o un gancho, deberá buscar otra herramienta: grupos de ejemplos compartidos.
Al igual que su contraparte no compartida, un grupo de ejemplo compartido puede contener ejemplos,
métodos auxiliares, declaraciones let y ganchos. La única diferencia es la forma en que se usan. Un
grupo de ejemplo compartido existe solo para ser compartido.
Para ayudarlo a "obtener las palabras correctas", RSpec proporciona múltiples formas de crear y usar
grupos de ejemplo compartidos. Estos vienen en pares, con un método para definir un grupo compartido
y otro para usarlo :
• shared_context e include_context son para reutilizar la configuración común y el asistente
lógica.
• shared_examples e include_examples son para reutilizar ejemplos.
La elección entre la redacción ..._context y ..._examples es puramente una cuestión de comunicar su
intención. Detrás de escena, se comportan de manera idéntica.
Sin embargo , hay una forma más de compartir el comportamiento que es diferente. it_behaves_like crea
un nuevo grupo de ejemplo anidado para contener el código compartido. La diferencia radica en cuán
aislado está el comportamiento compartido del resto de sus ejemplos. En las próximas secciones,
hablaremos sobre cuándo le gustaría usar cada enfoque.
Compartir contextos
Anteriormente en este capítulo, vimos que puede agrupar métodos auxiliares comunes en un módulo:
07structuringcodeexamples/08/expense_tracker/spec/acceptance/expense_tracker_api_spec.rb
module APIHelpers
incluye Rack::Prueba::Métodos
def app
ExpenseTracker::API.nuevo
fin
fin
informar fe de erratas • discutir
Machine Translated by Google
Compartir grupos de ejemplo • 123
Esta técnica funciona bien siempre y cuando solo trabaje con métodos auxiliares.
Tarde o temprano, sin embargo, encontrará que desea compartir algunas declaraciones let o ganchos
en su lugar.
Por ejemplo, es posible que desee agregar autenticación a su API. Una vez que lo haya hecho,
deberá modificar sus especificaciones existentes para iniciar sesión antes de que realicen sus
solicitudes. Dado que iniciar sesión es un detalle superfluo para la mayoría de sus especificaciones,
un anzuelo anterior sería el lugar perfecto para colocar este nuevo comportamiento:
07estructuracióncódigoejemplos/09/expense_tracker/spec/acceptance/expense_tracker_api_spec.rb
antes de hacer
basic_authorize 'test_user', 'test_password' fin
Aquí, estamos usando el método basic_authorize de Rack::Test para tratar la solicitud HTTP como
si viniera de un usuario de prueba que inició sesión.
Sin embargo , este enlace no puede entrar en su módulo APIHelpers . Los módulos de Plain Ruby
no son conscientes de las construcciones RSpec, como los ganchos. En su lugar, puede convertir su
módulo en un contexto compartido:
07structuringcodeexamples/09/expense_tracker/spec/acceptance/expense_tracker_api_spec.rb
RSpec.shared_context Los 'ayudantes de API'
incluyen Rack::Prueba::Métodos
def app
ExpenseTracker::API.nuevo final
antes de
hacer basic_authorize 'test_user', 'test_password' end
fin
Para usar este contexto compartido, simplemente llame a include_context desde cualquier grupo de
ejemplo que necesite realizar llamadas a la API:
07structuringcodeexamples/09/expense_tracker/spec/acceptance/expense_tracker_api_spec.rb
RSpec.describe 'Expense Tracker API', :db do
include_context 'ayudantes de API'
#...
fin
Con include_context, RSpec evalúa su bloque de grupo compartido dentro de este grupo, lo que
hace que agregue el enlace, el método auxiliar y la inclusión del módulo aquí. Al igual que con
include, también puede usarlo en un bloque RSpec.configure para aquellas situaciones excepcionales
en las que desea incluir el grupo compartido en todos los grupos de ejemplo:
informar fe de erratas • discutir
Machine Translated by Google
Capítulo 7. Ejemplos de código de estructuración • 124
07estructuracióncódigoejemplos/09/expense_tracker/spec/acceptance/
expense_tracker_api_spec.rb
RSpec.configure do |config| config.include_context
Fin de 'ayudantes de API'
A continuación, pasemos al otro caso de uso común para grupos de ejemplos compartidos:
compartir ejemplos en lugar de contexto.
Compartiendo ejemplos
Una de las ideas más poderosas en software es definir una sola interfaz con múltiples
implementaciones. Por ejemplo, es posible que su aplicación web necesite almacenar datos en
caché en un almacén de clavevalor.5 Hay muchas implementaciones de esta idea, cada una con
sus propias ventajas sobre las demás. Debido a que todos implementan la misma funcionalidad
básica de kv_store.store(clave, valor) y kv_store.fetch(clave), puede elegir la implementación que
mejor se adapte a sus necesidades.
Ni siquiera tiene que elegir una sola implementación. Puede usar un almacén de valores clave
para la producción y otro diferente para las pruebas. En producción, es probable que desee un
almacenamiento persistente que mantenga los datos entre solicitudes.
Para las pruebas, puede ahorrar tiempo y complejidad mediante el uso de un almacén de valor
clave en memoria.
Si usa más de un almacén de clavevalor, es importante tener cierta confianza en que tienen el
mismo comportamiento. Puede escribir especificaciones para probar este comportamiento y
organizarlas usando ejemplos compartidos.
Sin ejemplos compartidos, puede comenzar con la siguiente especificación para un HashKVStore
en memoria:
07estructuracióncódigoejemplos/10/shared_examples/spec/
hash_kv_store_spec.rb requiere 'hash_kv_store'
RSpec.describe HashKVStore hacer
let(:kv_store) { HashKVStore.nuevo }
' le permite recuperar valores previamente almacenados' do
kv_store.store(:language, 'Ruby')
kv_store.store(:os, 'linux')
expect(kv_store.fetch(:language)).to eq 'Ruby'
expect(kv_store.fetch(:os)).to eq 'linux' end
' genera un KeyError cuando obtienes una clave desconocida '
esperar { kv_store.fetch(:foo) }.to raise_error(KeyError) end
fin
5. https://en.wikipedia.org/wiki/Keyvalue_database
informar fe de erratas • discutir
Machine Translated by Google
Compartir grupos de ejemplo • 125
Para probar una segunda implementación de esta interfaz, como un FileKVStore respaldado en disco,
puede copiar y pegar la especificación completa y reemplazar todas las ocurrencias de HashKVStore
con FileKVStore. Pero luego tendría que agregar cualquier comportamiento común nuevo a ambos
archivos de especificaciones. Tendríamos que mantener sincronizados manualmente los dos archivos
de especificaciones.
Este es exactamente el tipo de duplicación que los grupos de ejemplos compartidos pueden ayudarlo a
solucionar. Para hacer el cambio, mueva su bloque de descripción a su propio archivo en especificación/
soporte, cámbielo a un bloque shared_examples tomando un argumento y use ese argumento en la
declaración let(:kv_store) :
07structuringcodeexamples/11/shared_examples/spec/support/
kv_store_shared_examples.rb RSpec.shared_examples 'KV store'
do |kv_store_class| let(:kv_store) { kv_store_class.new }
' le permite obtener valores previamente almacenados' do
kv_store.store(:language, 'Ruby')
kv_store.store(:os, 'linux')
expect(kv_store.fetch(:language)).to eq 'Ruby'
expect(kv_store.fetch(:os)).to eq 'linux' end
' genera un KeyError cuando obtienes una clave desconocida '
esperar { kv_store.fetch(:foo) }.to raise_error(KeyError) end
fin
Por convención, los ejemplos compartidos van en especificaciones/soporte; hemos llamado a este
archivo kv_store_shared_examples.rb después de la interfaz común que estamos probando.
El argumento del bloque, kv_store_class, provendrá del código de llamada (que veremos en un
momento). Aquí, representa la clase que estamos probando.
Las convenciones están ahí por una razón
Cuando sugerimos nombres y ubicaciones para los archivos de soporte, no solo estamos
siendo quisquillosos. Elegir el nombre correcto puede ayudarlo a evitar errores.
Hemos visto a personas nombrar sus archivos de soporte como shared_spec.rb, que
RSpec intentará cargar como un archivo de especificaciones normal, lo que generará
mensajes de advertencia.
Ahora, puede reemplazar su archivo de especificaciones original con uno mucho más simple:
07estructuracióncódigoejemplos/11/shared_examples/spec/
hash_kv_store_spec.rb
require 'hash_kv_store' require 'support/kv_store_shared_examples'
RSpec.describe HashKVStore do
it_behaves_like 'KV store', HashKVStore end
informar fe de erratas • discutir
Machine Translated by Google
Capítulo 7. Ejemplos de código de estructuración • 126
Estamos pasando explícitamente la clase de implementación HashKVStore cuando traemos los
ejemplos compartidos con it_behaves_like. El bloque shared_examples del fragmento anterior
usa esta clase en su declaración let(:kv_store) .
Anidamiento En la introducción a esta sección, mencionamos que puede incluir ejemplos
compartidos con la llamada include_examples o it_behaves_like . Hasta ahora, solo hemos
usado it_behaves_like. Hablemos de la diferencia entre las dos llamadas. Aquí hay una versión
del fragmento con include_examples en su lugar:
07structuringcodeexamples/11/shared_examples/spec/include_examples_spec.rb
RSpec.describe HashKVStore do
include_examples 'KV store', HashKVStore end
La especificación se comportaría bien, pero los problemas surgen si agregamos una segunda
llamada a include_examples:
07structuringcodeexamples/11/shared_examples/spec/include_examples_twice_spec.rb
RSpec.describe 'Almacenes de clavevalor' do
include_examples 'Almacén KV', HashKVStore
include_examples 'Almacén KV', FileKVStore final
Llamar a include_examples es como pegar todo en el grupo de ejemplo compartido directamente
en este bloque de descripción . En particular, obtendría dos declaraciones let para :kv_store:
una para HashKVStore y otra para FileKVStore. Uno sobrescribiría al otro.
El resultado de la documentación muestra cómo se pisan los dedos de los pies unos a otros:
$ rspec spec/include_examples_twice_spec.rb documentación de formato
Almacenes de clavevalor
le permite obtener valores previamente almacenados
genera un KeyError cuando obtiene una clave desconocida le
permite obtener valores previamente almacenados genera
un KeyError cuando obtiene una clave desconocida
Terminado en 0.00355 segundos (los archivos tardaron 0.10257 segundos en
cargarse) 4 ejemplos, 0 fallas
Usar it_behaves_like evita este problema:
07structuringcodeexamples/11/shared_examples/spec/it_behaves_like_twice_spec.rb
RSpec.describe 'Almacenes de clavevalor' do
it_behaves_like 'KV store', HashKVStore
it_behaves_like 'KV store', FileKVStore end
Aquí, cada grupo de ejemplo se anida en su propio contexto. Puede ver la diferencia cuando
ejecuta con la opción de documentación format para RSpec:
informar fe de erratas • discutir
Machine Translated by Google
Compartir grupos de ejemplo • 127
$ rspec spec/it_behaves_like_twice_spec.rb documentación de formato
Las tiendas de clave
valor se comportan como la tienda KV
le permite obtener valores almacenados previamente genera un
KeyError cuando obtiene una clave desconocida
se comporta como la tienda KV
le permite obtener valores almacenados previamente genera un
KeyError cuando obtiene una clave desconocida
Terminado en 0.00337 segundos (los archivos tardaron 0.09726 segundos en cargarse) 4 ejemplos, 0
fallas
Debido a que cada uso del grupo de ejemplo compartido obtiene su propio contexto anidado,
las dos declaraciones let no interfieren entre sí.
Cuando tenga dudas, elija it_behaves_like
¿Se pregunta qué método usar para incluir sus ejemplos compartidos?
it_behaves_like es casi siempre el que desea. Garantiza que los contenidos
del grupo compartido no se "filtren" en el contexto circundante e interactúen
con sus otros ejemplos de formas sorprendentes.
Recomendamos usar include_examples solo cuando esté seguro de que el
contexto del grupo compartido no entrará en conflicto con nada en el grupo
circundante y tiene una razón específica para usarlo. Una de esas razones
es la claridad: a veces, su salida de especificaciones (usando el formateador de
documentación) se leerá más legiblemente sin el anidamiento adicional.
Personalización de grupos compartidos con
bloques Como se muestra en estos ejemplos, puede personalizar el comportamiento de sus
grupos de ejemplo compartidos pasando un argumento al bloque it_behaves_like . Esta técnica
funciona bien cuando todo lo que necesita hacer es pasar un argumento estático.
A veces, sin embargo, se necesita más flexibilidad que eso.
En nuestros ejemplos hasta ahora, hemos pasado la clase que estamos probando, HashKVStore
o FileKVStore, como un argumento estático cuando incluimos el grupo. El código compartido
simplemente llama a new en la clase pasada para crear una nueva tienda.
Si estas clases requieren argumentos para la creación de instancias, pasar un argumento no
funcionará. Por ejemplo, un almacén de clavevalor basado en archivos puede requerir que
pase un nombre de archivo como argumento.
Afortunadamente, tienes un truco más bajo la manga: puedes pasar un bloque (en lugar de solo
un argumento) a tu llamada it_behaves_like :
informar fe de erratas • discutir
Machine Translated by Google
Capítulo 7. Ejemplos de código de estructuración • 128
07estructuracióncódigoejemplos/11/shared_examples/spec/
pass_block_spec.rb requiere 'tempfile'
RSpec.describe FileKVStore hacer
it_behaves_like 'Tienda KV' hacer
let(:tempfile) { Tempfile.new('kv.store') } let(:kv_store)
{ FileKVStore.new(tempfile.path) } end end
Con esta técnica, su bloque puede contener cualquier construcción RSpec que necesite.
Aquí, hemos usado una definición let , pero también puede agregar métodos auxiliares y
ganchos dentro del mismo tipo de bloque.
Para que esta técnica funcione, debe cambiar la definición del grupo de ejemplo compartido
de la tienda KV .
07estructuracióncódigoejemplos/11/shared_examples/spec/pass_block_spec.rb
RSpec.shared_examples 'KV store' hazlo 'le
permite obtener valores previamente almacenados' haz
kv_store.store(:idioma, 'Ruby')
kv_store.store(:os, 'linux')
expect(kv_store.fetch(:language)).to eq 'Ruby'
expect(kv_store.fetch(:os)).to eq 'linux' end
# resto de ejemplos... fin
El grupo compartido ya no necesita tomar un argumento de bloque o definir let(:kv_store).
Simplemente usa kv_store normalmente dentro de cada ejemplo, confiando en que el
grupo anfitrión lo definirá con el valor correcto para el contexto.
Tu turno
En este capítulo, echamos un segundo vistazo a las formas principales de estructurar sus
ejemplos en grupos. Viste lo importante que es tomarse el tiempo para obtener las
palabras correctas y usar términos en sus especificaciones como contexto o especificar
dónde tienen más sentido que describirlo .
También nos sumergimos en formas de sacar el código de configuración duplicado de sus
especificaciones: definiciones let , ganchos y métodos auxiliares. Aunque hemos tenido
encuentros breves con estas técnicas en capítulos anteriores, vio cómo sacarles el máximo
partido aquí.
Finalmente, hablamos sobre cómo compartir ejemplos o contextos completos. Ahora es
el momento de aplicar estas técnicas a su propio código.
informar fe de erratas • discutir
Machine Translated by Google
Tu turno • 129
Ejercicio
En este ejercicio, ha heredado especificaciones para dos analizadores de URI diferentes. Las
implementaciones tienen un comportamiento similar, pero no idéntico. Su tarea será descubrir
cuál es la funcionalidad común y luego extraerla a especificaciones compartidas utilizando las
técnicas de este capítulo. Primero, aquí hay un archivo de especificaciones para la biblioteca
URI integrada de Ruby :
07estructuracióncódigoejemplos/ejercicios/shared_examples_ejercicio/spec/
uri_spec.rb requiere 'uri'
RSpec.describe URI hacer
'analiza el host ' hacer
expect(URI.parse('http://foo.com/').host).to eq 'foo.com' end
' analiza el puerto' do
expect(URI.parse('http://example.com:9876').port).to eq 9876 end
' predetermina el puerto para un URI http en 80 '
expect(URI.parse('http://example.com/').port).to eq 80 end
' predetermina el puerto para un URI https en 443 '
expect(URI.parse('https://example.com/').port).to eq 443 end end
A continuación, aquí hay uno para Addressable, una alternativa a URI que cumple más con
los estándares:6
07estructuracióncódigoejemplos/ejercicios//shared_examples_exercise/spec/
addressable_spec.rb requiere 'direccionable'
RSpec.describe Addressable hazlo
'analiza el esquema' espera
(Addressable::URI.parse('https://a.com/').scheme).to eq 'https' end
'analiza el host ' hacer
expect(Addressable::URI.parse('https://foo.com/').host).to eq 'foo.com' end
'analiza el puerto ' hacer
expect(Addressable::URI.parse('http://example.com:9876').port).to eq 9876 end
' analiza el camino' hacer
expect(Addressable::URI.parse('http://a.com/foo').path).to eq '/foo' end
fin
6. https://github.com/sporkmonger/addressable
informar fe de erratas • discutir
Machine Translated by Google
Capítulo 7. Ejemplos de código de estructuración • 130
Tenga en cuenta que las especificaciones no son idénticas y ni siquiera cubren el mismo
comportamiento. A medida que investiga qué tienen en común estas bibliotecas y qué tienen
de diferente, es posible que se encuentre escribiendo nuevas especificaciones. Deberá
tomar una decisión sobre cada nuevo ejemplo en cuanto a si debe incluirse en un archivo de
especificaciones específico de la implementación o en las especificaciones compartidas.
informar fe de erratas • discutir
Machine Translated by Google
En este capítulo, verá:
• Qué tipo de información almacena RSpec sobre cada especificación •
Cómo puede etiquetar sus ejemplos con información personalizada •
Cómo realizar una configuración costosa solo cuando la necesita • Cómo
ejecutar solo un subconjunto de sus especificaciones
CAPÍTULO 8
Especificaciones de corte y troceado con metadatos
A lo largo de este libro, ha seguido un principio clave que ha hecho que sus especificaciones sean más
rápidas, más confiables y más fáciles de usar: ejecute solo el código que necesita.
Este principio aparece en varias prácticas que ha estado utilizando a medida que siguió los ejemplos
de código:
• Cuando esté aislando una falla, ejecute solo el ejemplo fallido. • Cuando esté
modificando una clase, ejecute solo sus pruebas unitarias. • Cuando tenga
un código de configuración costoso, ejecútelo solo para las especificaciones donde
lo necesita.
Una pieza clave de RSpec que ha hecho posibles muchas de estas prácticas es su poderoso sistema
de metadatos. Los metadatos sustentan muchas de las características de RSpec, y RSpec expone el
mismo sistema para su uso.
Has usado metadatos varias veces hasta ahora en tus ejemplos. En este capítulo, veremos más de
cerca cómo funciona. Al final, comprenderá lo suficiente como para dictar exactamente cuándo y cómo
se ejecutan sus especificaciones.
Definición de metadatos
Los metadatos de RSpec resuelven un problema muy específico: ¿ dónde guardo la información sobre
el contexto en el que se ejecutan mis especificaciones? Por contexto, nos referimos a cosas como:
• Configuración de ejemplo (por ejemplo, marcada como omitida o pendiente) • Ubicaciones
del código fuente
• Estado de la ejecución anterior •
Cómo un ejemplo se ejecuta de manera diferente a otros (por ejemplo, necesita un
navegador web o una base de datos)
Sin alguna forma de adjuntar datos a los ejemplos, usted (¡y los administradores de RSpec!) estarían
atrapados haciendo malabares con las variables globales y escribiendo un montón de código de
contabilidad.
informar fe de erratas • discutir
Machine Translated by Google
Capítulo 8. Especificaciones de troceado y troceado con metadatos • 132
La solución de RSpec a este problema no podría ser más simple: un simple hash de Ruby.
Cada ejemplo y grupo de ejemplos obtiene su propio hash, conocido como hash de
metadatos. RSpec completa este hash con cualquier metadato con el que haya etiquetado
explícitamente el ejemplo, además de algunas entradas útiles propias.
Metadatos definidos por RSpec
Un buen ejemplo vale más que mil palabras, así que pasemos directamente a uno. En un
directorio nuevo, cree un archivo llamado metadata_spec.rb con el siguiente contenido:
08metadata/01/spec/metadata_spec.rb
requiere 'pp'
RSpec.describe Hash hacer
' es utilizado por RSpec para metadatos' do |example| pp
ejemplo.fin metadatos
fin
Este fragmento muestra algo de lo que no hemos hablado antes: obtener acceso a las
propiedades de su ejemplo en tiempo de ejecución. Puede hacerlo haciendo que su bloque
tome un argumento. RSpec pasará un objeto que representa el ejemplo que se está
ejecutando actualmente. Volveremos sobre este tema más adelante en el capítulo.
La llamada a example.metadata devuelve un hash que contiene todos los metadatos.
Estamos usando la función pp (abreviatura de prettyprint) de la biblioteca estándar de Ruby
para volcar el contenido del hash en un formato fácil de leer.
Continúe y ejecute el ejemplo:
$ rspec spec/metadata_spec.rb
{:block=>
#<Proc:0x007fa6fc07e6a8@~/code/metadata/spec/
metadata_spec.rb:4>, :description_args=>["es usado por RSpec para
metadatos"], :description =>"es usado por RSpec para
metadatos", :full_description=>"Hash es usado por RSpec para
metadatos", :described_class=>Hash, :file_path=>"./
spec/
metadata_spec.rb", :line_number=>4 , :ubicación=>"./
spec/
metadata_spec.rb:4", :absolute_file_path=> "~/code/
metadata/spec/metadata_spec.rb", :rerun_file_path=>"./
spec/metadata_spec.rb", :
scoped_id=>"1:1", :execution_result=>
#<RSpec::Core::Example::ExecutionResult:0x007ffda2846a78
@started_at=20170613
13:34:00
0700>, :example_group=> {: bloque=> #<Proc:0x007fa6fb914bb0@~/code/metadata/spec/metadata_spec.rb:3>,
informar fe de erratas • discutir
Machine Translated by Google
Definición de metadatos • 133
« truncado »
:shared_group_inclusion_backtrace=>[], :last_run_status=>"desconocido"}
.
Terminado en 0.00279 segundos (los archivos tardaron 0.09431 segundos en
cargarse) 1 ejemplo, 0 fallas
¡Incluso antes de que hayamos definido los metadatos, RSpec ha adjuntado muchos propios! La
mayoría de las claves en este hash se explican por sí mismas, pero algunas merecen una mirada más
cercana:
:descripción
Solo la cadena que le pasamos ; en este caso, "es utilizado por RSpec..."
:full_description
Incluye el texto pasado para describir también; en este caso, "RSpec utiliza hash..."
:clase_descrita
La clase que pasamos al bloque de descripción más externo ; también disponible dentro de
cualquier ejemplo a través del método description_class de RSpec
:ruta de archivo
Directorio y nombre de archivo donde se define el ejemplo, relativo a la raíz de su proyecto; útil
para filtrar ejemplos por ubicación
:ejemplo_grupo
Le da acceso a los metadatos del grupo de ejemplo adjunto
:last_run_status
Será "aprobado", "pendiente", "fallido" o "desconocido"; el último valor aparece si no ha
configurado RSpec para registrar el estado de aprobación/rechazo o si el ejemplo nunca se ha
ejecutado
Como verá en las próximas secciones, tener esta información en tiempo de ejecución será útil.
Metadatos personalizados
Puede contar con que las claves de la sección anterior estarán presentes en cada hash de metadatos.
También pueden estar presentes otras claves, según los metadatos que haya establecido
explícitamente. Actualice metadata_spec.rb para agregar una entrada de metadatos rápida: verdadera :
08metadata/02/spec/metadata_spec.rb
requiere 'pp'
RSpec.describe Hash hacer
informar fe de erratas • discutir
Machine Translated by Google
Capítulo 8. Especificaciones de troceado y troceado con metadatos • 134
RSpec lo usa para metadatos, rápido: true do |example| pp ejemplo.fin metadatos
fin
Este estilo particular de uso, pasar una clave cuyo valor es verdadero, como en rápido: verdadero,
es tan común que RSpec proporciona un atajo. Puede simplemente pasar la clave por sí mismo:
08metadata/03/spec/metadata_spec.rb
requiere 'pp'
RSpec.describe Hash do
' es usado por RSpec para metadatos', :fast do |example|
pp ejemplo.fin metadatos
fin
En cualquier caso, cuando ejecute esta especificación, debería ver :fast=>true en la bonita salida
impresa.
Incluso puede pasar varias claves:
08metadata/04/spec/metadata_spec.rb
requiere 'pp'
RSpec.describe Hash hacer
' es utilizado por RSpec para metadatos', :fast, :focus do |example| pp
ejemplo.metadatos final
final
Verá tanto :fast=>true como :focus=>true cuando ejecute este ejemplo.
Finalmente, cuando configura metadatos personalizados en un grupo de ejemplo, los ejemplos
contenidos y los grupos anidados lo heredarán:
08metadata/04/spec/metadata_inheritance_spec.rb
requiere 'pp'
RSpec.describe Hash, :outer_group do it 'lo usa
RSpec para metadatos', :fast, :focus do |example| pp ejemplo.fin metadatos
contexto 'en un grupo anidado' hacer '
también se hereda' hacer |ejemplo|
pp ejemplo.metadatos
final
final final
Como era de esperar, estas especificaciones imprimirán :outer_group=>true dos veces: una para
el ejemplo en el grupo externo y otra para el ejemplo en el grupo interno.
informar fe de erratas • discutir
Machine Translated by Google
Definición de metadatos • 135
Antes de centrarnos en el uso de metadatos, hablemos de una forma más de configurar metadatos
personalizados.
Metadatos derivados
Como ha trabajado con los ejemplos de este libro, siempre ha establecido metadatos en un ejemplo o
grupo a la vez. A veces, sin embargo, desea establecer metadatos en muchos ejemplos a la vez.
Por ejemplo, puede marcar sus ejemplos de ejecución más rápida como : rápido y luego ejecutar solo
esas especificaciones usando la opción tag de RSpec :
$ rspec etiqueta rápido
Este comando le daría una visión general rápida de la salud de su código. El conjunto de especificaciones
rápidas consistiría en ciertas especificaciones de integración cuidadosamente seleccionadas, además de todo
en especificación/unidad (ya que las especificaciones de su unidad están destinadas a ejecutarse rápidamente).
Sería bueno no tener que etiquetar manualmente cada grupo de ejemplo en especificación/unidad con :
rápido. Afortunadamente, RSpec admite la configuración de metadatos en muchos ejemplos o grupos a la
vez, a través de su API de configuración.
Si agrega el siguiente código a spec/spec_helper.rb:
08metadata/05/spec/spec_helper.rb
RSpec.configure do |config|
config.define_derived_metadata(file_path: /spec\/unit/) do |meta| meta[:rápido] =
final verdadero
fin
…RSpec agregará los metadatos :fast a cada ejemplo en la carpeta spec/unit .
Analicemos cómo funciona este código.
El método define_derived_metdata de RSpec compara cada ejemplo con la expresión de filtro que le
damos. Aquí, la expresión del filtro es file_path: /spec\/unit/, lo que significa "coincidir con ejemplos
definidos dentro del directorio spec/unit ".
Cuando la expresión del filtro coincide, RSpec llama al bloque pasado. Dentro del bloque, podemos
modificar el hash de metadatos como queramos. Aquí, estamos agregando rápido: fiel a los metadatos de
cada ejemplo coincidente. En efecto, estamos filtrando por una pieza de metadatos (:file_path) para
establecer otra (:fast).
En este caso, usó una expresión regular para encontrar todos los nombres de archivo que contienen
especificación/unidad. En otras ocasiones, es posible que necesite que los valores coincidan exactamente.
En el siguiente fragmento, queremos hacer coincidir todas las especificaciones etiquetadas con
type: :model para indicar que están probando nuestros modelos de Rails:
informar fe de erratas • discutir
Machine Translated by Google
Capítulo 8. Especificaciones de troceado y troceado con metadatos • 136
08metadata/06/spec/spec_helper.rb
RSpec.configure do |config|
config.define_derived_metadata(tipo: :modelo) do |meta|
# ...
fin
fin
Detrás de escena, RSpec usa el operador === para comparar valores para su expresión de filtro. Escuchará más
acerca de este operador en Cómo los comparadores relacionan objetos, en la página 176. Por ahora, solo diremos
que este estilo de comparación permite todo tipo de cosas, como usar un Ruby lambda en su expresión de filtro.
Sin embargo, la mayoría de las veces, encontramos que las expresiones regulares y las coincidencias exactas son
lo suficientemente poderosas.
Metadatos predeterminados
En la sección anterior, utilizó metadatos para etiquetar algunos de los ejemplos automáticamente. Ahora, le daremos
un giro a este concepto: le mostraremos cómo habilitar algo de manera predeterminada en todos sus ejemplos,
pero permitir que los ejemplos individuales se desactiven.
De vuelta en Probar el comportamiento del libro mayor, en la página 84, configura los metadatos :aggregate_failures
en sus especificaciones de integración para obtener resultados de fallas más útiles. RSpec generalmente detiene
un ejemplo en la primera expectativa que falla. Con esta etiqueta en su lugar, sus especificaciones de integración
se mantendrían e informarían cada expectativa fallida.
Este aspecto de RSpec es tan útil que puede verse tentado a habilitarlo para cada ejemplo:
08metadata/07/aggregate_failures.rb
RSpec.configure do |config|
config.define_derived_metadata hacer |meta| #
Establece la bandera incondicionalmente;
# no permite que los ejemplos opten por no
participar meta[:aggregate_failures] =
true end
fin
Activar una función globalmente puede ser extremadamente útil en situaciones como esta.
Sin embargo, es una buena idea dejar que los ejemplos individuales opten por no participar en el comportamiento.
Por ejemplo, es posible que tenga algunas especificaciones de facturación que lleguen a una pasarela de pago
falsa. Como control de seguridad adicional, define un enlace anterior que detiene cualquier especificación que
intente usar la puerta de enlace real:
informar fe de erratas • discutir
Machine Translated by Google
Lectura de metadatos • 137
08metadata/07/aggregate_failures.rb
RSpec.describe 'Billing', added_failures: false do context 'usando el
servicio de pago falso' do before do
expect(MyApp.config.payment_gateway).to include('sandbox') end
# ...
fin
fin
A pesar de que added_failures está establecido en falso aquí, la configuración global lo anula.
Eso significa que si uno de sus ejemplos se configura accidentalmente para hablar con la
pasarela de pago real (en lugar de la caja de arena), el anzuelo anterior no lo detendrá.
La solución es fácil: en su llamada a define_derived_metadata, primero verifique si la clave
existe antes de anularla:
08metadata/08/spec/spec_helper.rb
RSpec.configure do |config|
config.define_derived_metadata hacer |meta|
meta[:aggregate_failures] = verdadero a menos que finalice meta.key?(:aggregate_failures)
fin
Ahora, puede configurar la bandera globalmente, pero aún así desactivarla para casos
individuales en los que no desea ese comportamiento.
A continuación, hablemos sobre cómo acceder a los metadatos y darles un buen uso.
Lectura de metadatos
Echa un vistazo más al bloque que escribiste al comienzo del capítulo:
08metadata/08/spec/metadata_spec.rb
' es usado por RSpec para metadatos' do |example| pp
ejemplo.fin metadatos
Como muestra este fragmento, RSpec entrega a su bloque un argumento de ejemplo , desde
el cual puede leer los metadatos. RSpec pasa el mismo tipo de argumento de bloque en
enlaces marcados con :example scope:
08metadata/09/around_hook.rb
RSpec.configure do |config|
config.around(:ejemplo) do |ejemplo|
pp ejemplo.fin metadatos
fin
informar fe de erratas • discutir
Machine Translated by Google
Capítulo 8. Especificaciones de troceado y troceado con metadatos • 138
…y en declaraciones let :
08metadata/10/spec/music_storage_spec.rb
RSpec.describe 'Almacenamiento de
música' do let(:s3_client) do |
example| S3Client.for(ejemplo.metadatos[:s3_adapter])
fin
' almacena música en el S3 real', s3_adapter: :real do
#...
fin
' almacena música en un S3 en memoria', s3_adapter: :memory do
#...
fin
fin
Ambas especificaciones se basan en un cliente S3, pero cada una usa una diferente. Una
especificación se ejecutará con S3Client.for(:real) y la otra con S3Client.for(:memory).
Hasta ahora, hemos visto cómo leer qué metadatos se definen en un ejemplo y cómo escribir
metadatos en uno o más ejemplos. La verdadera diversión comienza cuando activamos RSpec
y usamos toda esta información para cambiar su comportamiento.
Seleccionar qué especificaciones ejecutar
Cuando ejecuta sus especificaciones, a menudo desea cambiar las que incluye. En esta
sección, le mostraremos algunas situaciones diferentes en las que este tipo de rebanado y
troceado resulta útil.
Filtrado La
mayoría de las veces, cuando iniciamos RSpec, no ejecutamos toda la suite. Estamos ejecutando
especificaciones de unidad para una clase específica que estamos diseñando o estamos iniciando
algunas especificaciones de integración para detectar regresiones.
Primero abordamos la ejecución de ejemplos específicos en Ejecutar justo lo que necesita, en
la página 20. Vio cómo seleccionar ejemplos por estado de aprobación/reprobación, por
nuestra necesidad de enfocarnos en ellos y por etiquetas personalizadas. Profundicemos más
en cómo controlar qué especificaciones ejecutar.
Exclusión de ejemplos A
veces, desea excluir un conjunto de ejemplos de su ejecución de RSpec. Por ejemplo, cuando
está probando la compatibilidad de un proyecto entre los intérpretes de Ruby, puede tener
algunos ejemplos que son específicos de la implementación. Puede asignarles una etiqueta
como : jruby_only, y luego usar su bloque RSpec.configure para omitir esas especificaciones
cuando esté probando en otro intérprete de Ruby:
informar fe de erratas • discutir
Machine Translated by Google
Seleccionar qué especificaciones ejecutar • 139
08metadata/11/spec/spec_helper.rb
RSpec.configure do |config|
config.filter_run_excluyendo : jruby_only a menos que RUBY_PLATFORM == final 'java'
Aquí, la llamada filter_run_excluyendo indica qué ejemplos estamos omitiendo.
Incluyendo ejemplos
La otra cara de ese método es filter_run_inclusive, o simplemente filter_run para abreviar. Como
habrás adivinado, este método indica a RSpec que ejecute solo los ejemplos con metadatos
coincidentes.
Este estilo de filtrado es bastante de fuerza bruta. Si ningún ejemplo coincide con el filtro, RSpec
no ejecutará nada en absoluto.
Un enfoque más útil en general es usar filter_run_when_matching. Con este método, si nada
coincide con el filtro, RSpec simplemente lo ignora. Por ejemplo, si no ha marcado ninguna
especificación como enfocada (mediante los métodos fdescribe/fcontext/fit o mediante el enfoque:
metadatos verdaderos ), el siguiente filtro no tendrá efecto:
08metadata/12/spec/spec_helper.rb
RSpec.configure do |config|
config.filter_run_when_matching : fin del foco
En esta sección, hemos usado RSpec.configure para definir filtros de ejemplo. Estas son
configuraciones permanentes, integradas en su código de configuración. Estarán en vigor cada
vez que ejecute RSpec.
A veces, sin embargo, desea filtrar ejemplos temporalmente para solo una o dos ejecuciones de
RSpec. Editar su archivo spec_helper.rb cada vez envejecería rápidamente. En su lugar, puede
utilizar la interfaz de línea de comandos de RSpec.
La línea de comando
Para ejecutar solo las especificaciones que coinciden con un metadato en particular, pase la
opción tag a rspec. Por ejemplo, es posible que desee ejecutar solo los ejemplos etiquetados
con :rápido:
$ rspec etiqueta rápido
Si antecede el nombre de la etiqueta con una tilde (~), RSpec trata el nombre como un filtro de
exclusión . Por ejemplo, para ejecutar todos los ejemplos que carecen de la etiqueta :fast , puede
usar el siguiente comando:
$ rspec tag ~rápido
informar fe de erratas • discutir
Machine Translated by Google
Capítulo 8. Especificaciones de troceado y troceado con metadatos • 140
Tenga en cuenta que algunos shells tratan a ~ como un carácter especial e intentan expandirlo a
un nombre de directorio. Puede evitar este problema citando el nombre de la etiqueta:
$ rspec tag '~rápido'
Los ejemplos anteriores buscan la veracidad de la etiqueta; cualquier valor además de nulo o
falso coincidirá. A veces, te preocupas por el valor de la etiqueta específica. Por ejemplo, puede
tener varias especificaciones etiquetadas con un ID de error de su sistema de seguimiento de
errores. Si ejecuta RSpec así:
$ rspec tag bug_id:123
…puede filtrar los ejemplos solo para aquellos relacionados con el ticket en el que está trabajando.
Compartir código condicionalmente
En Ejemplos de estructuración de código, discutimos tres formas de compartir código entre
muchos grupos de ejemplo:
• Ganchos de configuración de
nivel superior • Módulos que contienen métodos
auxiliares • Contextos compartidos que contienen construcciones RSpec (como ganchos y let
bloques)
Ha utilizado todas estas técnicas a lo largo de este libro. Por defecto, todos comparten código
incondicionalmente. Si define, digamos, un enlace anterior en su bloque de figura RSpec.con ,
el enlace se ejecutará para cada ejemplo.
Sin embargo, a menudo querrás usar un cierto fragmento de código compartido solo para
ejemplos específicos. Por ejemplo, en Aislamiento de sus especificaciones mediante transacciones
de base de datos, en la página 92, definió un enlace para envolver una transacción de base de
datos solo en los ejemplos etiquetados con :db:
06integrationspecs/07/expense_tracker/spec/support/db.rb
c.around(:ejemplo, :db) do |ejemplo|
DB.transaction(rollback: :siempre) { ejemplo.ejecutar } fin
Los metadatos son lo que permite esta flexibilidad y puede usarlos con todas las técnicas de
código compartido enumeradas anteriormente. Así es cómo:
Enlaces de
configuración Pase una expresión de filtro como segundo argumento a config.before,
config.after o config.around para ejecutar ese enlace solo para ejemplos que coincidan con el filtro.
informar fe de erratas • discutir
Machine Translated by Google
Cómo cambiar el funcionamiento de sus especificaciones • 141
Módulos
Agregue una expresión de filtro al final de su llamada config.include para incluir un módulo (y
sus métodos auxiliares) de forma condicional. Esto también funciona con los métodos
config.extend y config.prepend similares de RSpec , que se tratan en los documentos.1
Contextos compartidos
Al igual que con los módulos, agregue una expresión de filtro al llamar al texto
config.include_con. Esto traerá sus construcciones let compartidas (entre otras cosas) solo a
los grupos de ejemplo que desee.
Todas las técnicas de esta sección lo ayudan a compartir selectivamente el comportamiento entre
sus ejemplos, en función de los metadatos. Antes de concluir este capítulo, echemos un vistazo a
una forma más de usar los metadatos de RSpec: para controlar cómo se ejecutan sus especificaciones.
Cambiar el funcionamiento de sus especificaciones
RSpec le permite cambiar la forma en que se comportan sus especificaciones usando metadatos.
Ya has usado esta habilidad varias veces mientras trabajabas en este libro.
Estas son las opciones de metadatos que afectan la forma en que RSpec ejecuta sus ejemplos:
:agregar_fallas
Cambia la forma en que RSpec reacciona ante un error para que cada ejemplo se ejecute
hasta el final (en lugar de detenerse en la primera expectativa fallida).
:pendiente
Indica que espera que el ejemplo falle; RSpec lo ejecutará y lo informará como pendiente si
falló, o lo informará como una falla si pasó
:saltar
Le dice a RSpec que omita el ejemplo por completo, pero que aún incluya el ejemplo en la
salida (a diferencia del filtrado, que omite el ejemplo de la salida)
:orden
Establece el orden en el que RSpec ejecuta sus especificaciones (puede ser el mismo orden
en que están definidas, un orden aleatorio o un orden personalizado)
Tanto :pending como :skip toman una explicación opcional de por qué esta especificación no debe
ejecutarse normalmente, que RSpec imprimirá en la salida.
Como discutimos en Probar el caso no válido, en la página 89, RSpec es capaz de ejecutar sus
especificaciones en orden aleatorio y, en general, le recomendamos que lo haga. La alternativa
principal es :defined, lo que significa que RSpec ejecuta sus ejemplos en el orden en que ve sus
definiciones.
1. http://rspec.info/documentation/3.6/rspeccore/RSpec/Core/Configuration.html#extendinstance_method
informar fe de erratas • discutir
Machine Translated by Google
Capítulo 8. Especificaciones de troceado y troceado con metadatos • 142
La elección entre :definido y :aleatorio no necesita ser uno u otro. Si está migrando un paquete RSpec
completo del primero al último, puede ser difícil hacer el cambio de una sola vez. Como vio en el
ejercicio de seguimiento de gastos, los ejemplos pueden tener dependencias de orden ocultas.
Puede usar metadatos para hacer que la transición al orden aleatorio sea más gradual. Al etiquetar un
grupo de ejemplo con order: :random, puede ejecutar solo los ejemplos en ese grupo al azar:
08metadata/13/random_order.rb
RSpec.describe SomeNewExampleGroup, order: :random do
#...
fin
Una vez que haya hecho la transición de todos sus grupos al orden aleatorio, puede eliminar estos
metadatos y luego activar la aleatoriedad para todo el conjunto en su bloque RSpec.configure .
En raras ocasiones, necesitará un control aún más detallado sobre el pedido de especificaciones. Por
ejemplo, es posible que desee ejecutar todas las especificaciones de su unidad primero, o ejecutar
todas sus especificaciones en orden de la más rápida a la más lenta. En estos casos, puede configurar
los metadatos de :order en un orden personalizado. Los documentos de RSpec explican cómo hacerlo.2
Tu turno
En este capítulo, ha visto cómo rebanar y trocear sus especificaciones de cualquier manera que se le
haya ocurrido. Puede ejecutar solo los ejemplos más rápidos, o los de una plataforma específica, o los
que está enfocando para una tarea en particular. El resultado es que pasa menos tiempo esperando
que se ejecuten las especificaciones y más tiempo escribiendo código.
También hemos descorrido el telón para mostrarte que la magia detrás de esta flexibilidad es un hachís
de Ruby ordinario. Puede definir fácilmente las secciones transversales de sus especificaciones para
adaptarse a cualquier situación.
Ahora es el momento de poner a prueba esta nueva experiencia.
Ejercicio
Cuando tratamos de encontrar cuellos de botella en nuestras especificaciones, a menudo observamos
las operaciones de SQL, una de las principales fuentes de lentitud en las pruebas. Puede ser realmente
útil saber qué declaraciones SQL provienen de qué ejemplos. Con su conocimiento de cómo funcionan
los metadatos, puede configurar RSpec para reportar esta información.
2. http://rspec.info/documentation/3.6/rspeccore/RSpec/Core/Configuration.html#register_orderinginstance_method
informar fe de erratas • discutir
Machine Translated by Google
Tu turno • 143
En Aislamiento de sus especificaciones mediante transacciones de bases de datos, en la página 92,
etiquetó varios ejemplos con los metadatos :db para indicar que usan la base de datos a través de la
biblioteca Sequel. En este ejercicio, modificará el comportamiento de RSpec en función de estos
metadatos.
Primero, abra spec/support/db.rb y agregue las siguientes líneas al gancho before(:suite) :
08metadata/ejercicios/expense_tracker/spec/support/db.rb
FileUtils.mkdir_p('registro')
requiere 'registrador'
DB.loggers << Registrador.nuevo('registro/sequel.registro')
Este código configurará Sequel para registrar cada instrucción SQL ejecutada en el archivo de registro.
Ahora, use las técnicas de este capítulo para asegurarse de que Sequel también escriba las
descripciones de los ejemplos en su registro:
• Antes de que se ejecute cada ejemplo, escriba: Ejemplo inicial: #{example_description} •
Después de que se ejecute cada ejemplo, escriba: Ejemplo final: #{example_description}
Sugerencia: DB.log_info('algún mensaje') escribirá cualquier texto que necesite en el registro de Sequel.
informar fe de erratas • discutir
Machine Translated by Google
En este capítulo, verá:
• Cómo cambiar el comportamiento de RSpec en la línea de
comandos • Cómo personalizar la salida
de RSpec • Dónde guardar las opciones de línea de comandos más
utilizadas • Cómo configurar RSpec
en el código • Qué opciones de configuración serán útiles en sus proyectos
CAPÍTULO 9
Configuración de RSpec
A medida que ha trabajado en los ejercicios de este libro, a menudo ha cambiado el comportamiento de
RSpec para convertirlo en una mejor herramienta para sus necesidades. Estas son solo algunas de las cosas
que ha personalizado RSpec para que haga por usted:
• Configurar y desmantelar una base de datos de prueba, pero solo para los ejemplos que
requiere uno
• Informe cada expectativa fallida en un ejemplo, no solo la primera
• Ejecute solo los ejemplos en los que se está enfocando en este momento
En este capítulo, vamos a conectar los puntos entre todas estas personalizaciones individuales. Al final, sabrá
cómo configurar mucho más que unos pocos ajustes individuales. Tendrá una sólida visión general del
sistema de configuración de RSpec. En lugar de tener una herramienta de uso general, tendrá un entorno de
prueba hecho a medida para adaptarse a su flujo de trabajo.
Puede configurar RSpec de dos formas básicas:
• Un bloque RSpec.configure : proporciona acceso a todas las opciones de configuración; dado que este
bloque vive en su código, normalmente lo usará para realizar cambios permanentes
• Opciones de línea de comandos: proporciona acceso a algunas opciones de configuración,
configuraciones típicamente únicas que afectarán una ejecución específica de rspec
Vamos a echar un vistazo más de cerca a estas dos categorías, comenzando con las opciones de la línea de
comandos.
Configuración de la línea de comandos
Para ver todas las opciones de línea de comandos disponibles, ejecute rspec help. Obtendrá una lista
bastante grande de configuraciones, algunas de las cuales ya usó en este libro.
informar fe de erratas • discutir
Machine Translated by Google
Capítulo 9. Configuración de RSpec • 146
No vamos a repasarlos todos aquí, pero hay algunos que nos gustaría destacar.
Opciones de entorno
A veces, necesita controlar cómo RSpec carga su código Ruby. Por ejemplo, puede estar
experimentando con una versión modificada localmente de una biblioteca; en ese caso,
querrá que RSpec cargue su versión personalizada de esa biblioteca en lugar de la
predeterminada.
Las primeras dos opciones enumeradas en la salida help son para este tipo de entorno.
personalizaciones ment:
I CAMINO Especifique PATH para agregar a $LOAD_PATH (se puede
usar más de una vez).
r, requiere RUTA Requiere un archivo.
Si alguna vez ha pasado las opciones I o r al ejecutable de Ruby , estos modificadores
RSpec pueden parecerle familiares: fueron diseñados para coincidir con los de Ruby.
La segunda opción, r o require, facilita el uso de bibliotecas de soporte mientras realiza
pruebas. Por ejemplo, es posible que desee utilizar el depurador byebug para solucionar un
error en la especificación.1 Puede habilitar fácilmente la depuración para una sola ejecución
de RSpec utilizando la opción r junto con el nombre de la biblioteca:
$ rspecrbyebug
La otra opción en este grupo, I, agrega un directorio a la ruta de carga de Ruby.2 Esto
ayuda a Ruby a encontrar bibliotecas que carga desde una instrucción require o el
modificador require .
RSpec ya agrega los dos directorios más importantes a la ruta de carga: las carpetas lib y
spec de su proyecto . Pero a veces es posible que desee utilizar una biblioteca en particular
sin pasar por Bundler o RubyGems; ahí es donde esta bandera es útil.
Opciones de filtrado
Ahora que hemos cubierto las opciones que configuran el entorno para sus
especificaciones, hablemos de las opciones que rigen cuál de sus especificaciones se ejecutará RSpec.
Ejecutar solo las especificaciones que necesita en un momento dado lo hará mucho más
productivo. Con ese fin, RSpec admite una serie de opciones de filtrado, que se enumeran
más abajo en la salida help :
1. https://github.com/deividrodriguez/byebug
2. http://webappsforbeginners.rubymonstas.org/libraries/load_path.html
informar fe de erratas • discutir
Machine Translated by Google
Configuración de la línea de comandos • 147
**** Filtrado/etiquetas ****
Además de las siguientes opciones para seleccionar archivos específicos,
grupos o ejemplos, puede seleccionar ejemplos individuales agregando
los números de línea al nombre del archivo:
rspec ruta/a/a_spec.rb:37:87
También puede pasar identificadores de ejemplo encerrados entre corchetes:
rspec ruta/a/a_spec.rb[1:5,1:6] # ejecutar los ejemplos/grupos 5 y 6
definido en el 1er grupo
solo fallas Filtre solo los ejemplos que fallaron la última vez
que se ejecutaron.
siguientefracaso Aplique ̀onlyfailures` y cancele después de una
falla. (Equivalente a ̀onlyfailures failfast
order
definido`)
P, patrón PATRÓN Cargue archivos que coincidan con el patrón (predeterminado:
"especificación/**/*_especificación.rb").
excludepatrón PATRÓN Cargue archivos excepto aquellos que coincidan
con el patrón. Efecto opuesto de pattern.
e, ejemplo CADENA Ejecutar ejemplos cuyos nombres anidados completos
incluir STRING (se puede usar más de
una vez)
t, tag ETIQUETA[:VALOR] Ejecutar ejemplos con la etiqueta especificada,
o excluya ejemplos agregando ~ antes de
la etiqueta.
por ejemplo, ~lento
TAG siempre se convierte en un símbolo
ruta predeterminada RUTA Establezca la ruta predeterminada donde se ve RSpec
por ejemplo (puede ser una ruta a un archivo
o un directorio).
Ha utilizado varias de estas opciones a lo largo de este libro. Por ejemplo, tu
pasó onlyfailures para ejecutar solo las especificaciones que fallaron en la ejecución anterior de RSpec.
Viste cómo usar tag para ejecutar solo las especificaciones que se etiquetaron con una pieza
de metadatos, como :fast.
No vamos a perder mucho tiempo repasando estas banderas en detalle.
por segunda vez. Pero vale la pena reunir las opciones más comunes en una
lugar y explicando cuándo cada uno es útil:
rspec ruta/a/a_spec.rb:37
Agregar un número de línea a sus nombres de archivo es la forma más sencilla de ejecutar un
ejemplo o grupo en particular, particularmente si ha configurado su texto
editor para hacerlo con una pulsación de tecla.
informar fe de erratas • discutir
Machine Translated by Google
Capítulo 9. Configuración de RSpec • 148
onlyfailures Cada
vez que haya fallado en las especificaciones, generalmente son en las que desea centrar su atención. Esta opción facilita volver a
ejecutar solo los errores.
siguientefracaso
Una forma más quirúrgica de onlyfailures, esta opción es buena cuando desea corregir y
probar cada falla una por una.
ejemplo 'parte de una descripción'
Esta opción es útil cuando desea ejecutar un ejemplo o grupo en particular, y puede recordar
parte de la descripción pero no qué línea
esta encendido.
tag nombre_etiqueta
Si etiqueta cuidadosamente sus ejemplos y grupos con los metadatos apropiados, esta
poderosa opción le permitirá ejecutar secciones transversales arbitrarias de su suite.
Al ejecutar exactamente las especificaciones que necesita, minimiza el tiempo que tiene que esperar
para recibir comentarios sobre los cambios en su código.
Opciones de salida
Diferentes situaciones requieren diferentes niveles de detalle en la salida de su conjunto de pruebas.
En otra parte del texto help , verá una serie de opciones de salida:
**** Producción ****
f, formato FORMATO Elija un formateador.
[p]rgreso (predeterminado puntos)
[d]ocumentación (grupo y ejemplo
nombres)
[h]tml
[j]son
nombre de clase de formateador personalizado
o, out ARCHIVO Escriba la salida en un archivo en lugar de
$stdout. Esta opción se aplica al formato
especificado previamente, o al formato predeterminado
si no se ha seleccionado ningún formato.
especificado.
deprecationout ARCHIVO Escriba advertencias de obsolescencia en un archivo
en lugar de $stderr.
b, backtrace Habilite el rastreo completo.
forcecolor, forcecolor Forzar que la salida sea en color, incluso
si la salida no es un TTY Obliga a
sin color, sin color que la salida no sea en color, incluso si la salida es
un TTY Habilita la creación de perfiles
p, [no]perfil [CONTAR] de ejemplos y lista los ejemplos más lentos
(predeterminado: 10).
informar fe de erratas • discutir
Machine Translated by Google
Configuración de los valores predeterminados de la línea de comandos • 149
ejecución en seco Imprima la salida del formateador de su suite
sin ejecutar ningún ejemplo o ganchos
w, advertencias Habilitar advertencias rubí
Llegaremos a formateadores en un momento. Por ahora, echemos un vistazo más de cerca a tres
de las otras opciones en esta sección:
retroceder
RSpec normalmente trata de mantener cortos los rastreos de errores; excluye líneas de RSpec
y de cualquier gema que haya configurado. Cuando necesite más contexto para la depuración,
puede pasar backtrace (o simplemente b) y ver la pila de llamadas completa.
dryrun
Esta opción, combinada con format doc, es una forma útil de obtener rápidamente resultados
similares a los de una documentación para su proyecto, siempre y cuando haya tenido cuidado
de redactar bien su ejemplo y descripciones de grupos.
warnings
El modo de advertencia de Ruby puede señalar algunos errores comunes, como errores
ortográficos de variables de instancia. Desafortunadamente, Ruby imprimirá advertencias para
todo el código en ejecución, incluidas las gemas. Si está desarrollando una aplicación con
muchas dependencias, es probable que obtenga mucho ruido en la salida. Pero si está
desarrollando una biblioteca simple, esta opción puede ser útil.
Estas opciones lo ayudarán a profundizar en los detalles cuando esté diagnosticando una falla, sin
saturar el resultado de cada ejecución de prueba.
Configuración de valores predeterminados de la línea de comandos
Reunimos estas opciones de la línea de comandos en un capítulo para que pueda consultarlas
cuando quiera hacer algo especial para una sola ejecución de prueba. A veces, sin embargo, es
posible que necesite un comportamiento personalizado para cada ejecución.
En lugar de perder el tiempo escribiendo las mismas opciones una y otra vez, puede guardar un
conjunto de argumentos como valores predeterminados en la línea de comandos. Como implica el
término, RSpec los usará de forma predeterminada para cada ejecución, pero aún puede anularlos.
Para establecer los valores predeterminados, guarde las opciones deseadas en un archivo de texto en cualquiera de las
siguientes tres rutas:
~/.rspec
Use este archivo en su directorio de inicio para almacenar preferencias personales globales.
RSpec lo usará para cualquier proyecto en su máquina. Por ejemplo, es posible que prefiera
detener la ejecución de la prueba en el primer ejemplo fallido, mientras que su
informar fe de erratas • discutir
Machine Translated by Google
Capítulo 9. Configuración de RSpec • 150
los compañeros de equipo podrían no hacerlo. En este caso, podría poner failfast en su
archivo ~/.rspec , y esta configuración se aplicaría solo para usted.
./.rspec
Este archivo en el directorio raíz de un proyecto es para valores predeterminados a nivel de
proyecto. Use moderación aquí; solo coloque las opciones que sean necesarias para que la
suite funcione correctamente, o para los estándares que su equipo acuerde. Por ejemplo, si hay
un archivo que siempre desea cargar, puede solicitarlo automáticamente.
(De hecho, cuando genera un nuevo proyecto con rspec init, RSpec coloca require
spec_helper en el archivo .rspec del proyecto por usted).
./.rspeclocal
Este archivo, que se encuentra junto al archivo .rspec de un proyecto , es para sus preferencias
personales para ese proyecto. Dado que todos pueden tener su propia versión de este archivo,
asegúrese de excluirlo de su sistema de control de código fuente.
Las opciones tienen prioridad en el orden en que las hemos enumerado aquí, lo que significa que las
opciones locales anularán las más globales. Por ejemplo, si su proyecto tiene profile 5 establecido
en su archivo .rspec , puede anular esta configuración colocando noprofile en el archivo .rspec
local del proyecto .
También puede establecer valores predeterminados de línea de comandos en la variable de entorno SPEC_OPTS ; los
valores establecidos aquí anularán los establecidos en los archivos de texto.
Antes de pasar a la siguiente forma de configurar RSpec, tomemos un momento para poner en
práctica este conocimiento. Vamos a configurar RSpec para usar un formateador personalizado para
obtener el informe de salida exacto que queremos.
Uso de un formateador personalizado
En Personalización de la salida de sus especificaciones, en la página 16, usó los formateadores
integrados de RSpec para ver diferentes niveles de detalle en la salida de sus especificaciones.
Ahora, usará un formateador personalizado para hacer un pequeño ajuste en la forma en que RSpec
informa fallas.
Un formateador personalizado es una clase regular de Ruby que se registra con RSpec para recibir
notificaciones. A medida que se ejecuta su suite, RSpec notifica al formateador de los eventos a los
que está suscrito, como iniciar un grupo de ejemplo, ejecutar un ejemplo o encontrar una falla. Este
sistema flexible le da espacio para todo tipo de cambios creativos en la salida. Aquí, vamos a cambiar
la forma en que RSpec informa fallas.
Los formateadores incorporados de RSpec muestran detalles de fallas (mensajes y seguimientos) al
final de la ejecución. Este es un buen valor predeterminado, ya que coloca una lista de trabajo útil al
final de la salida donde es fácil de encontrar.
informar fe de erratas • discutir
Machine Translated by Google
Uso de un formateador personalizado • 151
Sin embargo, a medida que su conjunto de especificaciones crece y comienza a demorar más en
completarse, puede ser bueno ver los detalles de fallas tan pronto como ocurren. De esa manera,
puede comenzar a investigar una falla mientras el resto de la suite continúa ejecutándose.
Hemos creado un formateador personalizado para usted que realiza este cambio en la salida de
RSpec. En esta sección, instalará el formateador y configurará RSpec para usarlo. Luego,
profundizaremos en cómo funciona el formateador.
Configuración del formateador
El nuevo formateador se llama rspecprint_failures_eagerly. Lo hemos hecho disponible como
una gema, pero vas a clonar su fuente en tu máquina en lugar de usar la instalación de gemas.
De esta manera, podrá usarlo para todos los proyectos en su máquina, en lugar de solo los que
lo mencionan en su Gemfile.
Primero, clone el código fuente del formateador en su directorio de inicio:
$ clon de git https://github.com/rspec3book/rspecprint_failures_eagerly.git
Ahora, para cargar la nueva biblioteca para cada proyecto RSpec, coloque el siguiente contenido
en un archivo llamado .rspec en su directorio de inicio:
09configuringrspec/02/configuring_rspec/.rspec
I<%= ENV['HOME'] %>/rspecprint_failures_eagerly/lib require 'rspec/
print_failures_eagerly'
Como no dependerá de Bundler o RubyGems para administrar $LOAD_PATH de Ruby, tendrá
que hacerlo aquí. La primera línea, que comienza con I, hace que el código del formateador esté
disponible para RSpec. La segunda línea realmente carga la biblioteca.
RSpec es compatible con la sintaxis de la plantilla ERB (Ruby incrustado) en este archivo, y lo
estamos usando para leer el valor de la variable de entorno HOME.3
Una vez finalizada la configuración, puede ejecutar rspec y ver el formateador en acción. Así es
como se ve la salida en una suite con dos ejemplos de aprobación y dos fallas:
$ rspec a_spec.rb F
1) Un grupo con un fracaso tiene un ejemplo que falla
Fallo/Error: expect(1).to eq 2
esperado: 2
obtenido: 1
(comparado usando ==)
# ./a_spec.rb:3:in ̀bloque (2 niveles) en <superior (obligatorio)>'
3. https://codingbee.net/tutorials/ruby/rubytheerbtemplatingsystem
informar fe de erratas • discutir
Machine Translated by Google
Capítulo 9. Configuración de RSpec • 152
.F
2) Otro grupo con un fracaso tiene un ejemplo que falla
Fallo/Error: expect(1).to eq 2
esperado: 2
obtenido: 1
(comparado usando ==)
# ./a_spec.rb:13:in ̀bloque (2 niveles) en <superior (obligatorio)>'
.
Terminado en 0.0307 segundos (los archivos tardaron 0.08058 segundos en
cargarse) 4 ejemplos, 2 fallas
Ejemplos fallidos:
rspec ./a_spec.rb:2 # Un grupo con una falla tiene un ejemplo que falla rspec ./a_spec.rb:12
# Otro grupo con una falla tiene un ejemplo que falla
RSpec usó su formateador de progreso predeterminado (con puntos para aprobar especificaciones
y F para fallar). Pero mira dónde aparece el primer mensaje de error: después de la F pero antes
del punto que marca el segundo ejemplo. Gracias al nuevo formateador personalizado, RSpec
está imprimiendo fallas tan pronto como ocurren.
El nuevo formateador también funciona con el formateador de documentación integrado de RSpec:
$ rspec a_spec.rb format doc
Un grupo con una falla tiene
un ejemplo que falla (FAILED 1)
1) Un grupo con un fracaso tiene un ejemplo que falla
Fallo/Error: expect(1).to eq 2
esperado: 2
obtenido: 1
(comparado usando ==)
# ./a_spec.rb:3:in ̀bloque (2 niveles) en <superior (obligatorio)>'
tiene un ejemplo que tiene éxito
Otro grupo con una falla tiene un
ejemplo que falla (FAILED 2)
2) Otro grupo con un fracaso tiene un ejemplo que falla
Fallo/Error: expect(1).to eq 2
esperado: 2
obtenido: 1
(comparado usando ==)
informar fe de erratas • discutir
Machine Translated by Google
Uso de un formateador personalizado • 153
# ./a_spec.rb:13:in ̀bloque (2 niveles) en <superior (obligatorio)>'
tiene un ejemplo que tiene éxito
Finalizó en 0,03249 segundos (los archivos tardaron 0,08034 segundos en cargarse)
4 ejemplos, 2 fracasos
Ejemplos fallidos:
rspec ./a_spec.rb:2 # Un grupo con una falla tiene un ejemplo que falla
rspec ./a_spec.rb:12 # Otro grupo con una falla tiene un ejemplo que falla
Como antes, estamos viendo mensajes de error intercalados con el resto de la
salida, en lugar de recopilarse al final. Ahora que ha configurado RSpec para
utiliza este formateador y lo ha visto en acción, veamos cómo funciona
en realidad funciona
Cómo funcionan los formateadores
Un formateador pasa por tres pasos principales:
1. Registrarse en RSpec para recibir notificaciones específicas
2. Inicializarse al comienzo de la ejecución de RSpec
3. Reaccionar a los eventos a medida que ocurren
A medida que hablamos de esos pasos en detalle, es posible que desee ver los pasos del formateador
código fuente. Si abre rspecprint_failures_eagerly/lib/rspec/print_failures_eagerly.rb desde
su directorio de inicio, verá la siguiente clase de Ruby:
09configuraciónrspec/02/configuración_rspec/rspec/print_failures_eagerly.rb
RSpec del módulo de la línea 1
módulo PrintFailuresEagerly
formateador de clases
RSpec::Core::Formatters.register self, :example_failed
5
def inicializar (salida)
@salida = salida
@last_failure_index = 0
fin
10
def ejemplo_fallido(notificación)
@output.puts
@salida.pone notificación.completamente_formateada(@last_failure_index += 1)
@output.puts
15 fin
fin
fin
fin
informar fe de erratas • discutir
Machine Translated by Google
Capítulo 9. Configuración de RSpec • 154
Echemos un vistazo más de cerca a cómo funciona esta clase:
1. En la línea 4, registramos el formateador con RSpec, pasándole una lista de eventos sobre los que
queremos notificaciones. Aquí, solo nos importa el evento :example_failed . Para ver qué otras
notificaciones de eventos están disponibles para los formateadores, consulte los documentos de la
API.4
2. En el inicializador del formateador en la línea 6, almacenamos el argumento de salida que RSpec nos
pasa, para que sepamos dónde enviar los mensajes de error. Este objeto es una instancia estándar
de Ruby IO : ya sea el flujo de salida estándar o un archivo en el disco. También configuramos un
índice para rastrear qué falla vimos por última vez; lo necesitaremos más adelante para enumerar
las fallas en la salida.
3. La línea 11 es el corazón de nuestro formateador: la devolución de llamada example_failed que
registramos anteriormente. RSpec nos pasará un objeto de 'notificación' que contiene detalles
sobre el evento. Este objeto también tiene útiles asistentes de formato, como el método
full_formatted que devuelve el resultado de un error específico.
Ahora hemos visto cómo funciona el formateador una vez que se está ejecutando, pero no hemos
hablado de cómo se configura para ejecutarse en primer lugar. Hagámoslo ahora.
Conseguir que RSpec utilice el formateador
Los usuarios del nuevo formateador iniciarán RSpec con la opción require 'rspec/print_failures_eagerly' .
Ese indicador cargará la clase PrintFailuresEagerly::Formatter en la memoria, pero algún fragmento de
código necesitará configurar RSpec para usar esta clase como formateador.
Para formateadores más simples, RSpec proporciona una API de configuración llamada add_formatter.
Si estuviera usando esta API, llamaría al asistente dentro de un bloque estándar RSpec.configure así:
09configuringrspec/02/configuring_rspec/spec/
spec_helper.rb RSpec.configure
do |config| config.add_formatter final de
MyFormatter
Sin embargo, se necesitará un poco más de delicadeza para configurar este formateador. El código que
acabamos de ver imprimirá solo mensajes de error. Pero también queremos ver cualquier otro resultado
que aparezca normalmente, como puntos de progreso o descripciones de ejemplos.
Necesitaremos otro formateador para suministrar la mayor parte de la salida. Podríamos simplemente
confiar en que los usuarios pasen un formateador explícitamente a RSpec a través de la línea de comando,
4. http://rspec.info/documentation/3.6/rspeccore/RSpec/Core/Formatters/Protocol.html
informar fe de erratas • discutir
Machine Translated by Google
Uso de un formateador personalizado • 155
como en rspec formatter doc. Pero sería mejor no requerir este paso adicional y, en su lugar,
usar el formateador predeterminado de RSpec si no se pasa ninguno.
El bloque de configuración que mostramos aquí evitaría que RSpec proporcione un formateador
predeterminado. En cambio, debemos esperar hasta que se haya configurado el formateador y
luego agregar el nuestro. Para hacerlo, hemos puesto nuestro código de inicialización en un
gancho anterior (: suite) :
09configuringrspec/02/configuring_rspec/rspec/print_failures_eagerly.rb
RSpec.configure do |config|
config.before(:suite) hacer
config.add_formatter RSpec::PrintFailuresEagerly:: Fin del formateador
fin
Esta biblioteca debe ocuparse de un último detalle: limpiar la salida, específicamente, evitando
que los errores se impriman dos veces. En la siguiente sección, veremos cómo hacerlo.
Limpieza de la salida Los
formateadores RSpec normalmente imprimen una lista de fallas al final de su salida.
Dado que ya mostramos los mensajes de falla durante la ejecución de RSpec, no necesitamos
mostrarlos por segunda vez.
Tanto el formateador de progreso como el de documentación escuchan el evento del formateador
dump_failures que RSpec envía al final de una ejecución de prueba. Reaccionan a esta
notificación imprimiendo una lista de fallas. Este comportamiento vive en un método
dump_failures común en la clase BaseTextFormatter heredada por los dos formateadores.
Hemos definido nuestra propia versión de este método que no imprime nada para que no
recibamos mensajes de error duplicados. Este nuevo método dump_failures entra en un módulo
llamado SilenceDumpFailures, que luego podemos anteponer a la clase base del formateador
RSpec para deshacernos de la salida adicional:
09configuringrspec/02/configuring_rspec/rspec/print_failures_eagerly.rb
module SilenceDumpFailures def
dump_failures(_notification) end
RSpec::Core::Formatters::BaseTextFormatter.prepend(self) end
Para anular el comportamiento de los formateadores, necesitábamos parchear RSpec, es decir,
cambiar su comportamiento abriendo una clase RSpec central y modificando
informar fe de erratas • discutir
Machine Translated by Google
Capítulo 9. Configuración de RSpec • 156
de nuestro código.5 Aquí, el riesgo es mínimo. El método que estamos modificando es parte del protocolo de
formateador publicado de RSpec y, por lo tanto, no cambiará sin un cambio importante en la versión de
RSpec. Sin embargo, en general, la aplicación de parches a los monos debería ser una herramienta de último
recurso.
Esta pequeña cantidad de código Ruby es todo lo que se necesita para definir y configurar este formateador
para que pueda usarlo desde la línea de comandos. Ahora, pasemos a la otra forma principal de configurar
RSpec: el método de configuración .
RSpec.configure
Ha visto lo fácil que puede establecer las opciones de configuración para una ejecución de especificación
particular a través de la línea de comandos. También ha visto cómo hacer que sus opciones favoritas sean
las predeterminadas usando archivos .rspec .
Tan convenientes como son, los indicadores de la línea de comandos no están disponibles para todas las
opciones de RSpec, solo las que es probable que cambie de una ejecución a otra. Para el resto, deberá
llamar a RSpec.configure dentro de uno o más archivos de Ruby. Puede tener múltiples bloques de
configuración en su base de código; si lo hace, RSpec combinará las opciones de todos ellos.
En un proyecto típico, colocará la configuración en spec/spec_helper.rb y luego cargará este archivo
automáticamente agregando require spec_helper a su archivo .rspec .
Tenga cuidado con lo que carga desde spec_helper.rb
Es fácil que su archivo spec_helper se atasque con código que no necesita para cada
especificación, convirtiendo una ejecución de especificación que normalmente terminaría en
cientos de milisegundos en un multisegundo "Me pregunto qué es interesante en Twitter".
sudar tinta.
Tendrá una experiencia TDD mucho más agradable si limita spec_helper para cargar solo
las dependencias que siempre desea. Si necesita una biblioteca solo para un subconjunto
de especificaciones, cárguela de forma condicional realizando una de las siguientes
acciones:
• Agregue un gancho when_first_matching_example_defined dentro de su
Bloque Rspec.configure
• requiere su biblioteca desde la parte superior de los archivos de especificaciones que la necesitan
Ha utilizado RSpec.configure varias veces mientras trabajaba en los ejemplos de este libro. Haremos una
revisión rápida de las técnicas que ha visto y, en el proceso, le mostraremos algunas opciones más.
5. http://culttt.com/2015/06/17/whatismonkeypatchinginruby/
informar fe de erratas • discutir
Machine Translated by Google
RSpec.configure • 157
Manos
Los ganchos le permiten declarar fragmentos de código que se ejecutan antes, después o alrededor de sus
especificaciones. Un enlace puede ejecutarse para cada :ejemplo, una vez para cada :contexto o
globalmente para todo el :suite.
Vimos los ganchos en detalle en Hooks, en la página 113. Como recordatorio, aquí hay un gancho
previo típico definido en un bloque RSpec.configure :
09configuraciónrspec/03/
rspec_configure.rb RSpec.configure do |config|
config.before(:ejemplo) hacer
#...
fin
fin
Nos gustaría revisar otro gancho de configuración de propósito especial que no se ajusta al patrón típico
de antes/después/alrededor . En Aislamiento de sus especificaciones mediante transacciones de base
de datos, en la página 92, vio una forma de ejecutar código de configuración de base de datos costoso
bajo demanda, la primera vez que se necesita:
09configuraciónrspec/03/
rspec_configure.rb RSpec.configure
do |config| config.when_first_matching_example_defined (: db)
requiere 'support/db' end
end
Este enlace utiliza metadatos (el símbolo :db ) para realizar una configuración adicional solo para las
especificaciones que la necesitan.
Si bien los ganchos de configuración son una excelente manera de reducir la duplicación y mantener
sus ejemplos enfocados, existen desventajas significativas si los usa en exceso:
• Un conjunto de pruebas lento debido a la lógica adicional que se ejecuta para cada
ejemplo • Especificaciones que son más difíciles de entender porque su lógica está oculta en ganchos
Para evitar estas trampas mientras mantiene sus especificaciones organizadas, puede usar una
técnica más simple y explícita: usar módulos de Ruby dentro de sus bloques de configuración .
Compartir código usando módulos
Los módulos son una de las principales herramientas de Ruby para compartir código. Puede agregar
todos los métodos de un módulo a una clase llamando a include o anteponer:
09configuringrspec/03/
rspec_configure.rb
clase Intérprete incluir Cantar # no anulará los métodos del
Intérprete anteponer Bailar # puede anular los métodos del
Intérprete fin
informar fe de erratas • discutir
Machine Translated by Google
Capítulo 9. Configuración de RSpec • 158
Incluso puede traer métodos a un objeto individual:
09configuringrspec/03/rspec_configure.rb
persona_promedio = Persona_promedio.nueva
persona_promedio.extender Canto
RSpec proporciona el mismo tipo de interfaz dentro de los bloques RSpec.configure . Al llamar a include,
anteponer o extender en el objeto de configuración , puede incorporar métodos adicionales a sus ejemplos
o grupos.
09configuraciónrspec/03/rspec_configure.rb
RSpec.configure do |config| # Trae
métodos a cada ejemplo config.include
ExtraExampleMethods
# Trae métodos a cada ejemplo, # anula métodos
con el mismo nombre # (rara vez se usa) config.prepend
ImportantExampleMethods
# Trae métodos a cada grupo (junto con let/describe/etc.)
# Útil para agregar al idioma específico del dominio de RSpec config.extend
ExtraGroupMethods end
Debido a que funcionan como sus contrapartes de Ruby que ya usa, estos tres métodos de configuración
son excelentes para compartir métodos de Ruby en sus especificaciones.
Sin embargo, si necesita compartir más, como ganchos o definiciones let , deberá definir un grupo de
ejemplo compartido. Luego puede traer este grupo compartido automáticamente dentro de su bloque de
configuración :
09configuraciónrspec/03/rspec_configure.rb
RSpec.configure do |config|
config.include_context 'Mi grupo compartido' end
Ahora que hemos cubierto el código compartido a través de un bloque de configuración , hablemos sobre
cómo controlar cómo se ejecuta RSpec.
Filtrado
Varias veces a lo largo de este libro, ha encontrado la necesidad de ejecutar solo algunos
de los ejemplos en su suite. En varios puntos, ha utilizado el filtrado de RSpec para ejecutar
los siguientes subconjuntos de especificaciones:
• Un solo ejemplo o grupo por nombre • Solo las
especificaciones que coinciden con una determinada pieza de metadatos, como :fast • Solo
los ejemplos en los que está enfocando su atención • Solo los ejemplos que
fallaron la última vez que se ejecutaron
informar fe de erratas • discutir
Machine Translated by Google
RSpec.configure • 159
Veamos cómo se relacionan estas técnicas con el sistema de configuración de RSpec.
Dentro de un bloque de configuración , puede usar los siguientes métodos para especificar qué
especificaciones ejecutar:
config.example_status_persistence_file_path = 'spec/ejemplos.txt'
Le dice a RSpec dónde almacenar el estado aprobado, fallido o pendiente de cada ejemplo entre
ejecuciones, habilitando las opciones onlyfailures y nextfailure .
config.filter_run_excluyendo:specific_to_some_os
Excluye la ejecución de ejemplos; útil para exclusiones permanentes basadas en factores
ambientales como el sistema operativo, la versión de Ruby o una variable de entorno.
config.filter_run_when_matching: algunos_metadatos
Establece un filtro condicional que solo se aplica cuando hay ejemplos coincidentes; así es como,
por ejemplo, RSpec ejecuta solo los ejemplos que ha etiquetado con los metadatos :focus .
metadatos
Como discutimos en Cortar y dividir especificaciones con metadatos, el sistema de metadatos de RSpec
le permite categorizar sus especificaciones de una manera que tenga sentido para usted. Los metadatos
están profundamente conectados con el sistema de configuración. Muchas de las opciones de
configuración de RSpec que hemos discutido aceptan (o requieren) un argumento de metadatos, que
determina los ejemplos o grupos a los que se aplica la opción de configuración.
También puede establecer metadatos utilizando el sistema de configuración. Los siguientes métodos le
permiten escribir metadatos en ejemplos o grupos:
config.define_derived_metadata(file_path: /unidad/) { |meta| meta[:tipo] = :unidad }
Deriva un valor de metadatos de otro. Aquí, etiquetamos todas las especificaciones en el directorio
de la unidad con el tipo: :unidad. Si hubiéramos omitido el argumento file_path , esta llamada
habría establecido metadatos para todos los ejemplos.
config.alias_example_to :alias_for_it, some_metadata: :value
Define una alternativa al método integrado que crea un ejemplo y adjunta metadatos. Así es como
el método de ajuste incorporado de RSpec marca los ejemplos en los que desea enfocarse.
config.alias_example_group_to :alias_for_describe, some_metadata: :value
Como el alias anterior, excepto que funciona en grupos de ejemplo en lugar de ejemplos individuales
(como fdescribe de RSpec).
informar fe de erratas • discutir
Machine Translated by Google
Capítulo 9. Configuración de RSpec • 160
Opciones de salida
RSpec tiene como objetivo proporcionar resultados procesables , es decir, resultados que lo
ayuden a decidir qué hacer a continuación. Con ese fin, admite una serie de opciones para que
la salida sea útil para situaciones específicas:
config.warnings = true
Habilita el modo de advertencias de Ruby, como el indicador rspec warnings que
discutimos anteriormente. Esto lo ayuda a detectar algunos errores (como redefiniciones
de métodos y errores ortográficos variables), pero puede reportar toneladas de advertencias
adicionales en código de terceros, a menos que use algo como ruby_warning_filter para
reducir parte del ruido.6
config.profile_examples =
2 RSpec medirá cuánto tiempo tomó cada especificación e imprimirá el número dado de
ejemplos y grupos más lentos (dos, en este caso). Esto es útil para mantener rápido su
conjunto de pruebas.
Cuando falla una expectativa, RSpec imprime el seguimiento inverso que muestra la cadena de
llamadas a métodos desde su especificación hasta el código de nivel más bajo. RSpec excluye
sus propios marcos de pila de esta lista. También puede excluir otros
bibliotecas o archivos del backtrace:
config.backtrace_exclusion_patterns << /proveedor/
Excluye cualquier línea del seguimiento inverso que coincida con las expresiones regulares
dadas; por ejemplo, líneas que contengan el texto proveedor.
config.filter_gems_from_backtrace :bastidor, :sinatra
Excluye marcos de pila de bibliotecas específicas; aquí, no veremos llamadas desde el
interior del estante y gemas sinatra.
Si alguna vez necesita más detalles, puede obtener el seguimiento inverso completo (incluidos
los marcos de pila de RSpec y cualquier otro que haya configurado para ignorar) pasando
backtrace en la línea de comando.
Casi toda la salida de RSpec se puede personalizar con un formateador. Como hemos discutido
anteriormente, puede especificar un formateador en la línea de comando usando la opción
format o f . También puede agregar un formateador en un bloque RSpec.configure :
09configuraciónrspec/03/
rspec_configure.rb RSpec.configure do |config|
# Puede usar los mismos nombres de formateadores compatibles con la CLI...
config.add_formatter 'documentation'
6. https://github.com/semaperepelitsa/ruby_warning_filter
informar fe de erratas • discutir
Machine Translated by Google
RSpec.configure • 161
# ...o pase _cualquier_ clase de formateador, incluida una personalizada:
config.add_formatter Fuubar end
Este ejemplo utiliza el formateador Fuubar, que es uno de los formateadores de terceros más populares
y útiles.7
Como sugiere el método add_formatter , puede agregar varios formateadores, dirigiendo cada uno a
una salida diferente:
09configuraciónrspec/03/rspec_configure.rb
RSpec.configure do |config|
config.add_formatter 'documentación', $stdout
config.add_formatter 'html', 'specs.html' fin
Si no llama a add_formatter o elige un formateador de una opción de línea de comandos, RSpec usará
de forma predeterminada el formateador de progreso. Sin embargo, puede proporcionar un valor
predeterminado diferente usando config.default_formatter:
09configuraciónrspec/03/rspec_configure.rb
RSpec.configure do |config|
config.default_formatter = config.files_to_run.one? ? 'doc' : fin del 'progreso'
Con este fragmento, RSpec usará de forma predeterminada la documentación más detallada si está
ejecutando solo un archivo de especificaciones, o el formateador de progreso si está ejecutando varios
archivos. De todos modos, puede anular este valor predeterminado pasando un formateador en la línea
de comando.
Configuración de la biblioteca
La forma más común de ejecutar RSpec es usar rspeccore para ejecutar sus especificaciones, rspec
expectations para expresar los resultados esperados y rspecmocks para proporcionar dobles de
prueba. Pero no tienes que usarlos juntos. Se envían como tres gemas de rubí separadas precisamente
para que puedas intercambiar cualquiera de ellas. Incluso puede usar rspecmocks o rspecexpectations
con otro marco de prueba, como analizamos en Uso de partes de RSpec con otros marcos de prueba,
en la página 296.
se burla
La opción config.mock_with establece qué marco de objeto simulado usará RSpec.
Si desea utilizar Mocha en lugar de simulacros de RSpec, puede hacerlo con el siguiente código:8
7. https://jeffkreeftmeijer.com/2010/fuubartheinstafailingrspecprogressbarformatter/
8. http://gofreerange.com/mocha/docs/
informar fe de erratas • discutir
Machine Translated by Google
Capítulo 9. Configuración de RSpec • 162
09configuringrspec/04/configuring_rspec/mocha_spec.rb
RSpec.configure do |config|
config.mock_with :mocha fin
RSpec.describe 'config.mock_with :mocha' do it 'te permite
usar mocha en lugar de rspecmocks' do
item = stub('Libro', costo: 17.50)
credit_card = mock('CreditCard')
credit_card.expects(:charge).with(17.50)
PointOfSale.purchase(item, with: credit_card) end
fin
RSpec también es compatible con las bibliotecas de simulación :rr y :flexmock.9,10
Puede pasar un bloque a mock_with para establecer opciones para la biblioteca de simulación. En el
siguiente fragmento, activamos una verificación adicional en rspecmocks:
09configuringrspec/04/configuring_rspec/rspec_mocks_configuration_spec.rb
RSpec.configure do |config|
config.mock_with :rspec do |simulacros|
mocks.verify_partial_doubles = verdadero
mocks.verify_doubled_constant_names = verdadero final
fin
Hablaremos más sobre la configuración de sus dobles de prueba en Uso efectivo de dobles parciales, en
la página 271. En caso de que tenga curiosidad, esto es lo que hacen estas dos opciones:
mocks.verify_partial_doubles = true
Verifica que cada doble parcial, un objeto normal que se ha modificado parcialmente con el
comportamiento de doble de prueba, se ajusta a la interfaz original del objeto.
mocks.verify_doubled_constant_names = verdadero
Al crear un doble de verificación usando una cadena como "SomeClassName", RSpec verificará que
SomeClassName realmente existe.
Los documentos de configuración completos para rspecmocks están disponibles en línea.11
Expectativas
Así como mock_with le permite configurar una alternativa a rspecmocks, expect_with le permite elegir un
marco de aserción diferente en lugar de rspecexpectations:
9. http://rr.github.io/rr/
10. https://github.com/doudou/flexmock
11. http://rspec.info/documentation/3.6/rspecmocks/RSpec/Mocks/Configuration .html
informar fe de erratas • discutir
Machine Translated by Google
RSpec.configure • 163
09configuringrspec/04/configuring_rspec/expect_with_spec.rb
requiere 'incorrecto'
RSpec.configure do |config|
config.expect_with : minitest,: rspec, extremo incorrecto
RSpec.describe 'Uso de diferentes bibliotecas de afirmación/expectativa' do
let(:resultado) { 2 + 2 }
' funciona con afirmaciones minitest' do
afirmar_equal 4, resultado
final
' funciona con las expectativas de rspec' espera
(resultado) .to eq 4 final
' funciona con mal' hacer
# "Donde 2 y 2 siempre son 5..." afirmar
{resultado == 5} end
fin
Aquí, estamos usando tres bibliotecas diferentes: rspecexpectations, afirmaciones de
Minitest y una pequeña biblioteca llamada Wrong.12,13 (Normalmente no necesitará usar
varias bibliotecas de afirmaciones; solo estamos demostrando algunas opciones diferentes).
Puede dar expect_con un nombre de biblioteca conocido : rspec, :minitest o :test_unit, o
puede pasar un módulo de Ruby que contenga los métodos de aserción que desea usar. Si
está escribiendo su propio módulo, sus métodos deberían señalar una falla al generar una
excepción.
Otras opciones útiles
Antes de concluir este capítulo, echemos un vistazo a algunas opciones finales que no
encajan en las categorías que hemos descrito.
Modo Zero MonkeyPatching
La primera opción que nos gustaría destacar aquí es disabled_monkey_patching!:
09configuraciónrspec/05/rspec_configure.rb
RSpec.configure do |config|
config.disable_monkey_patching! fin
12. http://docs.seattlerb.org/minitest/
13. https://github.com/sconover/wrong
informar fe de erratas • discutir
Machine Translated by Google
Capítulo 9. Configuración de RSpec • 164
Esta bandera deshabilita la sintaxis original de RSpec:
09configuringrspec/05/rspec_configure.rb
# Sintaxis antigua
describir SwissArmyKnife do # método simple ̀describe`
es 'util' hacer
cuchillo.debería ser_útil # ̀debería` terminar la expectativa
fin
…a favor del estilo en RSpec 3:
09configuringrspec/05/rspec_configure.rb
# Nueva sintaxis
RSpec.describe SwissArmyKnife do # ̀describe` invocado en el módulo ̀RSpec` es 'útil' do
expect(knife).to
be_useful # ̀expect()`style expectation end
fin
La sintaxis anterior dependía en gran medida de los objetos básicos de Ruby con parches de
mono. El nuevo modo zeromonkeypatch no lo hace. El resultado es menos errores y casos
extremos en sus especificaciones. Además, su código seguirá funcionando con futuras versiones
de RSpec. Para obtener más información sobre este modo, consulte la publicación del blog de
RSpec que lo presentó.14
Orden aleatorio
Como discutimos en Probar el caso no válido, en la página 89, le recomendamos que configure
RSpec para ejecutar sus especificaciones en orden aleatorio:
09configuraciónrspec/05/
rspec_configure.rb RSpec.configure
do |config| config.order = :
final aleatorio
Ejecutar sus especificaciones en orden aleatorio ayuda a mostrar las dependencias de orden entre
sus ejemplos. Es más probable que descubra estos problemas cuando aparezcan por primera vez
y podrá corregir el error mientras el código aún está fresco en su mente.
Adición de su propia
configuración Todas las configuraciones que hemos visto hasta ahora vienen con RSpec. Pero no
estás limitado a esos. RSpec proporciona una API para agregar nuevas configuraciones, que luego
puede usar en sus propias bibliotecas que amplían el comportamiento de RSpec.
14. http://rspec.info/blog/2013/07/theplanforrspec3/#zeromonkeypatchingmode
informar fe de erratas • discutir
Machine Translated by Google
Tu turno • 165
Por ejemplo, suponga que está escribiendo un complemento RSpec y el servicio web que lo acompaña para ayudar a los
desarrolladores a realizar un seguimiento de las tendencias a largo plazo sobre sus conjuntos de pruebas. Después de
cada ejecución de especificación, su complemento informará los tiempos de ejecución y el estado de aprobación/rechazo al
servicio.
Sus usuarios necesitarán alguna forma de configurar su complemento para usar sus claves API
asignadas para el servicio web. En su biblioteca, llamaría a add_setting dentro de un bloque
RSpec.configure , pasándole el nombre de la configuración que está creando:
09configuraciónrspec/05/
rspec_configure.rb RSpec.configure do |config|
config.add_setting : fin de spec_history_api_key
Una vez que un desarrollador ha instalado su complemento, puede configurarlo para usar el
Clave API así:
09configuraciónrspec/05/
rspec_configure.rb RSpec.configure
do |config| config.spec_history_api_key = 'a762bc901fga4b185b'
fin
Incluso si solo está escribiendo una biblioteca para sus propios proyectos, agregar este tipo de
opciones de configuración puede facilitar su uso.
Tu turno
En este capítulo, analizamos dos formas de configurar RSpec: las opciones de la línea de
comandos y el método de configuración . Las opciones de la línea de comandos son fáciles de
descubrir y son excelentes para cambios únicos en el comportamiento de RSpec. El método de
configuración cubre más del comportamiento de RSpec y le brinda un control más detallado sobre
cómo se ejecuta RSpec.
No le estamos pidiendo a nadie que memorice todo el conjunto de opciones de configuración.
Pero los que le mostramos aquí lo ayudarán a convertir RSpec en un entorno de trabajo cómodo
y productivo.
Ejercicios
Ahora que ha visto la amplia gama de opciones de configuración que ofrece RSpec, intente usar
algunas de ellas en estos ejercicios.
Uso de un formateador personalizado
Busque y lea la documentación de los siguientes formateadores RSpec (algunos de los cuales
son más útiles que otros):
informar fe de erratas • discutir
Machine Translated by Google
Capítulo 9. Configuración de RSpec • 166
• Fivemat15 •
Fuubar16 •
NyanCat17 •
Cualquier otro que puedas encontrar
Instale un par de estas gemas en su sistema. Pruebe cada formateador en la línea de comando
con uno de sus proyectos; la aplicación de seguimiento de gastos que escribiste en la segunda
parte de este libro sería perfecta.
Una vez que haya encontrado un formateador que le guste, configure uno de sus proyectos para
usarlo en cada ejecución. Si realmente disfruta usar este formateador, es posible que desee
configurar RSpec para usarlo en todos sus proyectos.
Detección de especificaciones de
ejecución lenta Como hemos discutido antes, mantener sus especificaciones funcionando
rápidamente es clave para lograr un flujo productivo. En este ejercicio, escribirá una biblioteca que
medirá algunos de los tiempos de ejecución de sus especificaciones y fallará en cualquier ejemplo
que sea demasiado lento.
No desea aplicar los mismos requisitos de tiempo a todas sus especificaciones; después de todo,
las especificaciones de integración y aceptación suelen ser más lentas que las especificaciones
de unidades. Su biblioteca debe usar una pieza de metadatos configurables, como :fail_if_slower_than,
para establecer el umbral. Por ejemplo, cualquier ejemplo del siguiente grupo debería fallar si
tarda más de una centésima de segundo en ejecutarse:
09configuringrspec/exercises/fail_if_slower_than_spec.rb
RSpec.describe SomeFastUnitSpecs, fail_if_slower_than: 0.01 hacer
#...
fin
Los usuarios podrán configurar estos umbrales automáticamente para secciones completas de
sus suites, utilizando las técnicas de Metadatos derivados, en la página 135.
Cuando su biblioteca se carga por primera vez, debe definir un enlace de configuración alrededor .
El gancho comparará el tiempo del reloj de pared antes y después de que se ejecute cada ejemplo,
y luego fallará el ejemplo si toma demasiado tiempo.
15. https://github.com/tpope/fivemat
16. https://github.com/thekompanee/fuubar
17. https://github.com/mattsears/nyancatformatter
informar fe de erratas • discutir
Machine Translated by Google
Parte IV
Expectativas RSpec
Con rspecexpectations, puede expresar fácilmente los
resultados esperados sobre su código. Si bien la sintaxis
inicialmente puede parecer extraña o incluso "mágica",
debajo de las cubiertas utiliza objetos de comparación
simples que se pueden componer de formas útiles y poderosas.
En esta sección, profundizaremos en cómo funciona rspec
expectations, cómo componer comparadores y por qué es
útil hacerlo. Haremos un recorrido por los emparejadores
incluidos en RSpec y luego le mostraremos cómo crear sus
propios emparejadores específicos de dominio para sus proyectos.
Machine Translated by Google
En este capítulo, verá:
• Cómo especificar los resultados esperados de su código con las expectativas
de rspec • Qué
es un comparador
• Cómo hacer coincidir estructuras de datos complejas, centrándose solo en
los detalles importantes •
CAPÍTULO 10
Cómo combinar emparejadores con y/u operadores
Explorando las expectativas de RSpec
En RSpec Core, vimos cómo rspeccore lo ayuda a estructurar su código de prueba en
grupos de ejemplo y ejemplos. Pero tener una estructura sólida no es suficiente para escribir
buenas pruebas. Si nuestras especificaciones ejecutan el código sin mirar el resultado, en
realidad no estamos probando nada, excepto que el código no falla por completo.
Ahí es donde entra en juego rspecexpectations. Proporciona una API para especificar los
resultados esperados.
Cada ejemplo de RSpec debe contener una o más expectativas. Estos expresan lo que
espera que sea cierto en un punto específico de su código. Si sus expectativas no se
cumplen, RSpec reprobará el ejemplo con un claro mensaje de diagnóstico.
En este capítulo, veremos cómo una parte crucial de las expectativas, el emparejador, se
puede combinar de formas nuevas y útiles. Antes de profundizar en cómo funcionan las
expectativas, veamos algunas expectativas de muestra para ver de lo que son capaces:
10exploringrspecexpectations/01/expectation_examples.rb
ratio = 22 / 7.0
expect(ratio).to be_within(0.1).of(Math::PI)
numeros = [13, 3, 99]
esperar(numeros).a todos ser_impares
alfabeto = ('a'..'z').to_a
esperar(alfabeto).to start_with('a').and end_with('z')
Intenta leerlos en voz alta. "Se espera que la relación esté dentro de 0.1 de pi". "Espera que
todos los números sean impares". "Espera que el alfabeto comience con a y termine con z".
Estas expectativas se leen exactamente como lo que verifican.
El objetivo principal de rspecexpectations es la claridad, tanto en los ejemplos que escribe
como en la salida cuando algo sale mal. En este capítulo, le mostraremos cómo funcionan y
cómo usarlos en sus ejemplos de RSpec, ¡o incluso sin RSpec!
informar fe de erratas • discutir
Machine Translated by Google
Capítulo 10. Exploración de las expectativas de RSpec • 170
Partes de una expectativa
Cuando ves una expectativa como la siguiente:
10exploringrspecexpectations/02/parts_of_an_expectation.rb
expect(mazo.cartas.cuenta).to eq(52), 'no jugar con un mazo completo'
… notará varias formas de puntuación en uso: paréntesis, puntos y espacios en blanco. Si bien hay
cierta variedad aquí, la sintaxis usa de manera consistente solo unas pocas partes simples:
• Un sujeto, lo que está probando, es decir, una instancia de una clase de Ruby.
• Un comparador: un objeto que especifica lo que espera que sea cierto sobre el
sujeto, y proporciona la lógica de aprobación/rechazo
• (opcionalmente) un mensaje de error personalizado
Estas partes se mantienen unidas con un poco de código adhesivo: expect, junto con el método to o
not_to .
Veamos cómo funcionan estas partes probándolas en una sesión de IRB:
$ irb
Para usar rspecexpectations, debe solicitar la biblioteca y luego incluir el módulo RSpec::Matchers .
Normalmente, rspeccore hace esto por usted, pero cuando usa rspecexpectations en otro contexto,
debe traerlo usted mismo:
>> requiere 'rspec/expectations' =>
verdadero
>> incluye RSpec::Matchers
=> Objeto
Una vez hecho esto, ahora puede crear una expectativa:
>> esperar(1).to eq(1) =>
verdadero
>> esperar(1).a eq(2)
RSpec::Expectations::ExpectationNotMetError: esperado:
2 obtenido: 1
(comparado usando ==)
« retroceso truncado »
informar fe de erratas • discutir
Machine Translated by Google
Partes de una expectativa • 171
RSpec señala fallas al generar una excepción. Otros marcos de prueba usan una técnica similar
cuando falla una aserción.
Envolviendo su tema con esperar
Ruby comienza a evaluar su expectativa en el método de expectativa . Empezaremos por ahí
también. Escriba el siguiente código en su sesión de IRB:
>> esperar_uno = esperar(1)
=> #<RSpec::Expectations::ExpectationTarget:0x007fb4eb83a818 @target=1>
Aquí, nuestro sujeto es el número 1. Lo hemos envuelto en el método expect para darnos un lugar
para adjuntar métodos como to o not_to. En otras palabras, el método expect envuelve nuestro
objeto en un adaptador apto para pruebas.
¿Qué pasó con debería?
Si ha usado versiones anteriores de RSpec, probablemente esté familiarizado con la sintaxis anterior
de las expectativas:
'comida'.debe coincidir(/foo/)
Si bien esta notación era fácil de leer y funcionaba bien en la mayoría de los casos, tenía algunos
inconvenientes. Para implementarlo, RSpec tuvo que parchear todos los objetos del sistema con los
métodos should y should_not . Esto provocó errores confusos en ciertos casos extremos, como usar
BasicObject o delegar llamadas de método proxy a un objeto subyacente.
La nueva sintaxis expect es igual de legible y mucho más fácil de usar correctamente. Además, su
implementación no requiere parches mono en absoluto.a
a. http://rspec.info/blog/2012/06/rspecsnewexpectationsyntax/
Uso de un comparador
Si expect envuelve su objeto para probarlo, entonces el comparador realmente realiza la prueba.
El comparador comprueba que el sujeto cumple sus criterios. Los emparejadores pueden comparar
números, encontrar patrones en el texto, examinar estructuras de datos profundamente anidadas
o realizar cualquier comportamiento personalizado que necesite.
El módulo RSpec::Matchers se envía con métodos integrados para crear comparadores. Aquí,
usaremos su método eq para crear un comparador que solo coincida con el número 1:
>> ser_uno = eq(1)
=> #<RSpec::Matchers::BuiltIn::Eq:0x007fb4eb82dd98 @esperado=1>
Este comparador no puede hacer nada por sí mismo; todavía necesitamos combinarlo con el tema
que vimos en la sección anterior.
informar fe de erratas • discutir
Machine Translated by Google
Capítulo 10. Exploración de las expectativas de RSpec • 172
Juntando las piezas
En este punto, tenemos un tema, 1, que hemos envuelto dentro del método expect para
que sea comprobable. También tenemos un emparejador, be_one. Podemos juntarlos
usando el método to or not_to :
>> expect_one.to(be_one) =>
verdadero
>> esperar_uno.no_a(ser_uno)
RSpec::Expectations::ExpectationNotMetError: esperado:
valor != 1 obtenido: 1
(comparado usando ==)
« retroceso truncado »
El método to intenta hacer coincidir el sujeto (en este caso, el número entero 1) con el
comparador proporcionado. Si hay una coincidencia, el método devuelve verdadero; si no,
se recupera con un mensaje de error detallado.
El método not_to hace lo contrario: falla si el sujeto coincide . Si te gusta dividir los infinitivos
con audacia, también puedes usar to_not en lugar de not_to:
>> esperar (1). not_to eq (2)
=> cierto
>> esperar(1).to_not eq(2) =>
verdadero
Cuando piensa en las expectativas de RSpec como un par de objetos simples de Ruby
pegados, la sintaxis se vuelve clara. Usará paréntesis con la llamada al método expect , un
punto para adjuntar el método to o not_to y un espacio que conduce al comparador.
Cuando una expectativa falla
Cuando el código que está probando no se comporta como esperaba, es esencial tener
información de error buena y detallada para diagnosticar rápidamente lo que está
sucediendo. Como ha visto, los comparadores de RSpec brindan mensajes de falla útiles
desde el primer momento. A veces, sin embargo, necesitas un poco más de detalle.
Por ejemplo, considere la siguiente expectativa:
>> resp = Struct.new(:status, :body).new(400, 'parámetro de consulta desconocido ̀sort`') => #<struct
status=400, body="parámetro de consulta desconocido ̀sort`">
>> esperar(resp.estado).to eq(200)
RSpec::Expectations::ExpectationNotMetError: esperado:
200 obtenido: 400
informar fe de erratas • discutir
Machine Translated by Google
Partes de una expectativa • 173
(comparado usando ==)
« retroceso truncado »
“Esperado 200; got 400” es técnicamente correcto, pero no proporciona suficiente información para
comprender por qué recibimos una respuesta de 400 “Solicitud incorrecta”. El servidor HTTP nos
dice qué hicimos mal en el cuerpo de la respuesta, pero esa información no está incluida en el
mensaje de error. Para incluir esta información adicional, puede pasar un mensaje de error alternativo
junto con el comparador to to o not_to:
>> expect(resp.status).to eq(200), "Obtuve un #{resp.status}: #{resp.body}"
RSpec::Expectations::ExpectationNotMetError: Obtuve un 400: parámetro de consulta desconocido
̀sort`
« retroceso truncado »
Si el mensaje de falla es costoso de generar (por ejemplo, escanear un archivo de volcado de núcleo
grande), puede pasar un objeto invocable como un objeto Proc o Method . De esa manera, solo
paga el costo si la especificación falla:
>> expect(resp.estado).to eq(200), resp.método(:cuerpo)
RSpec::Expectations::ExpectationNotMetError: parámetro de consulta desconocido ̀sort`
« retroceso truncado »
Los mensajes de error claros le ahorran tiempo
No podemos decirle cuántas veces hemos visto "afirmación fallida, no se da ningún
mensaje" en conjuntos de pruebas heredados. Cuando encuentra un mensaje de
error vago como ese, lo mejor que puede hacer es agregar un montón de llamadas
puts con información de depuración y luego volver a ejecutar sus pruebas.
Un buen mensaje de error le dice exactamente qué salió mal para que pueda
comenzar a solucionarlo de inmediato. El tiempo ahorrado en el diagnóstico de fallas
puede traducirse directamente en costos más bajos del proyecto.
Cuando el mensaje de falla predeterminado del comparador no proporciona suficientes detalles, un
mensaje personalizado puede ser justo lo que necesita. Si se encuentra usando el mismo mensaje
repetidamente, puede ahorrar tiempo escribiendo su propio comparador. Le mostraremos cómo
hacerlo en Crear coincidencias personalizadas.
Expectativas de RSpec frente a afirmaciones tradicionales
Puede parecer que las expectativas de RSpec tienen muchas partes móviles. Si ha utilizado
aserciones tradicionales en un marco de prueba de xUnit, como el Minitest integrado de Ruby, es
posible que se pregunte si la complejidad adicional vale la pena.
De hecho, las expectativas y las afirmaciones son el mismo concepto básico, solo que con diferentes
énfasis.
informar fe de erratas • discutir
Machine Translated by Google
Capítulo 10. Exploración de las expectativas de RSpec • 174
Las afirmaciones son más simples de explicar que las expectativas de RSpec, y la simplicidad es algo bueno,
pero eso no necesariamente hace que una sea mejor que la otra.
La complejidad de RSpec proporciona una serie de ventajas sobre los métodos de aserción simples .
• Componibilidad: los Matchers son objetos de primera clase que se pueden combinar y usar de formas
flexibles que los métodos de aserción simples no pueden.
• Negación: los emparejadores se pueden negar automáticamente pasándolos a expect(object).not_to, sin
necesidad de escribir un método assert_not_xyz o refute_xyz para emparejar con assert_xyz.
• Legibilidad: hemos optado por utilizar una sintaxis que, cuando se lee en voz alta, suena como una
descripción en inglés del resultado que espera.
• Más errores útiles. Por ejemplo, la expectativa para la siguiente colección
de números:
10exploringrspecexpectations/02/good_failure_messages.rb
expect([13, 2, 3, 99]).to all be_odd
... le dice exactamente qué elemento de la colección falló:
se esperaba [13, 2, 3, 99] que todos fueran impares
el objeto en el índice 1 no coincidió: se
esperaba que ̀2.odd?` devolviera verdadero, obtuvo falso
La aserción equivalente al estilo xUnit, assert [13, 2, 3, 99].all?(&:odd), simplemente informa que se
esperaba que lo falso fuera verdadero.
Aunque nos encanta usar expectativas, seremos los primeros en decir que no son adecuadas para todos los
proyectos. Puede usar fácilmente RSpec con una biblioteca menos compleja, como las afirmaciones de
Minitest, como demostramos en Configuración de la biblioteca, en la página 161.
Cómo funcionan los emparejadores
Anteriormente, vimos que los emparejadores son objetos que tienen una lógica de aprobación/falla. Echemos
un vistazo más de cerca a cómo funcionan.
Un comparador es un poco como una expresión regular. Así como la expresión regular /^\[warn\] / define una
categoría de cadenas (aquellas que comienzan con [warn] seguidas de un espacio), un comparador define
una categoría de objetos.
El parecido no termina ahí. Como verá en un momento, los comparadores implementan uno de los mismos
protocolos que las expresiones regulares, lo que les permite usarse de manera similar.
informar fe de erratas • discutir
Machine Translated by Google
Cómo funcionan los emparejadores • 175
Cualquier objeto de Ruby se puede usar como comparador siempre que implemente un conjunto
mínimo de métodos. Este protocolo es tan simple que incluso puede crear un objeto comparador
en IRB. En los siguientes ejemplos de código, haremos precisamente eso.
De vuelta en su sesión de IRB, escriba el siguiente código (si está iniciando una nueva sesión,
no olvide volver a ejecutar el código de configuración):
>> comparador = Objeto.nuevo
=> #<Objeto:0x007fe2e213ea58>
Veamos cómo RSpec intenta usar este objeto cuando lo pasamos como comparador:
>> expect(1).to matcher
NoMethodError: método indefinido ̀¿coincidencias?' para #<Objeto:0x007feff9326f18>
« retroceso truncado »
Esta expectativa ha desencadenado una excepción NoMethodError . ¿RSpec espera que cada
comparador implemente una coincidencia? método, que toma un objeto y devuelve verdadero
si el objeto coincide (y falso en caso contrario). Definamos eso ahora, y volvamos a probar el
comparador:
>> def matcher.coincidencias?(real) >>
actual == 1 >> fin
=> :coincidencias?
>> expect(1).to matcher =>
verdadero
¡Éxito! ¿Qué pasa cuando el partido falla? Intentemos hacer coincidir contra 2 ahora:
>> expect(2).to matcher
NoMethodError: método no definido ̀failure_message' para
#<Object:0x007fe2e213ea58>
« retroceso truncado »
Cuando el objeto proporcionado no coincide, RSpec llama al método failure_mes sage del
comparador para obtener un mensaje apropiado para mostrar al usuario. Definamos eso ahora:
>> def matcher.failure_message >> 'objeto
esperado igual a 1' >> fin
=> :failure_message >>
expect(2).to matcher
RSpec::Expectations::ExpectationNotMetError: objeto esperado igual a 1
« retroceso truncado »
Con ese método en su lugar, RSpec ahora arroja un ExpectationNotMetError que contiene
nuestro mensaje.
informar fe de erratas • discutir
Machine Translated by Google
Capítulo 10. Exploración de las expectativas de RSpec • 176
Estos dos métodos: ¿coincidencias? y failure_message—son todo lo que necesita para definir
un comparador simple. El protocolo contiene varios métodos opcionales que puede usar para
personalizar aún más el comportamiento de su comparador; los discutiremos en Uso de
Matcher DSL, en la página 220.
Componer emparejadores
Incluso por sí solos, los comparadores son herramientas poderosas para sus pruebas. Pero
realmente brillan cuando los compones con otros comparadores para especificar exactamente
lo que esperas (y nada más). El resultado son pruebas más robustas y menos fallas falsas.
Hay algunas formas diferentes de componer emparejadores:
• Pasar un comparador directamente a otro •
Integrar emparejadores en estructuras de datos Array y Hash •
Combinar emparejadores con operadores lógicos y/o
Antes de considerar estos tres casos, veamos cómo los comparadores determinan si el sujeto
coincide o no.
Cómo los emparejadores emparejan objetos
Los emparejadores se basan en uno de los protocolos estándar de Ruby para proporcionar
compatibilidad: el humilde método === . Este método, a menudo llamado "tres iguales" o
"igualdad de casos", define una categoría a la que pueden (o no) pertenecer otros objetos. Las
clases, los rangos y las expresiones regulares integradas de Ruby definen este operador, y
también puede agregarlo a sus propios objetos.
Veamos cómo funciona la igualdad de casos en una sesión IRB:
>> /^\[advertencia\] / === '[advertencia] Poco espacio en disco'
=> cierto
>> /^\[aviso\] / === '[error] Sin memoria' => falso
>> (1..10) === 5 =>
verdadero
>> (1..10) === 15 =>
falso
Estamos jugando con === aquí solo para tener una idea de cómo Ruby (y RSpec) lo usan
internamente. La mayoría de las veces, no lo llamará directamente desde el código de producción.
En su lugar, Ruby lo llamará dentro de cada cláusula when de una expresión de caso .
Solo para recalcar el punto de que los comparadores son simples objetos de Ruby que
implementan ===, así es como se vería un comparador dentro de una expresión de caso :
informar fe de erratas • discutir
Machine Translated by Google
Combinadores de composición • 177
>> def describe_valor(valor) >>
caso valor
>> when be_within(0.1).of(Math::PI) then 'Pi' >> when
be_within(0.1).of(2 * Math::PI) then 'Double Pi' >> end >> end
=> :describir_valor >>
describir_valor(3.14159)
=> "Pi"
>> describir_valor(6.28319)
=> "Doble Pi"
Las expectativas de RSpec realizan la misma verificación interna que hace la declaración de
caso de Ruby : llaman === en el objeto que pasa. Ese objeto puede ser cualquier cosa,
incluido otro comparador.
Pasar un emparejador a otro
Puede que no sea obvio por qué necesita pasar un comparador a otro emparejador.
Supongamos que espera que una matriz en particular comience con un valor cercano a π. Con
RSpec, puede pasar el comparador be_within(0.1).of(Math::PI) al comparador start_with para
especificar este comportamiento:
>> numeros = [3.14159, 1.734, 4.273] =>
[3.14159, 1.734, 4.273] >>
esperar(numeros).to start_with( be_within(0.1).of(Math::PI) ) => true
¡Simplemente funciona! Ahora, veamos cómo se ve el mensaje cuando falla la expectativa:
>> esperar([]).to start_with( be_within(0.1).of(Math::PI) )
RSpec::Expectations::ExpectationNotMetError: esperado [] para comenzar con dentro de 0.1 de
3.141592653589793 « retroceso
truncado »
Desafortunadamente, el mensaje de falla es el gramaticalmente incómodo "se esperaba que []
para comenzar estuviera dentro de 0.1 de π". Afortunadamente, RSpec proporciona alias
(generalmente en forma de frase nominal) para los comparadores integrados que se leen
mucho mejor en situaciones como estas:
>> esperar([]).to start_with(a_value_within(0.1).of(Math::PI) )
RSpec::Expectations::ExpectationNotMetError: esperado [] para comenzar con un valor dentro de
0.1 de 3.141592653589793 « retroceso truncado
»
Mucho mejor. Esto en realidad se lee como inglés, y es mucho más inteligible: "se esperaba
que [] comenzara con un valor dentro de 0.1 de π".
informar fe de erratas • discutir
Machine Translated by Google
Capítulo 10. Exploración de las expectativas de RSpec • 178
El emparejador a_value_within es un alias para be_within que actúa de manera idéntica,
excepto por cómo se describe a sí mismo. RSpec proporciona una cantidad de alias como
este para cada uno de los comparadores incorporados.1 Como veremos en Definición de alias
de comparador, en la página 218, es trivial definir sus propios alias también.
Incrustación de comparadores en estructuras de datos de matrices
y hash Además de pasar un comparador a otro, también puede incrustar comparadores
dentro de una matriz en cualquier posición, o dentro de un hash en lugar de un valor.
RSpec comparará los artículos correspondientes usando ===. Esta técnica funciona en
cualquier nivel de anidamiento. Veamos un ejemplo:
10exploringrspecexpectations/04/composing_matchers.rb
presidentes =
[ { nombre: 'George Washington', año_nacimiento: 1732 }, { nombre:
'John Adams', año_nacimiento: 1735 }, { nombre: 'Thomas Jefferson',
año_nacimiento: 1743 }, # ...
] esperar(presidentes).empezar_con(
{ nombre: 'George Washington', año_nacimiento: un_valor_entre(1730, 1740) }, { nombre: 'John
Adams', año_nacimiento: un_valor_entre(1730, 1740) }
)
Aquí, estamos usando un_valor_entre(1730, 1740) para los años de nacimiento de George
Washington y John Adams en lugar de un número específico. Por supuesto, sabemos que sus
años de nacimiento fueron exactamente 1732 y 1735, respectivamente. Pero no todos los
valores del mundo real van a ser tan precisos. Si prueba un comportamiento más específico
de lo que realmente necesita, o si está probando una lógica no determinista, sus
especificaciones pueden fallar si la implementación cambia ligeramente.
Esta capacidad de componer comparadores, pasándolos entre sí o incrustándolos en
estructuras de datos, le permite ser tan preciso o vago como necesite ser. Cuando especifica
exactamente el comportamiento que espera (y nada más), sus pruebas se vuelven menos
frágiles.
Combinación de emparejadores con operadores lógicos y/o
Hay otra forma de combinar comparadores: expresiones compuestas de comparadores.
Cada comparador incorporado tiene dos métodos (y y o) que le permiten combinar lógicamente
dos emparejadores en un emparejador compuesto:
10exploringrspecexpectations/04/composing_matchers.rb
alphabet = ('a'..'z').to_a
expect(alphabet).to start_with('a').and end_with('z')
1. http://rspec.info/documentation/3.6/rspecexpectations/RSpec/Matchers.html
informar fe de erratas • discutir
Machine Translated by Google
Combinadores de composición • 179
stoplight_color = %w[ verde rojo amarillo ].sample
expect(stoplight_color).to eq('verde').o eq('rojo').or eq('amarillo')
Como vemos en el ejemplo stoplight_color , puede encadenar emparejadores en cadenas
arbitrariamente largas. Puede usar las palabras y/o, o puede usar & y | operadores:
10exploringrspecexpectations/04/composing_matchers.rb
alphabet = ('a'..'z').to_a
expect(alphabet).to start_with('a') & end_with('z')
stoplight_color = %w[ verde rojo amarillo ].sample
expect(stoplight_color).to eq('green') | eq('rojo') | eq('amarillo')
Esta sintaxis puede parecer realmente elegante y compleja, pero internamente es bastante
simple. Estos métodos devuelven un nuevo comparador que envuelve los dos operandos.
El comparador and solo tiene éxito si ambos operandos coinciden. El comparador or tiene éxito
si cualquiera de los operandos coincide.
Husmear en IRB le dará una idea de cómo funcionan:
>> empezar_con_a_y_terminar_con_z = comenzar_con('a').y terminar_con('z')
=> #<RSpec::Matchers::BuiltIn::Compuesto::And:0x007f94dc83ba30
@matcher_1=#<RSpec::Matchers::BuiltIn::StartWith:0x007f94dc82bd38
@actual_no_tiene_elementos_ordenados=falso, @esperado="a">,
@matcher_2=#<RSpec::Matchers::BuiltIn::EndWith:0x007f94dc82bc20
@actual_does_not_have_ordered_elements=false, @expected="z">>
Aquí hemos creado un comparador compuesto, una instancia de
RSpec::Matchers::BuiltIn ::Compound::And, que mantiene las referencias internas a los
emparejadores originales start_with y end_with .
El nuevo combinador compuesto funciona como cualquier otro. Incluso proporciona su propio
mensaje de error, basado en los mensajes de los comparadores subyacentes:
>> expect(['a', 'z']).to start_with_a_and_end_with_z => true
>> expect(['a', 'y']).to start_with_a_and_end_with_z
RSpec::Expectations::ExpectationNotMetError: esperado ["a", "y"] para terminar con "z" «
retroceso
truncado »
>> expect(['b', 'y']).to start_with_a_and_end_with_z
RSpec::Expectations::ExpectationNotMetError: start with esperado ["b", "y"] para
"a"
...y:
se esperaba que ["b", "y"] terminara con "z"
« retroceso truncado »
Los emparejadores compuestos son lo suficientemente inteligentes como para mostrar mensajes de falla solo
para los bits que fallaron.
informar fe de erratas • discutir
Machine Translated by Google
Capítulo 10. Exploración de las expectativas de RSpec • 180
Como todos los emparejadores, los emparejadores compuestos se pueden pasar como argumentos a otros
emparejadores:
10exploringrspecexpectations/04/composing_matchers.rb
letter_ranges = ['N to Z', 'A to M']
expect(letter_ranges).to contains_exactly(
una_cadena_comenzando_con('A') y terminando_con('M'),
una_cadena_comenzando_con('N') y terminando_con('Z')
)
Puede mezclar y combinar estas técnicas para componer emparejadores, todo en una sola expectativa.
También puede anidarlos tan profundamente como lo necesite. Aquí, combinamos comparadores con
operadores lógicos y luego anidamos esas combinaciones en un comparador de colecciones. Esta flexibilidad
le permite describir datos complejos con precisión.
Ahora que ha visto cómo combinar comparadores, centremos nuestra atención en la salida.
Descripciones de ejemplos generados
Los emparejadores tienen otra habilidad útil sobre los métodos de afirmación más simples : se describen a
sí mismos. El protocolo de comparación incluye el método de descripción opcional (pero recomendado) .
Todos los emparejadores integrados definen este método:
>> start_with(1).description =>
"empezar con 1"
>> (start_with(1) & end_with(9)).description => "comienza
con 1 y termina con 9"
>> contiene_exactamente (una_cadena_comienza_con(1) y termina_con(9)).descripción =>
"contiene exactamente (una cadena que comienza con 1 y termina con 9)"
Como puede ver, las descripciones de los emparejadores compuestos y compuestos incluyen la descripción
de cada parte. Estas descripciones se utilizan en los mensajes de error cuando pasa un comparador a otro.
También pueden ayudarlo a reducir la duplicación en sus especificaciones. Por lo general, cada ejemplo tiene
una cadena que indica el comportamiento previsto:
10exploringrspecexpectations/06/spec/cookie_recipe_spec.rb
clase CookieRecipe
attr_reader : ingredientes
def inicializar
@ingredients = [:mantequilla, :leche, :harina, :azúcar, :huevos, :chocolate_chips] end
fin
RSpec.describe CookieRecipe, '#ingredients' do
debe incluir :mantequilla, :leche y :huevos '
expect(CookieRecipe.new.ingredients).to include(:mantequilla, :leche, :huevos) end
informar fe de erratas • discutir
Machine Translated by Google
Descripciones de ejemplos generados • 181
' no debería incluir: aceite_de_pescado' espera
(CookieRecipe.nuevos.ingredientes).no_incluir (:aceite_de_pescado) end
fin
Estos aparecen en la salida cuando ejecuta sus especificaciones con el formateador de documentación:
$ rspec spec/cookie_recipe_spec.rb documentación de formato
CookieRecipe#los ingredientes
deben incluir: mantequilla,: leche y: huevos no
deben incluir: aceite de pescado
Terminado en 0.00197 segundos (los archivos tardaron 0.08433 segundos en
cargarse) 2 ejemplos, 0 fallas
Sin embargo, observe la duplicación en las especificaciones. Hemos explicado cada ejemplo dos veces:
una en la descripción y otra en el código. Si eliminamos el primero, RSpec escribirá su propia descripción
basada en el código:
10exploringrspecexpectations/06/spec/cookie_recipe_no_doc_strings_spec.rb
RSpec.describe CookieRecipe, '#ingredients' sí especifica
sí
espera(CookieRecipe.nuevos.ingredientes).para incluir(:mantequilla, :leche, :huevos) fin
especifique
do expect(CookieRecipe.new.ingredients).not_to include(:fish_oil) end
fin
También hemos cambiado al alias de especificación para evitar la frase gramaticalmente incómoda que
se espera. Cuando ejecutamos esta versión de las especificaciones:
$ rspec spec/cookie_recipe_no_doc_strings_spec.rb documentación de formato
CookieRecipe#los ingredientes
deben incluir: mantequilla,: leche y: los huevos no
deben incluir: aceite de pescado
Terminado en 0.00212 segundos (los archivos tardaron 0.08655 segundos en
cargarse) 2 ejemplos, 0 fallas
...el resultado es exactamente el mismo que cuando escribimos las descripciones a mano.
De esta manera, sin embargo, las especificaciones están un poco más preparadas para el futuro. Si
queremos cambiar un ejemplo más adelante, para que la receta no contenga lácteos, por ejemplo, no
tenemos que preocuparnos por mantener nuestra descripción en inglés sincronizada con el código.
Para generar estas descripciones, RSpec toma la descripción de la última expectativa ejecutada y le
antepone los prefijos " debería " o "no debería".
informar fe de erratas • discutir
Machine Translated by Google
Capítulo 10. Exploración de las expectativas de RSpec • 182
Podemos simplificar aún más nuestras especificaciones usando el método subject de rspec core
en lugar de repetir el código de creación de recetas. Esta construcción es el equivalente de llamar
a let(:subject) { ... }:
10exploringrspecexpectations/06/spec/cookie_recipe_subject_spec.rb
RSpec.describe CookieRecipe, '#ingredients' do subject
{ CookieRecipe.new.ingredients } it { is_expected.to
include(:butter, :milk, :eggs) } { is_expected.not_to include (:fish_oil) }
end
El método subject define cómo construir el objeto que estamos probando. RSpec nos da
is_expected como forma abreviada de esperar (sujeto). La frase it is_expected se lee muy bien
en cada especificación aquí, aunque en proyectos más grandes tendemos a favorecer
construcciones let más explícitas en aras de la claridad.
Una vez más, obtenemos el mismo resultado de documentación:
$ rspec spec/cookie_recipe_subject_spec.rb documentación de formato
CookieRecipe#los ingredientes
deben incluir: mantequilla,: leche y: los huevos no
deben incluir: aceite de pescado
Terminado en 0.00225 segundos (los archivos tardaron 0.08616 segundos en
cargarse) 2 ejemplos, 0 fallas
Aquí hay una forma alternativa de esta sintaxis de una sola línea que se parece aún más a las
descripciones generadas:
10exploringrspecexpectations/06/spec/cookie_recipe_should_spec.rb
RSpec.describe CookieRecipe, '#ingredients' do subject
{ CookieRecipe.new.ingredients } it { should
include(:butter, :milk, :eggs) } it { should_not
include(:fish_oil) } end
Espera, ¿no acabamos de decir que debería ser problemático en What Happened to should?, en
la página 171? El problema con el viejo debería de RSpec 2 y antes no es el nombre; es el hecho
de que RSpec tuvo que parchear la clase principal de Object para implementarlo.
Esto debería ser diferente: es solo un alias local para expect(subject).to. Puede usar should o
is_expected.to, lo que prefiera.
Le recomendamos que utilice esta sintaxis de una sola línea con moderación. Es posible enfatizar
demasiado la brevedad y confiar demasiado en frases ingeniosas. Para mantener nuestro
entusiasmo bajo control, repasemos algunas de las desventajas de las descripciones generadas:
informar fe de erratas • discutir
Machine Translated by Google
Tu turno • 183
• Dado que no están disponibles hasta el tiempo de ejecución, no puede usarlos con la opción example para
ejecutar una sola especificación.
• La salida de sus especificaciones puede ser engañosa si cambia su código de configuración y
olvide actualizar la documentación de descripción o contexto .
• Las especificaciones escritas en un estilo de una sola línea conllevan una carga cognitiva adicional; tienes que
comprender cómo se relacionan el tema y se_espera/debe relacionarse.
El único caso en el que recomendamos la sintaxis de una sola línea es cuando la descripción generada es un
duplicado casi exacto de lo que habría escrito a mano.
Para ver un buen ejemplo, consulte las especificaciones de la biblioteca de coincidencia de cadenas Mustermann.2
Tu turno
En este capítulo, ha aprendido las partes principales de una expectativa: el método expect , el sujeto, to/not_to y el
comparador. Escribió algunas expectativas simples utilizando expect y algunos de los comparadores integrados de
RSpec. Viste cómo combinar emparejadores pasándolos entre sí y combinándolos con y/o.
En el próximo capítulo, lo llevaremos a un recorrido por los emparejadores que se envían con RSpec. Pero primero,
pruebe su nuevo conocimiento de las expectativas con un rápido
ejercicio.
Ejercicio
Hemos preparado un archivo de especificaciones sin terminar que le dará la oportunidad de probar los conceptos
explicados en este capítulo. Lo alentamos a que lo descargue, lo ejecute y lo edite hasta que haya obtenido todas
las especificaciones para aprobar de manera consistente:
10exploringrspecexpectations/exercises/data_generator_spec.rb
requiere 'fecha'
class DataGenerator def
valor_booleano
[verdadero, falso].muestra
final
def email_address_value
dominio = %w[ gmail.com yahoo.com aol.com hotmail.com ].sample
nombre_de_caracteres = (0..9).to_a + ('a'..'z').to_a + ('A' .. 'Z')
Fin "#{nombre de usuario}
@#{dominio}"
2. https://github.com/sinatra/mustermann/blob/v0.4.0/mustermann/spec/sinatra_spec.rb
informar fe de erratas • discutir
Machine Translated by Google
Capítulo 10. Exploración de las expectativas de RSpec • 184
def date_value
Date.new( (1950..1999).to_a.sample,
(1..12).to_a.sample,
(1..28).to_a.sample,
) fin
def registro_usuario {
email_address: email_address_value, date_of_birth:
date_value, active: boolean_value
} fin
usuarios def (recuento)
Array.new(count) { user_record } final
fin
RSpec.configure do |c| c.fail_fast
= verdadero c.formateador
= 'documentación' c.color
= cierto
c.pedir = :definido
fin
RSpec.describe DataGenerator hacer
def be_a_booleano
# Ruby no tiene una clase booleana, así que esto no funciona.
# ¿Hay alguna forma en que podamos usar ̀o` para combinar dos emparejadores en su
lugar? be_a
(booleano) fin
" genera valores booleanos" do value =
DataGenerator.new.boolean_value expect(value).to
be_a_boolean end
def be_a_date_before_2000 # Combine
el comparador ̀be_a(klass)` con el comparador ̀be < value` # para crear un comparador que coincida
con fechas anteriores al 1 de enero de 2000. fill_me_in end
" genera fechas anteriores al 1 de enero de 2000" do
value = DataGenerator.new.date_value expect(value).to
be_a_date_before_2000 end
informar fe de erratas • discutir
Machine Translated by Google
Tu turno • 185
def be_an_email_address
# Pase una expresión regular simple a ̀coincidir` para definir un comparador de direcciones de correo electrónico.
# No se preocupe por la validación completa del correo electrónico; algo muy simple está bien. final de
coincidencia (/ alguna
expresión regular /)
" genera direcciones de correo electrónico"
do value = DataGenerator.new.email_address_value
expect(value).to be_an_email_address end
def emparejar_la_forma_de_un_registro_de_usuario
# Use ̀be_a_boolean`, ̀be_a_date_before_2000` y ̀be_an_email_address` # en el hash
pasado a ̀match` a continuación para definir este comparador.
match(fill_this_in: "con un hash que describe la forma de los datos") end
" genera registros de usuario" do user
= DataGenerator.new.user_record
expect(user).to match_the_shape_of_a_user_record end
def all_match_the_shape_of_a_user_record #
Combina el comparador ̀all` y ̀match_the_shape_of_a_user_record` aquí. lléname_en fin
" genera una lista de registros de usuarios" do
users = DataGenerator.new.users(4)
expect(users).to all_match_the_shape_of_a_user_record end
fin
informar fe de erratas • discutir
Machine Translated by Google
En este capítulo, verá:
• Un recorrido por los emparejadores incluidos
• La diferencia entre primitivo y de orden superior
emparejadores
• La diferencia entre el valor y las expectativas de bloque
CAPÍTULO 11
Comparadores incluidos en las expectativas de RSpec
En el capítulo anterior, aprendió a escribir expectativas para verificar el comportamiento de su código. Tienes
que conocer las diversas partes de una expectativa, como el tema y el comparador.
Ahora es el momento de echar un vistazo más de cerca a los emparejadores. Los ha llamado en sus
especificaciones y los ha combinado con otros emparejadores. Incluso ha escrito uno simple desde cero,
aunque la mayoría de las veces no tendrá que hacerlo. RSpec viene con una tonelada de comparadores útiles
para ayudarlo a especificar exactamente cómo desea que se comporte su código.
En este capítulo, haremos un recorrido por los comparadores integrados de RSpec. No vamos a enumerar de
forma exhaustiva todos los emparejadores disponibles; para eso está el Apéndice 3, Hoja de trucos de los
emparejadores, en la página 307 . Pero destacaremos los aspectos más destacados para que pueda elegir el
mejor emparejador para cada situación.
Los emparejadores en rspecexpectations se dividen en tres amplias categorías:
• Coincidencias primitivas para tipos de datos básicos como cadenas, números, etc.
• Matchers de orden superior que pueden tomar otros matchers como entrada, entonces (entre
otros usos) aplicarlos a través de las colecciones
• Comparadores de bloques para comprobar las propiedades del código, incluidos los bloques, las
excepciones y los efectos secundarios.
Comenzaremos nuestro recorrido con los emparejadores primitivos más utilizados.
Emparejadores primitivos
La palabra primitivo en un lenguaje de programación se refiere a un tipo de datos básico que no se puede
dividir en partes más pequeñas. Los números booleanos, enteros y de punto flotante son todos primitivos.
informar fe de erratas • discutir
Machine Translated by Google
Capítulo 11. Comparadores incluidos en las expectativas de RSpec • 188
Los emparejadores primitivos de RSpec son similares. Tienen definiciones simples y precisas que no se
pueden desglosar más. No están destinados a aceptar otros emparejadores como entrada (pero puede
ir en la otra dirección, pasándolos a otros emparejadores). Por lo general, solo pasan la operación que
está realizando, por ejemplo, una verificación de igualdad, directamente al sujeto de la expectativa.
Igualdad e Identidad
Los comparadores más fundamentales de RSpec se preocupan por las variaciones de la pregunta,
"¿Son estas dos cosas iguales?" Según el contexto, "lo mismo" puede referirse a una de varias cosas:
• Identidad: por ejemplo, dos referencias a un objeto
• Igualdad de clave hash: dos objetos del mismo tipo y valor, como dos
copias de la cadena "hola"
• Igualdad de valores: dos objetos de tipos compatibles con el mismo significado,
como el número entero 42 y el número de punto flotante 42.0
La mayoría de las veces, los programadores de Ruby se preocupan por el último de estos: la igualdad
de valores, incorporada en el operador == de Ruby .
Igualdad de
valores En RSpec, utiliza el comparador eq para verificar la igualdad de valores.
11matchersincludedinrspecexpectations/01/primitive_matchers.rb
expect(Math.sqrt(9)).to eq(3)
# equivalente a:
Math.sqrt(9) == 3
La mayoría de las veces, este emparejador es el que desea. Sin embargo, a veces tienes una necesidad
más específica.
Identidad
Supongamos que está probando una clase de Permutaciones que genera todos los ordenamientos
posibles de un conjunto de palabras. Esta operación se vuelve costosa rápidamente, por lo que le
gustaría memorizar (caché) el resultado.
Inicialmente, puede intentar usar el comparador eq para asegurarse de que la segunda llamada alcance
el valor almacenado en caché:
11matchersincluidosenrspecexpectations/01/primitive_matchers.rb
perms = Permutaciones.nuevo
first_try = perms.of(long_word_list) second_try =
perms.of(long_word_list)
expect(segundo_intento).to eq(primer_intento)
informar fe de erratas • discutir
Machine Translated by Google
Emparejadores primitivos • 189
Esta prueba probablemente le dará falsas garantías. Si el caché subyacente se comporta mal o
nunca se implementó, el cálculo simplemente se ejecutará nuevamente y producirá una nueva
lista de palabras en el mismo orden. Debido a que ambas matrices tienen el mismo contenido, su
prueba pasará incorrectamente.
En su lugar, le gustaría saber si first_try y second_try se refieren o no al mismo objeto subyacente,
no solo a dos copias con contenido idéntico.
Para esta comparación más estricta, usaría el comparador igual de RSpec , ¿que pasa al igual de
Ruby? método detrás de escena:
11matchersincludedinrspecexpectations/01/primitive_matchers.rb
expect(second_try).to equal(first_try)
Las propias especificaciones internas de RSpec para el método RSpec.configuration utilizan esta
técnica para asegurarse de que el método siempre devuelva la misma RSpec::Core::Configuration
instancia:
11matchersincludedin rspecexpectations/01/
primitive_matchers.rb
RSpec.describe RSpec describe '.configuration'
' devuelve el mismo objeto cada vez' espera
(RSpec.configuration).to equal(RSpec.configuration) end
fin
fin
Si lo prefiere, también puede usar be(x) como un alias para equal(x), para enfatizar que este
comparador se trata de identidad en lugar de igualdad de valores:
11matchersincludedinrspecexpectations/01/primitive_matchers.rb
expect(RSpec.configuration).to be(RSpec.configuration)
La tercera noción de igualdad se encuentra entre estos dos en términos de rigor.
Igualdad de clave
hash Los programadores rara vez comprueban directamente la igualdad de clave hash. Como su
nombre lo indica, se usa para verificar que dos valores deben considerarse la misma clave hash .
Si encuentra un uso de este método en la naturaleza, es probable que se llame desde un objeto
diseñado para comportarse como un diccionario.
¿El comparador eql de RSpec , basado en el eql incorporado de Ruby ? método, comprueba la
igualdad de la clave hash. Generalmente, se comporta igual que el comparador eq (ya que eql?
generalmente se comporta igual que ==). Una diferencia notable es que eql? siempre considera
que los números enteros y los números de punto flotante son diferentes:
informar fe de erratas • discutir
Machine Translated by Google
Capítulo 11. Comparadores incluidos en las expectativas de RSpec • 190
11matchersincludedinrspecexpectations/01/primitive_matchers.rb
# 3 == 3.0:
esperar(3).a eq(3.0)
# ...pero 3.eql?(3.0) es falso:
expect(3).not_to eql(3.0)
Este comportamiento permite que 3 y 3.0 se utilicen como claves diferentes en el mismo hash.
En caso de duda, use eq
Todas estas formas diferentes de comparar objetos pueden parecer confusas.
Cuando no esté seguro de qué comparador es el correcto, pruebe primero con eq . En la
mayoría de las situaciones, la igualdad de valores es lo que necesita.
variaciones
Estos tres comparadores tienen alias que se leen mejor en expresiones compuestas de comparadores:
• un_objeto_eq_a alias eq •
un_objeto_igual_a alias igual •
un_objeto_eql_a alias eql
Por ejemplo, considere la siguiente expectativa que verifica una lista de clases de Ruby:
11matchersincludedinrspecexpectations/01/primitive_matchers.rb
expect([String, Regexp]).to include(String)
La intención era requerir que la clase Ruby String real estuviera presente. Pero esta especificación también
permitirá incorrectamente que pasen cadenas simples de Ruby:
11matchersincludedinrspecexpectations/01/primitive_matchers.rb
expect(['a string', Regexp]).to include(String)
Como vimos en el capítulo anterior, los comparadores de orden superior como include verifican sus
argumentos con el operador de tres iguales, ===. En este caso, RSpec termina comprobando String ===
'una cadena', que devuelve verdadero.
La solución es pasar el comparador an_object_eq_to a include, para que los criterios de aprobación/rechazo
sean más precisos:
11matchersincludedinrspecexpectations/01/primitive_matchers.rb
expect([String, Regexp]).to include(an_object_eq_to String)
Los tres comparadores de igualdad que hemos discutido funcionarán en cualquier objeto de Ruby.
informar fe de erratas • discutir
Machine Translated by Google
Emparejadores primitivos • 191
veracidad
Si bien Ruby tiene valores verdaderos y falsos literales , permite que cualquier objeto se use en un
condicional. Las reglas son muy simples: falso y nulo se tratan como falsos, y todo lo demás se trata como
verdadero (¡incluso el número cero!).
En un guiño al comediante Stephen Colbert, los rubyistas se refieren a estas categorías de valores como
verdadero y falso (o falso; no hay una ortografía estándar).1 RSpec
sigue este ejemplo con sus comparadores de veracidad:
11matchersincludedinrspecexpectations/01/primitive_matchers.rb
esperar(verdadero).ser_verdadero
esperar (0).ser_verdadero
esperar(falso).no_ser_verdadero esperar
(nil) .no_ser_verdadero
# ...y por otro lado:
esperar(falso) .ser_falso
esperar(nil) .ser_falso
esperar(verdadero).no_ser_falso
esperar (0).no_ser_falso
Si encuentra desagradable el lenguaje de "ser sincero" y "ser falso", tenga en cuenta que el nombre es
intencional. Es un sutil empujón que has elegido un emparejador más informal. Si desea especificar que un
valor es exactamente igual a verdadero o falso, simplemente use uno de los comparadores de igualdad
que describimos en la última sección:
11matchersincludedinrspecexpectations/01/primitive_matchers.rb
esperar(1.¿impar?).ser verdadero
esperar(2.¿impar?).to igualar falso
Al igual que los comparadores de igualdad que vimos anteriormente, los comparadores de veracidad tienen
alias diseñados para leer bien en expresiones compuestas de emparejadores:
• be_truthy tiene un alias como
a_truthy_value. • be_falsey tiene el alias de be_falsy, a_falsey_value y a_falsy_value.
Comparaciones de operadores
Hemos usado el método be con argumentos antes, como en expect(answer).to be(42).
Este método tiene otra forma, una sin argumentos. Con él, puede realizar comparaciones de mayor que y
menor que (o usar cualquier operador binario de Ruby):
1. http://www.cc.com/videoclips/63ite2/thecolbertreportthewordveracidad
informar fe de erratas • discutir
Machine Translated by Google
Capítulo 11. Comparadores incluidos en las expectativas de RSpec • 192
11matchersincludedinrspecexpectations/01/primitive_matchers.rb
esperar(1).ser == 1
esperar(1).ser < 2
esperar(1).ser <= 2
esperar(2) .to be > 1
expect(2).to be >= 1
expect(String).to be === 'una cadena' expect(/
foo/).to be =~ 'food'
En cada caso, RSpec usa su operador, como == o <, para comparar los valores reales y
esperados. El comparador en la primera línea, be == 1, es equivalente a eq(1). Usa el que
prefieras.
Al pasar uno de estos comparadores de operadores a un comparador diferente, probablemente
querrá usar el alias a_value. Por ejemplo:
11matchersincludedinrspecexpectations/01/primitive_matchers.rb
squares = 1.upto(4).map { |i| i * i }
esperar(cuadrados).para incluir(un_valor > 15)
Hasta ahora, hemos estado comparando valores precisos como números enteros y cadenas. A
continuación, veremos qué sucede cuando arrastramos números de punto flotante imprecisos a
la mezcla.
Comparaciones de delta y rango
Los números de punto flotante son una realidad desafortunada en el mundo de la computación
binaria con precisión limitada. Verificar la igualdad exacta de dos flotadores con frecuencia
causará fallas. Por ejemplo, si prueba esta expectativa aparentemente sencilla:
11coincidenciasincluidasenrspecexpectations/01/
primitive_matchers.rb expect(0.1 + 0.2).to eq(0.3)
…entonces obtienes un fallo:
esperado: 0.3
obtenido: 0.30000000000000004
(comparado usando ==)
Esta falla puede sorprenderlo, pero verá un comportamiento similar en cualquier lenguaje que
use flotantes IEEE754, como lo hace Ruby.2 Así como las matemáticas decimales no pueden
expresar la mayoría de los números reales usando una cantidad finita de dígitos (por ejemplo, 1 /
3 = 0.333…), la representación binaria interna de números de punto flotante de su computadora
también es imperfecta.
2. http://0.30000000000000004.com/
informar fe de erratas • discutir
Machine Translated by Google
Emparejadores primitivos • 193
Diferencia absoluta
En lugar de buscar la igualdad exacta con los flotadores, debe usar el comparador
be_within de RSpec :
11matchersincludedinrspecexpectations/01/
primitive_matchers.rb expect(0.1 + 0.2).to be_within(0.0001).of(0.3)
El valor que hemos pasado a be_within aquí es el delta, o la diferencia absoluta en
cualquier dirección. Esta expectativa particular pasa siempre que el valor esté entre 0.2999
y 0.3001 (lo cual es, por supuesto).
Diferencia relativa
Igualmente útil es el método percent_of , donde proporciona una diferencia relativa en su
lugar:
11matchersincludedinrspecexpectations/01/
primitive_matchers.rb
town_population = 1237 expect(town_population).to be_within(25).percent_of(1000)
Hemos usado números enteros en este ejemplo para mostrar que los comparadores
relacionados con rangos no son solo para números de coma flotante. Todos los
emparejadores de esta sección funcionan bien para ambos tipos de números. La
aproximación de enteros resulta útil cuando se prueban cosas que tienen valores
ligeramente diferentes cada vez, como el consumo de memoria.
Un único comparador be_within admite valores absolutos y relativos, según el método que
se encadene. Este estilo se denomina interfaz fluida y lo ayuda a escribir expectativas que
se leen naturalmente: "esperar que [real] esté dentro de [delta] de [esperado]".
3
Como verá a lo largo del resto de este capítulo, muchos de los otros emparejadores
integrados admiten este mismo tipo de interfaz fluida. También es fácil agregarlo a sus
emparejadores personalizados, como verá en Agregar una interfaz fluida, en la página 222.
Rangos
A veces, es mejor expresar los valores esperados en términos de un rango, en lugar de
un valor objetivo y delta. Para estas situaciones, puede usar el comparador be_ between :
11matchersincludedinrspecexpectations/01/
primitive_matchers.rb expect(town_population).to be_ between(750, 1250)
3. https://martinfowler.com/bliki/FluentInterface.html
informar fe de erratas • discutir
Machine Translated by Google
Capítulo 11. Comparadores incluidos en las expectativas de RSpec • 194
Al igual que el emparejador be_within , be_ between admite una interfaz fluida. Puede usar
be_between(x, y).inclusive o be_ between(x, y).exclusive para elegir explícitamente un rango
inclusivo o exclusivo. El valor predeterminado es inclusivo, como Comparable#entre?(x, y) de Ruby.
Finalmente, estos dos comparadores tienen alias que están diseñados para leer bien en
expresiones compuestas de comparadores: be_within tiene un alias de a_value_within y be_
between tiene un alias de a_value_ between.
Predicados dinámicos
Un predicado es un método que responde a una pregunta con una respuesta booleana. En Ruby,
los nombres de los métodos predicados normalmente omiten el verbo y terminan con un signo
de interrogación. Por ejemplo, la clase Array de Ruby proporciona un vacío? método en lugar de
is_empty.
Esta convención es tan común que RSpec incluye soporte especial para ella en forma de
comparadores de predicados dinámicos.
Cómo usarlos
Cuando usa un comparador no reconocido de la forma be_..., RSpec quita el be_, agrega un
signo de interrogación al final y llama a ese método en el tema.
Por ejemplo, para que RSpec llame a array.empty?, puede usar be_empty en su expectativa:
11matchersincludedinrspecexpectations/01/primitive_matchers.rb
expect([]).to be_empty
si esta vacio? devuelve un valor veraz (ver Veracidad, en la página 191), la expectativa pasa.
Alternativamente, puede usar un prefijo be_a_ o be_an_ para predicados que son sustantivos.
Por ejemplo, si tuviera un objeto de usuario con un administrador. predicado, cualquiera de estos
funcionaría:
11matchersincludedinrspecexpectations/01/primitive_matchers.rb
esperar(usuario).ser_administrador
esperar(usuario).ser_un_administrador
Este último se lee mucho mejor. Para los predicados que comienzan con has_, como
hash.has_key?(:age), puede usar un comparador dinámico de predicados que comience con
have_:
11matchersincludedinrspecexpectations/01/primitive_matchers.rb
hash = { name: 'Harry Potter', age: 17, house: 'Gryffindor' } expect(hash).to
have_key(:age)
informar fe de erratas • discutir
Machine Translated by Google
Emparejadores primitivos • 195
Este ejemplo demuestra otra característica de los comparadores de predicados dinámicos de
RSpec: admiten argumentos y un bloque. Si pasa argumentos (o un bloque) a un comparador de
predicado dinámico, RSpec los reenviará al método de predicado cuando lo llame en el sujeto de
expectativa.
compensaciones
Tan legibles y útiles como pueden ser los comparadores de predicados dinámicos, tienen algunas
compensaciones. Al igual que los comparadores de veracidad, los comparadores de predicados
dinámicos utilizan la semántica condicional booleana flexible de Ruby. La mayoría de las veces,
esto es lo que desea, pero necesitará usar una técnica diferente si desea probar resultados
verdaderos o falsos exactos .
Un problema mayor es la documentación. Debido a que los comparadores dinámicos se generan
sobre la marcha, no tienen documentación. Sus compañeros de equipo pueden ver be_an_admin
en sus pruebas y buscarlo en vano en los documentos de RSpec. Un comparador de igualdad
simple no tendría este problema:
11matchersincludedinrspecexpectations/01/primitive_matchers.rb
expect(user.admin?).to eq(true)
Sin embargo, la salida del comparador simple no es tan útil:
esperado: verdadero
obtenido: falso
(comparado usando ==)
El resultado de falla al usar be_an_admin es mucho mejor:
esperaba que ̀#<User name="Daphne">.admin?` devolviera verdadero, obtuvo falso
Los comparadores de predicados dinámicos son útiles en expresiones de comparador compuestas.
Por ejemplo, puede personalizar cómo coincide una colección en función de un predicado:
11matchersincludedinrspecexpectations/01/primitive_matchers.rb
expect(array_of_hashes).to include(have_key(:lol))
El lenguaje es un poco forzado aquí, ya que los comparadores dinámicos no tienen alias
incorporados. Si desea una versión agradable y legible de un sintagma nominal de un comparador
de predicados, tendrá que crearle un alias usted mismo. Afortunadamente, es trivial hacerlo,
como mostraremos en Definición de alias de Matcher, en la página 218.
Dadas estas ventajas y desventajas, no hay ningún caso seguro a favor o en contra de ellas.
Personalmente, nos gustan, pero háblalo con tu equipo antes de esparcir todo esto.
sobre sus especificaciones.
informar fe de erratas • discutir
Machine Translated by Google
Capítulo 11. Comparadores incluidos en las expectativas de RSpec • 196
Coincidencias dinámicas de predicados frente a
comparación con verdadero/falso Uno de los objetivos de TDD es ayudarlo a diseñar
sus API. Por esta razón, cuando escribimos especificaciones para un método de
predicado, nos gusta llamar al método directamente y comparar el valor devuelto
explícitamente con verdadero o falso. Si estamos probando un método llamado
¿ éxito?, diremos esperar(sujeto.éxito?).que sea verdadero en nuestros ejemplos.
Por otro lado, cuando un método de predicado no es lo que estamos probando
directamente, sino que simplemente está disponible en un objeto devuelto, nos
gusta usar un comparador de predicado dinámico. Por ejemplo, cuando probó el
método Ledger#record en el proyecto de seguimiento de gastos anteriormente en
este libro, le sugerimos que escribiera una expectativa como expect(result).to
be_success.
Satisfacción
A veces, tendrás una condición complicada que no se puede expresar con ninguno de los
emparejadores que hemos visto hasta ahora. Para estos casos, RSpec proporciona el comparador
de satisfacción . Para usarlo, envuelve su lógica de aprobación/falla en un bloque y entrega ese
bloque para satisfacer:
11matchersincludedinrspecexpectations/01/
primitive_matchers.rb expect(1).para satisfacer { |número| número.impar? }
RSpec pasa el sujeto de la expectativa, 1, al bloque, lo que admite cualquier lógica arbitraria que
pueda pensar.
Nos gusta pensar en la satisfacción como un adaptador: envuelve cualquier fragmento de código
Ruby y lo adapta al protocolo de comparación de RSpec. Esta capacidad es útil cuando se crea una
expresión de comparación compuesta:
11matchersincludedinrspecexpectations/01/
primitive_matchers.rb expect([1, 2, 3]).to include(an_object_satisfying(&:even?))
Aquí, estamos usando el alias de satisfago , an_object_satisfying con una expresión de comparación
compuesta. También estamos ahorrando un poco de palabrería al crear el bloque implícitamente,
4
usando el símbolo #to_proc de Ruby.
A pesar de lo flexible que es satisfacer , seguimos favoreciendo a los emparejadores especialmente diseñados. Estos
últimos proporcionan mensajes de error más específicos y útiles.
4. http://rubydoc.org/core2.4.1/Symbol.html#methodito_proc
informar fe de erratas • discutir
Machine Translated by Google
Emparejadores de orden superior • 197
Comparadores de orden superior
Todos los emparejadores vistos hasta ahora son primitivos. Ahora, vamos a ver los emparejadores de orden
superior, es decir, los emparejadores a los que puede pasar otros emparejadores.
Con esta técnica, puede crear comparadores compuestos que especifiquen exactamente el comportamiento
que desea.
Colecciones y cadenas Una
de las tareas principales de la programación, en cualquier lenguaje, es manejar
colecciones, y Ruby no es una excepción. RSpec se envía con seis comparadores
diferentes para tratar con estructuras de datos:
• incluir requiere que ciertos elementos estén presentes (en cualquier orden). •
start_with y end_with requieren que los elementos estén al principio o al final. • all comprueba
una propiedad común en todos los elementos. • Match compara una
estructura de datos con un patrón. • contiene_exactamente requiere
que ciertos elementos, y no otros, estén presentes (en cualquier
orden).
Todos estos comparadores también funcionan con cadenas, con algunas diferencias menores (¡después de
todo, las cadenas son solo colecciones de caracteres!). En las siguientes secciones, revisaremos los
emparejadores en detalle.
incluir
El comparador de inclusión es uno de los emparejadores más flexibles y útiles que ofrece RSpec.
También es una defensa clave contra la fragilidad. Al usar include en lugar de un comparador más estricto
como eq o match, puede especificar solo los elementos que le interesan.
La colección puede contener elementos no relacionados, y sus pruebas aún pasarán.
En su forma más simple, el comparador de inclusión funciona en cualquier objeto con una inclusión? método.
Las cadenas y las matrices admiten este método:
11matchersincludedinrspecexpectations/02/
higher_order_matchers.rb expect('a string').to
include('str') expect([1, 2, 3]).to include(3)
Para hashes, puede verificar la presencia de una clave específica o un par clavevalor (pasado como un
hash):
11matchersincludedinrspecexpectations/02/
higher_order_matchers.rb hash = { name: 'Harry Potter', age: 17, house:
'Gryffindor' } expect(hash).to
include(:name) expect( hachís).a incluir(edad: 17)
informar fe de erratas • discutir
Machine Translated by Google
Capítulo 11. Comparadores incluidos en las expectativas de RSpec • 198
El comparador de inclusión acepta un número variable de argumentos para que pueda especificar varias
subcadenas, elementos de matriz, claves hash o pares clavevalor:
11matchersincludedinrspecexpectations/02/higher_order_matchers.rb
expect('a string').to include('str', 'ing') expect([1, 2,
3]).to include( 3, 2) esperar(hash).para
incluir(:nombre, :edad) esperar(hash).para
incluir(nombre: 'Harry Potter', edad: 17)
Esto funciona bien, pero hay un problema relacionado con el número variable de elementos.
Considere este ejemplo:
11matchersincludedinrspecexpectations/02/higher_order_matchers.rb
esperados = [3, 2]
expect([1, 2, 3]).to include(expected)
Esta expectativa falla, aunque la matriz claramente incluye 3 y 2. Aquí está el mensaje de falla:
esperado [1, 2, 3] para incluir [3, 2]
El mensaje de error nos da una pista. Este comparador espera que la matriz incluya ([3, 2]), en lugar de
incluir (3, 2). Pasaría si la matriz real fuera algo así como [1, [3, 2]].
Para que RSpec busque los elementos individuales, debe extraerlos de la matriz. Puede hacerlo
anteponiendo el argumento esperado con el operador Ruby splat:5
11matchersincludedinrspecexpectations/02/higher_order_matchers.rb
expect([1, 2, 3]).to include(*expected)
El comparador de inclusión también está disponible como una_colección_que incluye, una_cadena_que
incluye y un_hash_que incluye, para cuando lo pasa a otros buscadores de coincidencias. Como
comparador de orden superior, include también puede recibir emparejadores como argumentos; consulte
Comparaciones de operadores, en la página 191 o Satisfacción, en la página 196 para ver ejemplos.
empezar_con y terminar_con
Estos dos comparadores son útiles cuando te preocupas por el contenido de una cadena o colección al
principio o al final, pero no te preocupas por el resto. Funcionan exactamente como sus nombres lo
indican:
11matchersincludedinrspecexpectations/02/higher_order_matchers.rb
expect('a string').to start_with('a str').and end_with('ng') expect([1, 2, 3] ).para
empezar_con(1).y terminar_con(3)
5. http://rubydoc.org/core2.4.1/doc/syntax/calling_methods_rdoc.html#labelArray+to+Arguments+Conversion
informar fe de erratas • discutir
Machine Translated by Google
Emparejadores de orden superior • 199
Como muestra el ejemplo de cadena, puede especificar tanto o tan poco de la cadena como
desee. Lo mismo se aplica a las matrices; puede verificar una secuencia de elementos al
principio o al final:
11matchersincludedinrspecexpectations/02/higher_order_matchers.rb
expect([1, 2, 3]).to start_with(1, 2) expect([1, 2, 3]).to
end_with(2 , 3)
La misma precaución sobre el uso del operador splat de Ruby con include se aplica también a
empieza_con y termina_con .
Siguiendo el patrón de alias que hemos visto en otros lugares, estos comparadores tienen dos
alias cada uno:
• una_cadena_que_comienza_con / una_cadena_que_termina_con
• una_colección_que_comienza_con / una_colección_que_termina_con
Podrías combinarlos, por ejemplo:
11matchersincludedinrspecexpectations/02/higher_order_matchers.rb
expect(['list', 'of', 'words']).to start_with(
a_string_ending_with('st') ).y
end_with( a_string_starting_with('wo')
)
El emparejador externo start_with verifica la palabra 'lista' usando el interno a_string_end
ing_with, y así sucesivamente.
todo
All Matcher es un tanto extraño: es el único Matcher incorporado que no es un verbo, y es el único
que siempre toma otro Matcher como argumento:
11matchersincludedinrspecexpectations/02/higher_order_matchers.rb
números = [2, 4, 6, 8]
expect(numbers).to all be_even
Esta expresión hace exactamente lo que dice: espera que todos los números de la matriz sean
pares. Aquí, 'be_even' es un predicado dinámico como los que vimos en Predicados dinámicos,
en la página 194. Se llama 'even?' en cada elemento de la matriz.
Un punto a tener en cuenta es que, como Enumerable#all?, este comparador pasa contra una
matriz vacía. Esto puede dar lugar a sorpresas. Considere el siguiente método incorrecto para
generar una lista de números:
informar fe de erratas • discutir
Machine Translated by Google
Capítulo 11. Comparadores incluidos en las expectativas de RSpec • 200
11matchersincludedinrspecexpectations/02/higher_order_matchers.rb
def self.evens_up_to(n = 0)
0.upto(n).select(&:odd?) end
expect(evens_up_to).to all be_even
Este método genera números impares en lugar de pares, pero nuestra expectativa no falló.
Olvidamos pasar un argumento a evens_up_to y devolvió una matriz vacía. Una solución es
usar un comparador compuesto para garantizar que la matriz no esté vacía:
11matchersincludedinrspecexpectations/02/higher_order_matchers.rb
RSpec::Matchers.define_negated_matcher :be_non_empty, :be_empty
esperar (evens_up_to).to be_non_empty.and all be_even
Estamos utilizando otra característica de RSpec, define_negated_matcher, para crear un
nuevo comparador be_non_empty que es lo opuesto a be_empty. Aprenderemos más sobre
define_negated_matcher en Negar Matchers, en la página 219.
Ahora, la expectativa marca correctamente el método roto como fallido:
esperaba que ̀[].empty?` devolviera falso, se volvió verdadero
El comparador de RSpec utiliza Enumerable#all de Ruby . bajo el capó. Es posible que se
pregunte si RSpec tiene o no coincidencias para los otros métodos Enumerables similares ,
como cualquiera o ninguno. No es así, porque esto conduciría a un código sin sentido como
expect(numbers).to none be_even. En su lugar, puede crear comparadores más fáciles de
6
leer usando to include o not_to include.
fósforo
Si llama a las API JSON o XML, a menudo termina con matrices y hashes profundamente
anidados. Match Matcher es una navaja suiza para este tipo de datos.
Como hizo con eq, proporciona una estructura de datos que se presenta como el resultado
que espera. Sin embargo, la coincidencia es más flexible. Puede sustituir un comparador
por cualquier elemento de matriz, o por cualquier valor hash, en cualquier nivel de anidamiento:
11matchersincludedinrspecexpectations/02/higher_order_matchers.rb
children =
[ { nombre: 'Coen', edad: 6 },
{ nombre: 'Daphne', edad: 4 },
{ nombre: 'Crosby' , edad: 2 }
]
6. Esa es la cuestión.
informar fe de erratas • discutir
Machine Translated by Google
Emparejadores de orden superior • 201
esperar(niños).para que coincida con
[ { nombre: 'Coen', edad: a_value > 5 }, { nombre:
'Daphne', edad: a_value_ between(3, 5) }, { name: 'Crosby', age:
a_value < 3 }
]
Cuando esté comparando con una cadena, haga coincidir los delegados con String#match, que acepta
una expresión regular o una cadena:
11matchersincludedinrspecexpectations/02/higher_order_matchers.rb
expect('a string').to match(/str/) expect('a
string').to match('str')
Naturalmente, este comparador tiene los alias an_object_matching y a_string_matching .
container_exactly
Hemos visto que la coincidencia verifica las estructuras de datos de forma más flexible que eq;
container_exactly es aún más flexible. La diferencia es que la coincidencia requiere un orden específico,
mientras que container_exactly ignora el orden. Por ejemplo, ambas expectativas pasan:
11matchersincludedinrspecexpectations/02/higher_order_matchers.rb
expect(child).to contains_exactly(
{ nombre: 'Daphne', edad: un_valor_entre(3, 5) }, { nombre:
'Crosby', edad: un_valor < 3 }, { nombre: 'Coen',
edad: un_valor > 5 }
)
esperar (niños). contener_exactamente (
{ nombre: 'Crosby', edad: un_valor < 3 }, { nombre:
'Coen', edad: un_valor > 5 }, { nombre: 'Daphne',
edad: un_valor_entre(3, 5) }
)
Al igual que include, container_exactly recibe múltiples elementos de matriz como argumentos separados.
También está disponible como una_colección_que_contiene_exactamente.
¿Qué comparador de colecciones debo usar?
Con media docena de comparadores de colección para elegir, es posible que se pregunte cuál es el
mejor para su situación. En general, le recomendamos que utilice el comparador más flexible que
todavía especifique el comportamiento que le interesa.
Evite la especificación excesiva: favorezca los emparejadores sueltos
El uso de un emparejador suelto hace que sus especificaciones sean menos frágiles;
evita que los detalles incidentales causen una falla inesperada.
El diagrama de flujo en la página 202 proporciona una referencia rápida para los diferentes usos de la
colección y los comparadores de cadenas.
informar fe de erratas • discutir
Machine Translated by Google
Capítulo 11. Comparadores incluidos en las expectativas de RSpec • 202
¿Es
Sí importante el orden No
de los elementos?
¿Solo le Preocúpate
Usar solo de un común
Utilice Sí importan los elementos Sí
todo
start_with / end_with iniciales o finales? ¿propiedad?
No No
Estricto
¿Te importan
Usar solo algunas
Sí Sí
¿ Usar
¿Se requiere coincidencia
ecualización? incluir entradas?
de elementos?
No No
Usar Usa
fósforo contiene_exactamente
Atributos de objeto
Algunos objetos de Ruby actúan como versiones más sofisticadas de hash. Struct,
OpenStruct y ActiveRecord pueden actuar como cubos para sus datos, que lee a través de atributos.
Si necesita comparar los atributos de un objeto con una plantilla, puede usar el comparador
have_attributes :
11matchersincludedinrspecexpectations/02/higher_order_matchers.rb
require 'uri' uri
= URI('http://github.com/rspec/rspec') expect(uri).to
have_attributes(host: ' github.com', ruta: '/rspec/rspec')
Este comparador es particularmente útil como argumento para otro comparador; el
formulario an_object_have_attributes es útil aquí:
11matchersincludedinrspecexpectations/02/higher_order_matchers.rb
expect([uri]).to include(an_object_have_attributes(host: 'github.com'))
Esta comparación es más indulgente que la igualdad exacta. Su objeto puede contener
atributos adicionales más allá de los que especifique y aun así satisfacer al comparador.
informar fe de erratas • discutir
Machine Translated by Google
Coincidencias de bloques • 203
Coincidencias de bloques
Con todas las expectativas que hemos visto hasta ahora, hemos pasado los objetos regulares de
Ruby a esperar:
11matchersincludedinrspecexpectations/03/block_matchers.rb
expect(3).to eq(3)
Esto está bien para verificar las propiedades de sus datos. Pero a veces necesita verificar las
propiedades de una pieza de código. Por ejemplo, tal vez se supone que cierta pieza de código genera
una excepción. En este caso, puede pasar un bloque a expect:
11matchersincludedinrspecexpectations/03/block_matchers.rb
expect { raise 'boom' }.to raise_error('boom')
RSpec ejecutará el bloque y observará los efectos secundarios específicos que especifique:
excepciones, variables mutantes, E/S, etc.
Levantar y lanzar
Es probable que esté familiarizado con generar excepciones de Ruby para saltar de su código en
ejecución e informar un error a la persona que llama. Ruby también tiene un concepto relacionado,
lanzar símbolos, para saltar a otras partes de su programa.
RSpec proporciona comparadores para ambas situaciones: los apropiadamente llamados raise_error
y throw_symbol.
aumento_error
Primero, echemos un vistazo a raise_error, también conocido como raise_exception. Este emparejador
es muy flexible y admite múltiples formas:
• raise_error sin argumentos coincide si se genera algún error.
• raise_error(SomeErrorClass) coincide si se genera SomeErrorClass o una subclase.
• raise_error('algún mensaje') coincide si se genera un error con un mensaje
exactamente igual a una cadena dada.
• raise_error(/alguna expresión regular/) coincide si se genera un error con un mensaje que coincide
con un patrón dado.
Puede combinar estos criterios si tanto la clase como el mensaje son importantes, ya sea pasando
dos argumentos o usando una interfaz fluida:
• raise_error(AlgunaClaseError, "algún mensaje") •
raise_error(AlgunaClaseError, /alguna expresión
regular/) • raise_error(AlgunaClaseError).with_message("algún mensaje")
• raise_error(AlgunaClaseError).with_message(/alguna expresión regular/)
informar fe de erratas • discutir
Machine Translated by Google
Capítulo 11. Comparadores incluidos en las expectativas de RSpec • 204
Con cualquiera de estos formularios, puede pasar otro comparador RSpec (como a_string_starting_with
para el nombre del mensaje) para controlar cómo funciona la coincidencia.
Por ejemplo, la siguiente expectativa garantiza que la excepción tenga establecido su atributo de
nombre :
11matchersincludedinrspecexpectations/03/block_matchers.rb
expect
{ 'hola'.mundo
}.to raise_error(un_objeto_que_tiene_atributos(nombre: :mundo))
Verificar las propiedades de las excepciones puede volverse realmente complicado. Si se encuentra
pasando una expresión de comparación compuesta anidada compleja a raise_error, verá un
mensaje de error realmente largo en la salida. Para evitar esta situación, puedes pasar un bloque a
raise_error y mover tu lógica allí:
11matchersincludedinrspecexpectations/03/block_matchers.rb
expect { 'hello'.world }.to raise_error(NoMethodError) do |ex| expect(ex.name).to
eq(:world) end
Hay un par de trampas con raise_error que pueden dar lugar a falsos positivos.
En primer lugar, raise_error (sin argumentos) coincidirá con cualquier error, y no puede distinguir la
diferencia entre las excepciones que quiso lanzar o no .
Por ejemplo, si cambia el nombre de un método pero olvida actualizar su especificación, Ruby
arrojará un NoMethodError. Un raise_error demasiado entusiasta se tragará esta excepción, y su
especificación pasará a pesar de que ya no está ejerciendo el método que desea probar.
Del mismo modo, puede usar raise_error (ArgumentError) para asegurarse de que uno de sus
métodos genere correctamente este error. Si luego realiza un cambio importante en la firma del
método, como agregar un argumento, pero se olvida de actualizar una persona que llama, Ruby
generará el mismo error. Su especificación pasará (porque todo lo que ve es el ArgumentError que
está esperando), pero el código seguirá estando roto.
De hecho, nos hemos encontrado con este tipo de falso positivo en el mismo RSpec.7
Nunca busque una sola excepción
Incluya siempre algún tipo de detalle, ya sea una clase de error personalizada
específica o un fragmento del mensaje, que sea exclusivo de la instrucción de
aumento específica que está probando.
Por otro lado, la forma negativa—espera {...}.not_to raise_error(...)—tiene el problema opuesto. Si
damos demasiados detalles en nuestras especificaciones, corremos el riesgo de ver un
7. https://github.com/rspec/rspecmocks/pull/550
informar fe de erratas • discutir
Machine Translated by Google
Coincidencias de bloques • 205
falso positivo. Considere esta expectativa, donde se supone que un método subyacente
age_of evita una excepción específica:
11matchersincludedinrspecexpectations/03/
block_matchers.rb expect { age__of(user) }.not_to raise_error(MissingDataError)
Este fragmento contiene un error tipográfico difícil de detectar: escribimos age__of con
dos puntos bajos. Cuando Ruby ejecuta esta línea, generará un NameError, que no es un
MissingDataError. La expectativa pasará, ¡aunque nuestro método ni siquiera se ejecute!
Debido a que esta es una trampa tan espinosa, RSpec 3 le advertirá en esta situación y le
sugerirá que elimine la clase de excepción y simplemente llame a not_to raise_error sin
argumentos. Este formulario no es susceptible al problema de los falsos positivos, ya que
detectará cualquier excepción.
De hecho, a menudo puede simplificar aún más sus especificaciones. Dado que RSpec
envuelve cada ejemplo con expect { example.run }.not_to raise_error, puede eliminar su
control explícito de not_to raise_error , a menos que desee conservarlo para mayor claridad.
Las excepciones
throw_symbol están diseñadas para situaciones excepcionales, como un error en la lógica
del programa. No son adecuados para el flujo de control diario, como saltar fuera de un
bucle profundamente anidado o llamar a un método. Para situaciones como estas, Ruby
proporciona la construcción throw . Puede probar su lógica de control con el comparador
throw_symbol de RSpec :
11matchersincludedinrspecexpectations/03/
block_matchers.rb expect { throw :found }.to throw_symbol(:found)
Ruby le permite incluir un objeto junto con el símbolo lanzado, y throw_symbol también se
puede usar para especificar ese objeto a través de un argumento adicional:
11matchersincludedinrspecexpectations/03/
block_matchers.rb expect { throw :found, 10 }.to throw_symbol(:found, a_value > 9)
Dado que throw_symbol es un comparador de orden superior, el argumento adicional
puede ser un valor exacto, un comparador RSpec o cualquier objeto que implemente ===.
Flexible
Los bloques son una de las características más distintivas de Ruby. Le permiten pasar
pequeños fragmentos de código utilizando una sintaxis fácil de leer. Cualquier método puede
informar fe de erratas • discutir
Machine Translated by Google
Capítulo 11. Comparadores incluidos en las expectativas de RSpec • 206
pasa el control a quien llama usando la palabra clave yield , y RSpec proporciona cuatro
comparadores diferentes para especificar este comportamiento.
rendimiento_control
El comparador de rendimiento más simple es yield_control:
11matchersincludedinrspecexpectations/03/
block_matchers.rb def
self.just_yield yield end
esperar { |block_checker| just_yield(&block_checker) }.to yield_control
Para que pase la expectativa, el método just_yield debe ceder el control a un bloque o a un
objeto que actúa como un bloque. RSpec nos proporciona un objeto de este tipo: un verificador
de bloques que verifica que realmente nos rendimos ante él. Todos los comparadores de
rendimiento utilizan esta técnica.
También puede especificar un número esperado de rendimientos encadenando una, dos, tres veces,
exactamente (n) veces, al menos (n) veces o al máximo (n) veces.
11matchersincludedinrspecexpectations/03/
block_matchers.rb expect { |block| 2.veces(&bloquear) }.to
yield_control.dos veces esperar { |bloquear| 2.times(&block) }.to
yield_control.at_most(4).times expect { |block| 4.times(&block) }.to yield_control.at_least(3).veces
El método times es solo una decoración, pero ayuda a mantener legibles las expectativas de
control de rendimiento .
rendimiento_con_argumentos
Cuando le importan los argumentos específicos que genera su método, puede verificarlos con
el comparador yield_with_args :
11matchersincludedinrspecexpectations/03/
block_matchers.rb def
self.just_yield_these(*args) yield(*args) end
esperar { |bloquear|
just_yield_these(10, 'comida', Matemáticas::PI, &bloque)
}.to yield_with_args(10, /foo/, a_value_within(0.1).of(3.14))
Como muestra este ejemplo, puede usar varios criterios diferentes para verificar un objeto
cedido, que incluyen:
• Un valor exacto como el número 10
• Un objeto que implementa ===, como la expresión regular /foo/ • Cualquier
comparador RSpec, como a_value_within()
informar fe de erratas • discutir
Machine Translated by Google
Coincidencias de bloques • 207
yield_with_no_args
Hasta ahora hemos visto yield_control, que no se preocupa por los argumentos, y
yield_with_args, que requiere que se produzcan ciertos argumentos. A veces, sin embargo,
le importa específicamente que su código no produzca argumentos. Para estos casos, RSpec
ofrece yield_with_no_args:
11matchersincludedinrspecexpectations/03/block_matchers.rb
expect { |block| just_yield_these(&block) }.to yield_with_no_args
yield_successive_args
Algunos métodos, en particular los del módulo Enumerable , pueden producir muchas veces.
Para verificar este comportamiento, debe combinar la capacidad de conteo de yield_control
con la verificación de parámetros de yield_with_args .
Eso es exactamente lo que hace yield_successive_args . Para usarlo, pasa uno o más
argumentos, cada uno de los cuales puede ser un objeto o una lista de objetos. El primer
objeto o lista va con la primera llamada a yield, y así sucesivamente:
11matchersincludedinrspecexpectations/03/block_matchers.rb
expect { |block|
['fútbol', 'taburete'].each_with_index(&block) }.to
yield_successive_args( [/foo/, 0],
[a_string_starting_with('bar'), 1]
La función incorporada de Ruby each_with_index producirá dos veces: primero con los dos
valores 'fútbol' y 0, luego con los dos valores 'taburete' y 1. Como hicimos con yield_with_args,
estamos verificando estos resultados usando una combinación de Valores de Ruby, objetos
de estilo de expresión regular y comparadores RSpec.
Mutación
En la naturaleza, es común que las acciones externas, como enviar un formulario web,
cambien algún estado dentro del sistema. El comparador de cambios le ayuda a especificar
este tipo de mutaciones. Aquí está el emparejador en su forma más básica:
11matchersincludedinrspecexpectations/03/block_matchers.rb
array = [1, 2, 3] expect
{ array << 4 }.to change { array.size }
El emparejador realiza las siguientes acciones a su vez:
1. Ejecute su bloque de cambios y almacene el resultado, array.size, como el valor anterior
2. Ejecute el código bajo prueba, matriz << 4
informar fe de erratas • discutir
Machine Translated by Google
Capítulo 11. Comparadores incluidos en las expectativas de RSpec • 208
3. Ejecute su bloque de cambios por segunda vez y almacene el resultado, array.size, como el
después del valor
4. Pase la expectativa si los valores antes y después son diferentes
Esta expectativa verifica si la expectativa cambió o no, sin importar cuánto. Para eso, tendremos
que recurrir a otra técnica.
Especificación de los detalles
del cambio Al igual que otros comparadores fluidos de RSpec, el comparador de cambios
ofrece una forma sencilla de dar detalles sobre el cambio. Específicamente, puede usar by,
by_at_least o by_at_most para especificar la cantidad del cambio:
11matchersincludedinrspecexpectations/03/block_matchers.rb
expect { array.concat([1, 2, 3]) }.to change { array.size }.by(3) expect { array.concat
([1, 2, 3]) }.to change { array.size }.by_at_least(2) esperar { array.concat([1, 2, 3]) }.to change
{ array.size }.by_at_most(4 )
Si le interesan los valores exactos de antes y después, puede encadenar desde y hacia su
comparador (ya sea individualmente o en conjunto):
11matchersincludedinrspecexpectations/03/block_matchers.rb
esperar { array << 4 }.para cambiar { array.size }.from(3) esperar
{ array << 5 }.para cambiar { array. size }.to(5) esperar { array <<
6 }.to change { array.size }.from(5).to(6) esperar { array << 7 }.to change
{ array.size }.to( 7).de(6)
Probablemente no le sorprenda saber que también puede pasar un comparador (o cualquier
objeto que implemente el protocolo === ) desde y hacia:
11matchersincludedinrspecexpectations/03/block_matchers.rb
x = 5
esperar { x += 10 }.to change
{ x } .from(a_value_ between(2,
7)) .to(a_value_ between(12, 17))
Tenga en cuenta que hay un poco de problema al pasar un comparador, al menos si solo usa
hacia o desde (y no ambos). Considere esta expectativa:
11comparadoresincluidosenrspecexpectations/03/block_matchers.rb
x = 5
esperar { x += 1 }.para cambiar { x }.from(un_valor_entre(2, 7))
Esta expectativa pasa, porque el valor de x cambió, y originalmente era un valor entre 2 y 7. Sin
embargo, tal como se lee, podría esperar que solo pase si el valor final de x ya no está entre 2
y 7. Si le interesan los valores anteriores y posteriores , es una buena idea especificar desde y
hasta.
informar fe de erratas • discutir
Machine Translated by Google
Tu turno • 209
Expectativas negativas
RSpec no le permite usar los tres métodos relativos por... o el método to con la forma de expectativa
negativa, espere { ... }.not_to change { ... }. Después de todo, cuando espera que un valor no cambie, no
tiene sentido especificar cuánto no cambió o el valor al que no cambió.
Sin embargo, las expectativas negativas funcionan con :
11comparadoresincluidosenrspecexpectations/03/block_matchers.rb
x = 5
esperar { }.not_to change { x }.from(5)
En este ejemplo, queremos que x permanezca en 5 antes y después de que se ejecute el bloque.
Producción
Muchas herramientas de Ruby escriben la salida en stdout o stderr, y RSpec incluye un comparador
específico para estos casos:
11matchersincludedinrspecexpectations/03/block_matchers.rb
expect { print 'OK' }.to output('OK').to_stdout expect { warn
'problem' }.to output(/prob/). to_stderr
Este comparador funciona reemplazando temporalmente la variable global $stdout o $stderr con StringIO
mientras ejecuta su bloque de expectativa . Esto generalmente funciona bien, pero tiene algunas trampas.
Por ejemplo, si usa la constante STDOUT explícitamente o genera un subproceso que escribe en uno
de los flujos, este comparador no funcionará correctamente. En su lugar, puede encadenar to_std(out|
err)_from_any_process para estas situaciones:
11matchersincludedinrspecexpectations/03/block_matchers.rb
expect { system('echo OK') }.to output("OK\n").to_stdout_from_any_process
El formulario ...from_any_process utiliza un mecanismo diferente: reabre temporalmente la secuencia
para escribir en un Tempfile. Esto funciona en más situaciones, pero es mucho, mucho más lento: 30
veces, según nuestros puntos de referencia. Por lo tanto, debe optar explícitamente por esta versión más
lenta del comparador de salida .
Tu turno
¡Hemos cubierto mucho terreno en este capítulo! Desde bloques de construcción básicos de Ruby como
cadenas y números, pasando por colecciones profundamente anidadas, hasta métodos con efectos
secundarios, puede encontrar un comparador que se adapte a sus necesidades.
informar fe de erratas • discutir
Machine Translated by Google
Capítulo 11. Comparadores incluidos en las expectativas de RSpec • 210
Todos estos emparejadores integrados en RSpec están diseñados para ayudarlo a hacer dos cosas:
• Exprese exactamente cómo desea que se comporte el código, sin ser demasiado estricto
o demasiado laxo
• Obtenga retroalimentación precisa cuando algo se rompa para que pueda encontrar exactamente
donde ocurrio la falla
Es mucho más importante tener en cuenta estos dos principios que memorizar todos los diferentes
emparejadores. A medida que pruebe suerte en los siguientes ejercicios, consulte el Apéndice 3, Hoja de
trucos del emparejador, en la página 307 para obtener inspiración para que prueben diferentes
emparejadores.
Ejercicios
Dado que los comparadores lo ayudan a diagnosticar fallas, queremos mostrarle cómo obtener mensajes
de falla útiles al elegir el comparador adecuado. Escribimos los siguientes ejercicios para fallar a
propósito. Si bien puede corregir el problema subyacente en tantos ejercicios como desee, concéntrese
primero en experimentar con diferentes emparejadores.
Números de teléfono coincidentes
Cree un nuevo archivo de especificaciones con la siguiente descripción para una clase que coincida con
los números de teléfono de una cadena y se los proporcione a la persona que llama, uno por uno:
11matchersincludedinrspecexpectations/exercises/phone_number_extractor_spec.rb
RSpec.describe PhoneNumberExtractor do
dejar(:texto) hacer
<<EOS
Melinda: (202) 5550168
Bob: 2025550199
Sabina: (202) 5550176
EOS
fin
' produce números de teléfono a medida que los encuentra' do
yielded_numbers = []
PhoneNumberExtractor.extract_from(texto) hacer |número|
números_rendidos << fin del número
expect(números_rendidos).to eq [ '(202)
5550168',
'2025550199',
'(202) 5550175'
] fin
fin
informar fe de erratas • discutir
Machine Translated by Google
Tu turno • 211
Aquí hay una implementación parcial de la especificación para que la agregues en la parte superior del
archivo, no lo suficiente como para aprobar, pero lo suficiente como para mostrar que hay espacio para
mejorar en nuestras especificaciones:
11matchersincludedinrspecexpectations/exercises/phone_number_extractor_spec.rb
class PhoneNumberExtractor def
self.extract_from(text, &block)
# Busque patrones como (###) ####### text.scan(/
(d{3}) d{3}d{4}/, &block) end
fin
Ejecute este archivo a través de RSpec. Ahora, eche un vistazo al código de especificación. Tuvimos que
trabajar mucho para configurar una colección separada para los números de teléfono proporcionados y luego
compararla. Cambie este ejemplo para usar un comparador que se adapte mejor a cómo funciona el método
extract_from . Observe cuánto más simple y clara es la especificación ahora.
Año de la bandera
Los próximos dos ejercicios estarán en el mismo grupo de ejemplo y compartirán la misma clase de
implementación. Comencemos con la especificación, que describe una empresa pública ficticia (llamada así
por un río) que tuvo un buen año. Agregue el siguiente código a un nuevo archivo:
11matchersincludedinrspecexpectations/exercises/public_company_spec.rb
RSpec.describe PublicCompany do
let(:company) { PublicCompany.new('Nile', 10, 100_000) }
' aumenta su capitalización de mercado cuando obtiene ingresos mejores de lo esperado '
before_market_cap = empresa.market_cap
company.got_better_better_than_expected_revenues
after_market_cap = empresa.market_cap
esperar (después de la capitalización de mercado antes de la capitalización de mercado). ser >=
50_000 fin
fin
En la parte superior de su archivo, agregue la siguiente implementación (aún no correcta) de la clase
PublicCompany :
11matchersincludedinrspecexpectations/exercises/public_company_spec.rb
PublicCompany = Struct.new(:name, :value_per_share, :share_count) do
def obtuvo_ingresos_mejores_de_los_esperados
self.value_per_share *= rand(1.05..1.10) fin
def market_cap
@market_cap ||= value_per_share * share_count end
fin
informar fe de erratas • discutir
Machine Translated by Google
Capítulo 11. Comparadores incluidos en las expectativas de RSpec • 212
Ejecute su especificación y observe el mensaje de error: esperado: >= 50000 / obtenido: 0. Es
bastante conciso y realmente no comunica la intención del código.
Actualice la expectativa para describir cómo debe comportarse el código, en lugar de cuál es el valor
de una variable.
Acerca de nuestra
empresa También queremos comprobar que nuestra clase almacena correctamente toda la
información que los inversores querrán saber sobre la empresa. Agregue el siguiente ejemplo dentro
del grupo de ejemplos del ejercicio anterior, justo después del otro ejemplo:
11matchersincludedinrspecexpectations/exercises/public_company_spec.rb '
proporciona atributos' do
expect(company.name).to eq('Nil')
expect(company.value_per_share).to eq(10) expect
(empresa.share_count).to eq(10_000)
expect(company.market_cap).to eq(1_000_000) end
Cuando ejecuta este nuevo ejemplo, RSpec detiene la prueba en el primer error, en company.name.
No podemos ver si alguno de los otros atributos
eran correctos.
Use un comparador diferente aquí que verifique todos los atributos e informe sobre cualquier
diferencia entre lo que esperamos y cómo se comporta realmente el código.
Tokenización de
palabras Para este ejercicio, estamos probando un tokenizador que divide el texto en palabras
individuales. Agregue la siguiente especificación a un nuevo archivo:
11matchersincludedinrspecexpectations/exercises/tokenizer_spec.rb
RSpec.describe Tokenizer do
dejar(:texto) hacer
<<EOS
Soy Sam.
yo soy sam
¿Te gustan los huevos verdes con jamón?
EOS
fin
' tokeniza múltiples líneas de texto' do tokenized =
Tokenizer.tokenize(text) expect(tokenized.first(6)).to
eq ['I', 'am', 'Sam.', 'Sam', 'I' , 'soy'] fin
fin
Agregue la siguiente implementación incorrecta de la clase Tokenizer en la parte superior de su nuevo
archivo:
informar fe de erratas • discutir
Machine Translated by Google
Tu turno • 213
11matchersincludedinrspecexpectations/exercises/tokenizer_spec.rb
class Tokenizer
def self.tokenize(string)
string.split(/ +/) end end
Ejecute la especificación y lea el mensaje de error. Nuestra especificación detectó el error, pero no proporcionó
ningún contexto más allá de las seis palabras que solicitamos. Además, si alguna vez actualizamos esta
especificación, debemos tener mucho cuidado para mantener el parámetro de longitud, primero (6), sincronizado
con la lista de palabras esperadas.
Cambie su especificación para usar un comparador más preparado para el futuro que no requiera que
extraigamos una cantidad codificada de tokens.
Bloques de construcción de la
naturaleza Para este ejemplo, desarmaremos las moléculas que forman el mundo que nos rodea.
Afortunadamente, es solo una simulación. Cree un nuevo archivo con las siguientes especificaciones para el
agua:
11matchersincludedinrspecexpectations/exercises/
water_spec.rb RSpec.describe
Water do it 'is H2O' do
expect(Agua.elementos.ordenar).to eq [:hidrógeno, :hidrógeno, :oxígeno] end
fin
En la parte superior de su archivo, agregue una implementación de Agua a la que le falte uno de sus átomos de
hidrógeno:
11matchersincludedinrspecexpectations/exercises/water_spec.rb
clase Agua
def self.elements
[:oxígeno, :hidrógeno] fin
fin
Ejecute su especificación. Fallará correctamente, pero la salida deja mucho que desear. Solo tenemos dos
colecciones volcadas en la consola, y depende de nosotros leerlas a mano y descubrir qué es diferente. Con
solo unos pocos artículos, comparar a mano es manejable, pero las diferencias se vuelven mucho más difíciles
de detectar a medida que las colecciones crecen.
Además, eche un vistazo a la llamada de clasificación que tuvimos que agregar. Esta especificación no tiene
nada que ver con la clasificación, pero teníamos que clasificar la colección para asegurarnos de que solo
estuviéramos comparando los elementos sin tener en cuenta el orden.
informar fe de erratas • discutir
Machine Translated by Google
Capítulo 11. Comparadores incluidos en las expectativas de RSpec • 214
Solucione nuestro error aquí y use un comparador cuyo mensaje de falla explique claramente la
diferencia entre las dos colecciones.
Trabajando para el fin de semana
Para nuestro ejemplo final, vamos a escribir una especificación relacionada con el calendario que
determina si un día determinado es el fin de semana:
11matchersincludedinrspecexpectations/exercises/
calendar_spec.rb RSpec.describe Calendar do
let(:sunday_date) { Calendar.new('Sun, 11 Jun 2017') }
' considera que los domingos son el fin de semana' espera
( sunday_date.on_weekend ?).
fin
Aquí hay una implementación obviamente incorrecta de on_weekend? método:
11matchersincludedinrspecexpectations/exercises/calendar_spec.rb
requieren 'fecha'
Calendar = Struct.new(:date_string) ¿ def
on_weekend?
Fecha.parse(fecha_cadena).sábado? fin
fin
Cuando ejecuta esta especificación, obtiene la frase forzada "para ser verdad" en la salida.
Cambie este comparador a uno que se lea más claramente en el informe de la prueba.
Puntos extra
Como mencionamos anteriormente, el punto principal de estos ejercicios era practicar el uso de
comparadores que expresan exactamente lo que quiere decir sobre el comportamiento de su
código (y nada más), y que le brindan un resultado claro.
Por lo tanto, no hay necesidad de corregir las implementaciones subyacentes. Pero para obtener
crédito adicional, siéntase libre de aprobar las especificaciones. Publique sus soluciones en los
foros y le enviaremos un GIF de una estrella dorada.
De cualquier manera, encuéntrenos en el próximo capítulo para ver cómo puede crear sus propios
emparejadores que sean tan expresivos como los integrados.
informar fe de erratas • discutir
Machine Translated by Google
En este capítulo, verá:
• Qué tan buenos son los comparadores para obtener especificaciones legibles y útiles
producción
• Cómo definir nuevos emparejadores en términos de los integrados de RSpec • Cómo
usar el DSL de RSpec para hacer un nuevo emparejador • Cómo se ve
una clase de emparejador personalizada escrita a mano
CAPÍTULO 12
Creación de emparejadores personalizados
En el capítulo anterior, hicimos un recorrido por los emparejadores que se envían con RSpec.
Puede ser productivo en RSpec solo con estos emparejadores. En proyectos más simples, son todo
lo que necesitará.
Eventualmente, sin embargo, llegarás a los límites de los emparejadores integrados.
Debido a que están destinados a probar el código Ruby de uso general, requieren que hable en
términos de Ruby en lugar de los términos de su proyecto.
Por ejemplo, las siguientes expectativas son un revoltijo de llamadas a métodos Ruby y valores
codificados:
12creatingcustommatchers/01/custom_matcher/spec/
event_spec.rb expect(art_show.tickets_sold.count).to
eq(0) expect(u2_concert.tickets_sold.count).to eq(u2_concert.capacity)
Se necesita mucha lectura y análisis, además de comprender la API de emisión de boletos que
estamos probando, para comprender exactamente qué comportamiento estamos buscando. Por el
contrario, el siguiente fragmento expresa el mismo comportamiento deseado en términos mucho más claros:
12creatingcustommatchers/01/custom_matcher/spec/
event_spec.rb expect(art_show).to
have_no_tickets_sold expect(u2_concert).to be_sold_out
Mucho, mucho mejor. Agregamos dos comparadores personalizados , have_no_tickets_sold y
be_sold_out, para que podamos describir el comportamiento en términos de eventos y boletos.
Estos son los términos que usaría el resto del equipo del proyecto.
Cuando escribe comparadores personalizados claros y fáciles de usar, obtiene varios beneficios:
• Tiene una mayor oportunidad de construir lo que quieren sus partes interesadas.
• Reduce el costo de los cambios de API (porque solo necesita actualizar su
emparejadores).
• Puede proporcionar mejores mensajes de error cuando algo sale mal.
informar fe de erratas • discutir
Machine Translated by Google
Capítulo 12. Creación de emparejadores personalizados • 216
Esa ventaja final, mejor resultado de prueba, requiere una mirada más cercana. El fragmento
original produciría mensajes de error como este:
esperado: 0
obtenido: 2
(comparado usando ==)
esperado: 10000
obtenido: 9900
(comparado usando ==)
La salida solo dice " x esperado / obtuve y", sin ninguna pista sobre los conceptos de nivel
superior. Por el contrario, consulte los mensajes de error del segundo fragmento:
se esperaba que #<Event "Art Show" (capacidad: 100)> no tuviera boletos vendidos, pero tenía 2
se esperaba que #<Evento "U2 Concert" (capacidad: 10000)> se agotara, pero tenía 100 entradas sin
vender
Este informe no solo habla el idioma de nuestro dominio, sino que también proporciona detalles
adicionales, como qué eventos específicos estamos probando aquí. En este capítulo, le
mostraremos cómo crear comparadores personalizados como estos dando pequeños pasos hacia
adelante a partir de los conceptos de RSpec que ya conoce.
Delegación a emparejadores existentes usando métodos auxiliares
Vamos a comenzar con una técnica que ya usó para mantener su código organizado: los métodos
auxiliares. RSpec proporciona sus propios comparadores a través de métodos integrados, como
container_exactly(...). Puede escribir fácilmente sus propios métodos que devuelvan los mismos
objetos de comparación pero usando nombres específicos para su dominio.
También puede agregar sus propias personalizaciones, como argumentos predeterminados.
Cuando desarrolló la aplicación de seguimiento de gastos en Creación de una aplicación con
RSpec 3, utilizó la expresión de comparación a_hash_incluyendo(id: some_id) para representar
un gasto esperado en particular. Aquí hay un ejemplo de uso:
12creatingcustommatchers/02/expense_tracker/spec/integration/app/ledger_spec.rb
expect(ledger.expenses_on('20170610')).to contains_exactly( a_hash_incluye(id:
result_1.expense_id), a_hash_incluye (id: result_2.expense_id)
Este emparejador hizo el trabajo. Sin embargo, observe cómo expresa la expectativa en términos
de objetos Ruby: hash e ID. El objeto que estás describiendo es un gasto. Sería bueno usar el
lenguaje de dominio de la aplicación:
informar fe de erratas • discutir
Machine Translated by Google
Delegación a emparejadores existentes usando métodos auxiliares • 217
12creatingcustommatchers/03/expense_tracker/spec/integration/app/ledger_spec.rb
expect(ledger.expenses_on('20170610')).to contains_exactly(
un_gasto_identificado_por(resultado_1.id_gasto),
un_gasto_identificado_por(resultado_2.id_gasto)
)
Es fácil implementar este nuevo comparador an_expense_identified_by . Todo lo que tiene que hacer
es escribir un método auxiliar con este nombre que delegue a a_hash_incluido.
Para que su nuevo comparador esté disponible para todas sus especificaciones, defínalo en un módulo
dentro de spec/spec_helper.rb. Luego, configure RSpec para incluir este módulo en todos sus grupos
de ejemplo:
12creatingcustommatchers/03/expense_tracker/spec/spec_helper.rb
módulo ExpenseTrackerMatchers
def un_gasto_identificado_por(id)
un_hash_incluido(id: id) end
fin
RSpec.configure do |config|
config.include ExpenseTrackerMatchers # ...resto
de la configuración...
No hay magia aquí. Es solo un método que delega a otro, como lo haces todo el tiempo en Ruby.
Sin embargo, hay algo en este método que podría hacer que te detengas.
Hemos definido an_expense_identified_by para que coincida con cualquier hash que contenga una clave
de identificación con un valor particular. Por ejemplo, un hash completamente no relacionado, como un
registro de usuario, engañaría a nuestro comparador:
{
identificación: 1,
correo electrónico: '[email protected]',
rol: 'admin'
}
Si este comparador pretende igualar solo los gastos, probablemente desee reducir las posibilidades de
un falso positivo. Puede hacerlo comprobando la presencia de las otras claves que contendría un gasto
válido:
12creatingcustommatchers/04/expense_tracker/spec/spec_helper.rb
def an_expense_identified_by(id)
a_hash_incluyendo(id: id).e incluyendo(:beneficiario, :cantidad, :fecha) end
El uso de un nombre específico de dominio hizo evidente que nuestra lógica de coincidencia era
demasiado genérica. Esta simple mejora hace que el comparador sea más robusto y también lo pone a
disposición de todo su conjunto de especificaciones.
informar fe de erratas • discutir
Machine Translated by Google
Capítulo 12. Creación de emparejadores personalizados • 218
Aaron Kromer describe otro gran uso de esta técnica en su publicación de blog, "Adiós a las
gemas de la API de JSON". 1
En él, utiliza un método de ayuda para devolver un
comparador que describe la forma exacta de una respuesta JSON que espera de una API en
particular.
Definición de alias de Matcher
En Pasar un comparador a otro, en la página 177, vio cómo RSpec define a_value_within
como un alias del comparador be_within . Le permite escribir expectativas que se leen sin
problemas como la siguiente:
12creatingcustommatchers/05/custom_matchers.rb
esperar(resultados).comenzar_con_un_valor_dentro de(0.1).de(Math::PI)
Puede utilizar las mismas técnicas en sus propios proyectos. Simplemente llame a alias_matcher
con el nombre del nuevo comparador primero, seguido del existente (el mismo orden que
usaría con el alias_method de Ruby):
12creatingcustommatchers/05/custom_matchers.rb
RSpec::Matchers.alias_matcher :an_admin, :be_an_admin
Este fragmento define un nuevo método, an_admin, que envuelve el comparador be_an_admin
existente (¿un comparador de predicado dinámico que llama a admin?; consulte Predicados
dinámicos, en la página 194). El nuevo comparador utilizará "un administrador", en lugar de
"ser un administrador", en su descripción y mensajes de error:
>> be_an_admin.description =>
"ser administrador"
>> an_admin.description =>
"un administrador"
El método alias_matcher también puede tomar un bloque, para cuando desee algo diferente
del nombre del método Ruby del comparador para sus descripciones. Por ejemplo, si desea
que an_admin aparezca como superusuario en la salida:
>> an_admin.description =>
"un superusuario"
…podrías definir tu alias así:
12creatingcustommatchers/05/custom_matchers.rb
RSpec::Matchers.alias_matcher :an_admin, :be_an_admin do |old_description|
old_description.sub('ser administrador', 'superusuario') end
Los comparadores de predicados dinámicos como estos son objetivos comunes para este tipo
de alias, ya que RSpec no se envía con sus propios alias para ellos.
1. http://aaronkromer.com/blog/20140929farewelljsonapigems.html
informar fe de erratas • discutir
Machine Translated by Google
Coincidencias negativas • 219
Coincidencias negativas
En Uniendo las piezas, en la página 172, vimos los métodos not_to y to_not de RSpec, que
especifican que una condición dada no debe cumplirse:
12creatingcustommatchers/05/
custom_matchers.rb expect(correct_grammar).to_not split_infinitives
Si te encuentras haciendo esto una y otra vez, puedes definir un comparador negado que
usarías así:
12creatingcustommatchers/05/
custom_matchers.rb expect(correct_grammar).to Avoid_splitting_infinitives
Es fácil crear su propio comparador negado. Todo lo que tienes que hacer es llamar a
define_negated_matcher:
12creatingcustommatchers/05/
custom_matchers.rb RSpec::Matchers.define_negated_matcher :evitar_dividir_infinitivos,
:split_infinitivos
Al igual que con alias_matcher, pasa el nombre del nuevo comparador, seguido del anterior. El
buscador de coincidencias Avoid_splitting_infinitives ahora se comportará como la negación de
split_infinitives.
Los emparejadores negativos son útiles para casos más complejos, como cuando se combinan
emparejadores. Por ejemplo, la siguiente expectativa es ambigua y RSpec nos advierte de
este problema:
>> esperar(adverbio).no_comenzar_con('a').y terminar_con('z')
NotImplementedError: ̀expect(...).not_to matcher.and matcher` no es compatible, ya
que crea un poco de ambigüedad. En su lugar, defina versiones negadas de cualquier
emparejador que desee negar con
`RSpec::Matchers.define_negated_matcher` y use ̀expect(...).to matcher.and
matcher`.
« retroceso truncado »
La ambigüedad es sutil: ¿debería coincidir el adverbio “absolutamente” (porque satisface la
condición “no termina en z”)? ¿O debería no coincidir (porque no cumple ambas condiciones)?
El mensaje de error de RSpec señala la ambigüedad y sugiere emparejadores negados como
alternativa. Así es como se verían esos emparejadores negados:
12creatingcustommatchers/05/
custom_matchers.rb RSpec::Matchers.define_negated_matcher :start_with_something_besides,
:Empezar con
RSpec::Matchers.define_negated_matcher :end_with_something_besides,
:terminar con
informar fe de erratas • discutir
Machine Translated by Google
Capítulo 12. Creación de emparejadores personalizados • 220
Ahora, podemos especificar el comportamiento exacto que queremos, sin ambigüedad:
12creatingcustommatchers/05/custom_matchers.rb #
Estricto: requiere que se cumplan ambas condiciones
expect('blazingly').to( start_with_something_besides('a').and
\ end_with_something_besides('z')
)
# Permisivo: requiere que se cumpla al menos una condición
expect('absolutamente').to( start_with_something_besides('a').or
\ end_with_something_besides('z')
)
Las técnicas que hemos visto hasta ahora (métodos auxiliares, alias de emparejadores y emparejadores
negados) tienen que ver con exponer los emparejadores existentes con nuevos nombres.
En la siguiente sección, daremos el siguiente paso lógico: crear un nuevo comparador que no esté basado
en uno existente.
Uso del emparejador DSL
En Creación de una aplicación con RSpec 3, creó una API de seguimiento de gastos. Si esto crece para
incluir cuentas de gastos, terminará escribiendo especificaciones que verifiquen los saldos de las cuentas.
En esta sección, vamos a crear un comparador have_a_balance_of personalizado que ayude con esas
expectativas. Así es como se verá finalmente el emparejador:
12creatingcustommatchers/06/custom_matcher/spec/initial_account_spec.rb
esperar(cuenta).to have_a_balance_of(30)
A diferencia de la mayoría de los otros ejemplos del libro, los fragmentos de código de esta sección no están
destinados a que los escriba mientras lee. Queremos centrarnos solo en el comparador, sin saturar los
ejemplos con todo el código de soporte. Si desea ejecutar estos fragmentos en su máquina, puede obtener
la clase que estamos probando (así como el comparador y algunas especificaciones) del código fuente del
libro.2
Hay dos formas de construir un comparador como el que acabamos de mostrarte:
Uso del emparejador DSL Para
la mayoría de las necesidades, RSpec proporciona un lenguaje específico de dominio (DSL) para definir
emparejadores personalizados.
Creación de una clase de Ruby
3
Cualquier clase de Ruby puede definir un comparador si implementa el protocolo de emparejamiento.
2. https://github.com/rspec3book/bookcode/blob/v1.0/12creatingcustommatchers/06/custom_matcher/lib/
cuenta.rb
3. http://rspec.info/documentation/3.6/rspecexpectations/RSpec/Matchers/MatcherProtocol.html
informar fe de erratas • discutir
Machine Translated by Google
Uso del Matcher DSL • 221
Te mostraremos ambas técnicas. Comenzaremos con el enfoque que generalmente recomendamos, el
emparejador DSL, y luego le mostraremos cómo se vería el mismo emparejador como una clase de Ruby.
Un emparejador personalizado mínimo
Para definir un comparador usando el DSL, llamamos a RSpec::Matchers.define, pasando el nombre del
comparador y un bloque que contiene la definición del comparador:
12creatingcustommatchers/06/custom_matcher/spec/support/
matchers.rb RSpec::Matchers.define :have_a_balance_of do |cantidad|
partido { |cuenta| cuenta.saldo_actual == monto } fin
El bloque exterior recibe los argumentos pasados al comparador. Cuando una especificación llama a
have_a_balance_of(amount), RSpec pasará la cantidad a este bloque.
El método de coincidencia define la lógica real de coincidencia/no coincidencia. El bloque interno recibe el
tema de la expectativa (la cuenta) y devuelve un valor real si el saldo de la cuenta coincide con la cantidad
esperada.
Mejora de los mensajes de error del comparador personalizado
Este emparejador fue fácil de escribir, pero no debemos declarar la victoria todavía.
Aquí está la salida que produce cuando falla una especificación:
1) ̀have_a_balance_of(amount)` falla cuando el saldo no coincide Fallo/Error:
esperar(cuenta).tener_un_saldo_de(35)
se esperaba que #<Account name="Checking"> tuviera un saldo de 35 # ./
spec/initial_account_spec.rb:17:en ̀bloque (2 niveles) en <top
El mensaje de falla nos dice que la cuenta debería haber tenido un saldo de 35.
Pero no dice cuál fue el saldo real . Podemos agregar esta información usando los métodos failure_message
y failure_message_when_negated :
12creatingcustommatchers/07/custom_matcher/spec/support/
matchers.rb RSpec::Matchers.define :have_a_balance_of do |cantidad|
partido { |cuenta| cuenta.saldo_actual == monto } mensaje_fallo
{ |cuenta| super() + motivo_fallo(cuenta) } mensaje_fallo_cuando_negado { |cuenta|
super() + fail_reason(cuenta) }
privado
def fail_reason(cuenta) ", pero
tenía un saldo de #{cuenta.saldo_actual}" end
fin
Definimos dos métodos para que RSpec pueda proporcionar nuestro texto de error personalizado tanto
para expect(...).to(...) como para expect(...).not_to(...). Al igual que con el partido, cada uno de estos métodos
informar fe de erratas • discutir
Machine Translated by Google
Capítulo 12. Creación de emparejadores personalizados • 222
tomar un bloque que recibe un valor de cuenta . Ahora, podemos obtener el saldo real de la
cuenta y ponerlo en el mensaje de error.
Tendremos que mantener la primera parte del mensaje de error que proporciona RSpec, la parte
que dice, esperaba... tener un saldo de 35. Al llamar a super(), delega a la implementación de
RSpec existente para esta parte .
Los emparejadores son solo clases
La razón por la que los emparejadores pueden llamar a super() al igual que las clases de Ruby es que
son clases de Ruby. RSpec::Matchers.define crea una clase de Ruby, y el bloque que le pasas es el
cuerpo de la clase. Dentro del bloque, podemos hacer cualquier cosa que haríamos dentro de una clase
de Ruby, incluida la definición de métodos auxiliares privados.
La mayor parte del mensaje de falla será el mismo para los comparadores regulares y negados,
por lo que hemos abstraído esa parte común en un método auxiliar de razón de falla . Eche un
vistazo a la salida con estos cambios en su lugar:
1) ̀have_a_balance_of(amount)` falla cuando el saldo no coincide Fallo/Error:
esperar(cuenta).tener_un_saldo_de(35)
esperaba que #<Account name="Checking"> tuviera un saldo de 35, pero tenía un saldo
de 30
# ./spec/initial_account_spec.rb:17:in ̀bloque (2 niveles) en <top
Mucho más útil.
Agregar una interfaz fluida
4
Muchos de los comparadores integrados de RSpec utilizan una donde puedes encadenar
interfaz fluida, un método tras otro para crear una expectativa fácil de entender:
• be_within(0.1).of(50)
• change { ... }.from(x).to(y)
• output(/warning/).to_stderr
Es fácil agregar este mismo tipo de interfaces fluidas a sus propios emparejadores personalizados.
Continuando con el ejemplo del saldo de la cuenta, supongamos que nuestra clase Cuenta
proporciona dos métodos para consultar el saldo:
• saldo_actual •
saldo_a_de(fecha)
Sería bueno admitir ambos métodos desde el mismo comparador, al permitir que las
especificaciones agreguen un modificador as_of opcional con una fecha:
4. https://martinfowler.com/bliki/FluentInterface.html
informar fe de erratas • discutir
Machine Translated by Google
Uso del Matcher DSL • 223
12creatingcustommatchers/08/custom_matcher/spec/
as_of_account_spec.rb expect(account).to
have_a_balance_of(30) # o
esperar(cuenta).tener_un_saldo_de(10).as_of(Fecha.nueva(2017, 6, 12))
Matcher DSL de RSpec ofrece un método de cadena para definir este estilo de interfaz.
Aquí hay un comparador actualizado que admite llamadas encadenadas a as_of:
12creatingcustommatchers/08/custom_matcher/spec/support/matchers.rb
Línea 1 RSpec::Matchers.define :have_a_balance_of do |cantidad|
cadena(:a_de) { |fecha| @as_of_date = fecha }
coincidencia { |cuenta| saldo_cuenta(cuenta) == cantidad }
mensaje_fallo { |cuenta| super() + motivo_fallo(cuenta) } 5
mensaje_fallo_cuando_negado { |cuenta| super() + fail_reason(cuenta) }
privado
def fail_reason(cuenta)
10 ", pero tenía un saldo de #{account_balance(cuenta)}" fin
def balance_cuenta(cuenta) if
@a_la_fecha
15 cuenta.saldo_a_de(@a_la_fecha) else
cuenta.saldo_actual end
fin
20 fin
La mayoría de las líneas de este fragmento han cambiado; aquí hay un desglose de lo que es diferente:
• En la línea 2, llamamos a chain(:as_of), que define el método as_of para nosotros; este método
oculta la fecha en una variable de instancia para que podamos pasarla a la cuenta que estamos
probando.
• El nuevo método account_balance en la línea 13 busca una fecha (que solo estará presente si la
persona que llama usó as_of), luego llama al método subyacente balance_as_of o current_balance
según corresponda.
• Los métodos match y failure_reason ahora usan el nuevo ayudante account_balance
método.
Cuando falla una especificación, el mensaje de error mencionará automáticamente la fecha a partir de
si una especificación lo llama:
1) ̀have_a_balance_of(cantidad)` falla cuando el saldo de una fecha no coincide Fallo/Error:
esperar(cuenta).to
have_a_balance_of(15).as_of(Date.new(2017, 6, 12)) esperado
#<Nombre de cuenta= "Cuenta corriente"> para tener un saldo de 15 al
informar fe de erratas • discutir
Machine Translated by Google
Capítulo 12. Creación de emparejadores personalizados • 224
#<Fecha: 20170612 ((2457917j,0s,0n),+0s,2299161j)>, pero tenía un saldo de 10
# ./spec/as_of_account_spec.rb:19:in ̀bloque (2 niveles) en <top
Este comparador personalizado se está volviendo bastante conveniente de usar ahora. Pero
deberíamos tomarnos un momento para considerar cómo interactuará con otros emparejadores.
Hacer Componible Nuestro Matcher
Los emparejadores basados en DSL se pueden usar en expresiones compuestas a través de y/o, sin
ninguna configuración adicional. Nuestro nuevo comparador de saldo de cuenta ya admite el siguiente
uso:
12creatingcustommatchers/08/custom_matcher/spec/as_of_account_spec.rb
expect(account).to have_a_balance_of(30).and \
have_attributes(name: 'Checking')
También podemos definir alias para él...
12creatingcustommatchers/08/custom_matcher/spec/support/matchers.rb
RSpec::Matchers.alias_matcher :una_cuenta_con_un_saldo_de, :tener_un_saldo_de
… y luego pasar estos alias a otros comparadores:
12creatingcustommatchers/08/custom_matcher/spec/as_of_account_spec.rb
expect(user_accounts).to include(an_account_with_a_balance_of(30))
Sin embargo, hay una cosa que nuestro comparador aún no admite: no podemos pasarle otros
emparejadores. Sería bueno poder consultar los saldos de las cuentas.
usando algo que no sea la igualdad estricta (¿por qué preocuparse por monedas de cinco centavos y
diez centavos cuando estamos tratando con cuentas de gastos multimillonarias?):
12creatingcustommatchers/09/custom_matcher/spec/composed_account_spec.rb
esperar(cuenta).to have_a_balance_of(a_value < 11_000_000)
# o
esperar(cuenta).tener_un_saldo_de(un_valor_dentro de(50).de(10_500_000))
Solo se necesita un pequeño cambio para que esto funcione. El bloque de coincidencia existente
compara la cantidad usando el operador == :
12creatingcustommatchers/09/custom_matcher/spec/support/matchers.rb
cuenta_saldo(cuenta) == cuenta
En su lugar, necesitaremos usar el valor_coincidencia de RSpec. método:
12creatingcustommatchers/09/custom_matcher/spec/support/matchers.rb
valores_coincidencia?(cantidad, saldo_cuenta(cuenta))
informar fe de erratas • discutir
Machine Translated by Google
Definición de una clase de comparación • 225
Al igual que con los marcos de aserción tradicionales, el valor esperado va primero. Si este valor
es un comparador, RSpec lo tratará como tal; de lo contrario, recurrirá a la comparación usando
==.
Aquí está el bloque completo del partido ahora:
12creatingcustommatchers/09/custom_matcher/spec/support/
matchers.rb match { |cuenta| valores_coincidencia?(cantidad, cuenta_saldo(cuenta)) }
Los métodos DSL que ha visto aquí son todo lo que necesita para comenzar con sus propios
emparejadores. Sin embargo, RSpec proporciona varios otros métodos para ayudarlo a ajustar el
comportamiento de sus comparadores. Puede imprimir diferencias significativas entre los valores
reales y esperados, proporcionar un manejo de excepciones personalizado y más.
Para obtener más información, consulte la lista de métodos DSL.5
Definición de una clase de comparación
La mayoría de las veces, usará el DSL para definir comparadores personalizados. A veces, sin
embargo, necesita un poco más de control o puede preferir definir el comparador de la manera
más explícita posible.
Como mencionamos anteriormente en el capítulo, cualquier objeto de Ruby que implemente el
protocolo de coincidencia puede servir como un comparador RSpec. Es bastante fácil traducir el
ejemplo de DSL de la sección anterior a una clase de Ruby que tenga los métodos necesarios.
La clase de rubí
Esta clase se trata de una página de código, pero tiene muchas similitudes con la versión DSL
más corta que escribimos. Eche un vistazo aquí, y luego mencionaremos algunos aspectos
destacados:
12creatingcustommatchers/10/custom_matcher/spec/support/matchers.rb
Línea 1 clase HaveABalanceOf
incluir RSpec::Matchers::Componible
def inicializar (cantidad)
5 @cantidad = cantidad
fin
def as_of(fecha)
@as_of_date = fecha
10 propia
fin
5. http://rspec.info/documentation/3.6/rspecexpectations/RSpec/Matchers/DSL/Macros.html
informar fe de erratas • discutir
Machine Translated by Google
Capítulo 12. Creación de emparejadores personalizados • 226
def partidos? (cuenta)
15 @cuenta = cuenta
¿valores_coincidencia?(@cantidad, saldo_cuenta)
fin
descripción def
20 si @as_of_date
"tener un saldo de #{description_of(@amount)} al #{@as_of_date}"
demás
"tener un saldo de #{description_of(@amount)}"
fin
25 fin
def mensaje_fallo
"se esperaba #{@account.inspect} a #{description}" + fail_reason
fin
30
def fail_message_when_negated
"se esperaba que #{@account.inspect} no #{description}" + fail_reason
fin
35 privado
def fail_motivo
", pero tenía un saldo de #{account_balance}"
fin
40
def cuenta_saldo
si @as_of_date
@cuenta.saldo_as_of(@as_of_date)
demás
45 @cuenta.saldo_actual
fin
fin
fin
Recorramos esta clase de arriba a abajo. En primer lugar, incluimos la
RSpec::Matchers::Composable mixin on line 2.6 Este módulo define algunos métodos
para ti que hacen posible la composición, incluyendo and, or, y el operador === .
También proporciona ayudantes a los que puede llamar cuando está definiendo un orden superior.
comparador, como valores_coincidencia? y descripción_de.
En el inicializador de la línea 4, almacenamos la cantidad deseada en una variable de instancia
para que tengamos algo con lo que comparar en los partidos? método.
En la línea 8, el método as_of proporciona la interfaz fluida que permite a las personas que llaman escribir
esperar(...).tener_un_saldo_de(...).como_de(...). Se parece mucho a la versión DSL, excepto
que aquí tenemos que devolver self explícitamente. Sin esa línea, as_of regresaría
6. http://rspec.info/documentation/3.6/rspecexpectations/RSpec/Matchers/Composable.html
informar fe de erratas • discutir
Machine Translated by Google
Definición de una clase de comparación • 227
un objeto Fecha . RSpec intentaría usar la fecha como un objeto comparador y la especificación
fallaría.
A continuación, los partidos? El método en la línea 14 se parece a la versión DSL, excepto que
necesitamos aferrarnos a la @cuenta para que los mensajes de error puedan acceder a ella.
Tenemos que definir el método de descripción explícitamente en la línea 19, en lugar de que RSpec
lo genere a partir de los nombres de los comparadores y modificadores. El ayudante description_of
(del módulo Composable ) es como llamar a inspeccionar, pero con un manejo especial para los
comparadores. Lo usamos aquí porque estamos definiendo un comparador de orden superior.
El resto de los métodos, los mensajes de falla y los ayudantes privados, son básicamente los mismos
que sus contrapartes de DSL.
Integración RSpec Hasta
ahora, acabamos de definir una clase de Ruby, HaveABalanceOf. Para que este código esté
disponible para RSpec como comparador, necesitamos definir un método auxiliar, have_a_balance_of:
12creatingcustommatchers/10/custom_matcher/spec/support/matchers.rb
módulo AccountMatchers
def have_a_balance_of(cantidad)
HaveABalanceOf.nuevo(cantidad)
fin
fin
RSpec.configure do |config|
config.include Final de AccountMatchers
Aquí, pusimos el ayudante en un módulo y configuramos RSpec para incluir el módulo, haciendo
que el método esté disponible para nuestros ejemplos.
La clase Matcher se parece a su equivalente DSL, pero ocupa mucho más espacio y tiene que incluir
bastante código repetitivo. En la mayoría de los casos, será mejor que utilice el DSL para definir sus
emparejadores.
Sin embargo, en ciertas situaciones, la clase de comparación personalizada se ajusta mejor:
• Si su comparador se va a usar cientos o miles de veces, escribir su propia clase evita un poco
de sobrecarga adicional inherente a la forma en que se evalúa el DSL.
• Algunos equipos prefieren un código más explícito.
• Si omite la mezcla RSpec::Matchers::Composable , su comparador no tendrá ninguna
dependencia en RSpec y funcionará en contextos que no sean RSpec.
informar fe de erratas • discutir
Machine Translated by Google
Capítulo 12. Creación de emparejadores personalizados • 228
Como ejemplo de ese último beneficio, la biblioteca Shoulda Matchers define comparadores
independientes del marco que funcionan con RSpec, Minitest y Test::Unit.7
Use el DSL a menos que esté escribiendo una biblioteca
La mayoría de las veces, querrá usar el DSL para definir sus emparejadores.
Le ahorrará espacio, será más fácil de leer y se integrará automáticamente con otras
características de RSpec.
Si está escribiendo una biblioteca de comparadores que las personas usarán para
probar sus propios proyectos (posiblemente no RSpec), es posible que desee
escribir una clase personalizada en su lugar.
Tu turno
Este capítulo abrió un camino gradual hacia los emparejadores personalizados. Comenzamos
creando comparadores personalizados a partir de los existentes de RSpec, utilizando métodos
auxiliares, alias y negación. A continuación, definimos un comparador completamente nuevo usando
DSL de RSpec. Finalmente, descorrimos el telón y demostramos que no hay magia detrás de los
emparejadores. Son simplemente clases de Ruby.
Con un buen conjunto de comparadores personalizados, sus especificaciones serán más legibles.
Cuando hay una falla, el mensaje de error mejorado le ahorrará tiempo para encontrar la causa.
Ahora es el momento de intentar escribir tu propio comparador personalizado.
Ejercicios
En la parte superior del capítulo, analizamos algunos comparadores hipotéticos para una API de
venta de entradas para conciertos. En estos ejercicios, implementará estos comparadores.
Para mantener las cosas simples, está bien poner todo su código para este ejercicio en un archivo.
Comience con la clase Evento que probará:
12creatingcustommatchers/exercises/custom_matcher/spec/event_matchers_spec.rb
Event = Struct.new(:name, :capacidad) do def
buy_ticket_for(guest) tickets_sold <<
guest end
def entradas_vendidas
@entradas_vendidas ||= []
fin
7. https://github.com/thoughtbot/shouldamatchers
informar fe de erratas • discutir
Machine Translated by Google
Tu turno • 229
def inspeccionar
"#<Evento #{nombre.inspeccionar} (capacidad: #{capacidad})>"
end
fin
Ahora es el momento de agregar las primeras especificaciones.
Cuando no se han vendido entradas
Ponga el siguiente grupo de ejemplo después de su clase:
12creatingcustommatchers/exercises/custom_matcher/spec/event_matchers_spec.rb
RSpec.describe '`have_no_tickets_sold` matcher' do example
'superando las expectativas' do
art_show = Event.new('Art Show', 100)
expect(art_show).to have_no_tickets_sold end
ejemplo 'expectativa fallida' hacer
espectáculo_de_arte = Event.new(' Exhibición de Arte',
100) espectáculo_de_arte.comprar_entrada_para(:un_amigo)
expect(art_show).to have_no_tickets_sold end end
Continúe y ejecute su archivo de especificaciones. Ambos ejemplos deberían fallar, porque el
comparador have_no_tickets_sold aún no existe.
Implemente este comparador usando el comparador DSL. Cuando su lógica de coincidencia sea
correcta, tendrá un ejemplo de aprobación y uno de falla. Luego, puede centrar su atención en
proporcionar un buen mensaje de falla.
Selling Out
Ahora que sabemos cómo manejar las ventas de boletos sin éxito, probemos qué sucede con
un concierto con entradas agotadas. Agregue el siguiente grupo de ejemplo:
12creatingcustommatchers/exercises/custom_matcher/spec/event_matchers_spec.rb
RSpec.describe '`be_sold_out` matcher' do example
'superando las expectativas' do u2_concert
= Event.new('U2 Concert', 10_000) 10_000.times
{ u2_concert.compra_ticket_for(:a_fan) }
esperar (u2_concert).to be_sold_out end
informar fe de erratas • discutir
Machine Translated by Google
Capítulo 12. Creación de emparejadores personalizados • 230
ejemplo 'expectativa fallida' do u2_concert
= Event.new('U2 Concert', 10_000) 9_900.times
{ u2_concert.purchase_ticket_for(:a_fan) }
esperar (u2_concert).to be_sold_out end
fin
Al igual que con el ejercicio anterior, este fragmento utiliza un comparador, be_sold_out,
que no se ha definido. Agregue esa definición ahora. Como lo hizo antes, obtenga la
lógica de coincidencia correcta antes de pasar al mensaje de error.
Puntos extra
Ahora que ha creado este comparador con DSL, vuelva a implementarlo como una clase
de emparejador.
informar fe de erratas • discutir
Machine Translated by Google
Parte V
Simulacros RSpec
Un conjunto de pruebas robusto se ejecutará rápido, será determinista
y cubrirá todas las rutas de código esenciales. Desafortunadamente,
las dependencias a menudo se interponen en el camino de estos objetivos.
A veces, no podemos probar el código de manera confiable mientras
está integrado con otras bibliotecas o sistemas.
En esta parte del libro, vamos a hablar sobre los dobles de prueba,
incluidos los objetos simulados. Estos le permiten controlar estrictamente
el entorno en el que se ejecutan sus pruebas. El resultado serán
especificaciones más rápidas y confiables.
Machine Translated by Google
En este capítulo, verá:
• Cómo los dobles pueden aislar su código de sus dependencias •
Las diferencias entre simulacros, stubs, espías y objetos nulos •
Cómo agregar un comportamiento doble de prueba a un objeto Ruby
existente • Cómo mantener sus dobles y sus objetos reales sincronizados
CAPÍTULO 13
Comprender los dobles de prueba
En las películas, un doble de acción reemplaza a un actor, absorbiendo un puñetazo o una caída
cuando el actor no puede o no debe hacerlo. En marcos de prueba como RSpec, un doble de
prueba cumple el mismo rol. Sustituye a otro objeto durante la prueba.
Ha usado este concepto antes, en Test Doubles: Mocks, Stubs, and Others, en la página 67.
Cuando escribió las especificaciones de su unidad API, trató la capa de almacenamiento como si
se comportara exactamente como lo necesitaba, aunque ¡esa capa aún no había sido escrita!
Esta capacidad de aislar partes de su sistema mientras las prueba es súper poderosa. Con los
dobles de prueba, puede:
• Ejercer rutas de código de difícil acceso, como el código de manejo de errores para un
servicio de terceros confiable
• Escriba las especificaciones para una capa de su sistema antes de haber creado todos sus elementos dependientes.
cies, como lo hizo con el rastreador de gastos
• Use una API mientras aún la está diseñando para que pueda solucionar problemas con
el diseño antes de dedicar tiempo a la implementación
• Demostrar cómo funciona un componente en relación con sus vecinos en el sistema
tem, lo que conduce a pruebas menos frágiles
En este capítulo, le mostraremos cómo comenzar con rspecmocks, la biblioteca integrada de
RSpec para crear dobles de prueba. Las técnicas que aprenda aquí y en los próximos dos
capítulos harán que sus especificaciones sean más rápidas y resistentes.
Tipos de dobles de prueba
Cuando presentamos por primera vez los dobles de prueba, insinuamos que hay diferentes
nombres para los dobles: simulacros, espías, etc. Pero pasamos por alto algunas de las diferencias.
Echemos un vistazo más de cerca ahora.
informar fe de erratas • discutir
Machine Translated by Google
Capítulo 13. Comprensión de los dobles de prueba • 234
Hay un par de maneras diferentes de pensar en un doble de prueba. Uno es el modo de uso del
doble, es decir, para qué lo está usando y qué espera que haga. La otra cosa a considerar es
cómo se crea el doble.
Llamaremos a esto el origen del doble.
Estos son los modos de uso de los que hablaremos en este capítulo:
Talón
Devuelve respuestas enlatadas, evitando cualquier cálculo significativo o E/S
Imitar
Espera mensajes específicos; generará un error si no los recibe al final del ejemplo
Objeto nulo
Un doble de prueba benigno que puede sustituir a cualquier objeto; se devuelve a sí mismo
en respuesta a cualquier mensaje
Espiar
Registra los mensajes que recibe, para que puedas consultarlos más tarde.
Estamos basando los términos aquí en el vocabulario que Gerard Meszaros desarrolló en su libro,
xUnit Test Patterns [Mes07].
Además de tener un modo de uso, un doble de prueba tiene un origen, que indica cuál es su clase
de Ruby subyacente. Algunos dobles se basan en objetos reales de Ruby y otros son totalmente
falsos:
Doble puro
Un doble cuyo comportamiento proviene completamente del marco de prueba; esto es lo que
normalmente piensa la gente cuando habla de objetos simulados
Doble parcial
Un objeto Ruby existente que adopta un comportamiento doble de prueba; su interfaz es una
mezcla de implementaciones reales y falsas
Verificando Doble
Totalmente falso como un doble puro, pero restringe su interfaz en función de un objeto real
como un doble parcial; proporciona un doble de prueba más seguro al verificar que coincide
con la API que está representando
Constante recortada
Una constante de Ruby, como una clase o un nombre de módulo, que crea, elimina o
reemplaza para una sola prueba
informar fe de erratas • discutir
Machine Translated by Google
Modos de uso: Mocks, Stubs y Spies • 235
Cualquier doble de prueba dado tendrá tanto un origen como un modo de uso. Por ejemplo,
puede tener una doble acción pura como stub, o una doble acción verificadora como
un espía.
Vamos a explorar los diferentes tipos de dobles de forma interactiva en una sesión de IRB
en vivo. Por lo general, los marcos de objetos simulados asumen que se ejecutan dentro
de una prueba individual, pero los dobles de prueba de RSpec admiten un modo
independiente especial para este tipo de experimentos.
Para usar este modo, inicie IRB y solicite el siguiente archivo:
>> requiere 'rspec/simulacros/independiente' =>
verdadero
Mantenga esta sesión activa mientras prueba los siguientes ejemplos.
Modos de uso: Mocks, Stubs y Spies
Primero, hablemos de los diferentes modos de uso de los dobles de prueba.
Dobles de prueba genéricos
El método doble de RSpec crea un doble de prueba genérico que puede usar en cualquier
modo. La forma más sencilla de llamar a este método es sin argumentos. Probemos eso
ahora:
>> libro mayor = doble
=> #<Doble (anónimo)>
De alguna manera, este doble actúa como un objeto Ruby ordinario. A medida que le envíe
mensajes (en otras palabras, invoque métodos en él), aceptará algunos mensajes y
rechazará otros.
La diferencia es que un doble genérico le brinda más información de depuración que un
objeto Ruby normal. Intenta enviar el mensaje de registro a tu doble de prueba ahora:
>> libro mayor.registro(un: :gasto)
RSpec::Mocks::MockExpectationError: #<Doble (anónimo)> recibió un mensaje
inesperado :registro con ({:an=>:gasto})
« retroceso truncado »
Cuando enviamos este mensaje, el doble generó una excepción. Los dobles son estrictos
por defecto: rechazarán todos los mensajes excepto los que hayas permitido específicamente.
Veremos cómo hacerlo más adelante.
informar fe de erratas • discutir
Machine Translated by Google
Capítulo 13. Comprensión de los dobles de prueba • 236
Echa un vistazo dentro de ese mensaje de error. RSpec muestra tanto el nombre del
mensaje como los argumentos que enviamos a nuestro doble; esto ya es más información
que un Ruby NoMethodError típico.
RSpec describe el objeto solo como #<Doble (anónimo)>, sin ninguna pista sobre para
qué lo estamos usando. Puede obtener un poco más de detalles en el mensaje de error
al nombrar el rol que desempeña el doble; simplemente pase un nombre al método doble .
Dado que este objeto sustituye a una instancia de Ledger , llamémoslo 'Ledger':
>> libro mayor = double('Libro mayor')
=> #<Double "Ledger"> >>
ledger.record(an: :expense)
RSpec::Mocks::MockExpectationError: #<Double "Ledger"> recibió un mensaje inesperado: registro
con ({:an=>:gastos}) « retroceso truncado »
El mensaje de error contiene el nombre del rol ahora. Esta información adicional es útil
cuando usa varios dobles en el mismo ejemplo y necesita diferenciarlos.
Este mismo método doble puede crear cualquiera de los otros tipos de dobles de prueba
que usará en sus especificaciones: stubs, simulacros, espías y objetos nulos. En las
próximas secciones, vamos a echar un vistazo a cada uno de estos a su vez.
talones
Como dijimos al comienzo de este capítulo, los stubs son simples. Devuelven respuestas
preprogramadas y enlatadas. Los stubs son mejores para simular métodos de consulta,
es decir , métodos que devuelven un valor pero no tienen efectos secundarios.
La forma más sencilla de definir un stub es pasar un hash de nombres de métodos y
devolver valores al método double :
>> http_response = double('HTTPResponse', estado: 200, cuerpo: 'OK')
=> #<Doble "HTTPResponse"> >>
http_response.status => 200
>>
http_response.body => "OK"
Como alternativa, puede realizar estos dos pasos (crear el stub y configurar los mensajes
enlatados) por separado. Para hacerlo, pase ese mismo hash de nombres de métodos y
devuelva valores a allow(...).to receive_messages(...), así:
>> http_respuesta = doble('HTTPRespuesta')
=> #<Doble "HTTPResponse"> >>
permitir(http_response).recibir_mensajes(estado: 200, cuerpo: 'OK')
informar fe de erratas • discutir
Machine Translated by Google
Modos de uso: Mocks, Stubs y Spies • 237
=> {:estado=>200, :cuerpo=>"OK"} >>
http_response.status => 200
>> http_response.body =>
"OK"
De hecho, la sintaxis hash es solo una abreviatura para deletrear cada mensaje permitido
individualmente:
>> permitir (http_response).to receive(:status).and_return(200)
=> #<RSpec::Mocks::MessageExpectation #<Double "HTTPResponse">.status(cualquier
argumentos)>
>> allow (http_response).to receive(:body).and_return('OK')
=> #<RSpec::Mocks::MessageExpectation #<Doble "HTTPResponse">.body(cualquier
argumento)>
Esta sintaxis más detallada no le ofrece mucho para stubs simples como estos.
Pero será vital en el próximo capítulo, donde necesitamos más precisión.
Todos estos talones son simples. Observan mensajes específicos y devuelven el mismo valor
cada vez que reciben un mensaje determinado. No actúan de manera diferente en función de
sus argumentos y, de hecho, ignoran sus argumentos:
>> http_response.status(:args, :are, :ignored) => 200
>> http_response.body(:bloques, :son, :también) { :ignorado }
=> "Está bien"
En el próximo capítulo, hablaremos sobre cómo expresar los parámetros esperados y devolver
los valores con mayor precisión.
Los stubs como estos lo ayudan a probar un tipo específico de comportamiento, el tipo que se
puede verificar simplemente observando los valores devueltos. El método que está probando
normalmente realizará los siguientes pasos:
1. Consultar datos de una dependencia
2. Realice un cálculo sobre esos datos.
3. Devolver un resultado
Sus especificaciones pueden verificar el comportamiento de su objeto con solo mirar el valor
devuelto en el paso 3. Todo lo que tiene que hacer el stub es devolver una respuesta adecuada
a la consulta en el paso 1.
A veces, necesita probar un objeto que no encaja en este patrón. En estas situaciones, puede
recurrir a otro tipo de doble de prueba diseñado para este caso de uso: un objeto simulado.
informar fe de erratas • discutir
Machine Translated by Google
Capítulo 13. Comprensión de los dobles de prueba • 238
se burla
Los simulacros son excelentes cuando se trata de métodos de comando. Con estos, no es un
valor de retorno lo que le importa, sino un efecto secundario. He aquí una secuencia típica:
1. Recibir un evento del sistema
2. Toma una decisión en base a ese evento
3. Realizar una acción que tenga un efecto secundario
Por ejemplo, la función Responder de un bot de chat puede recibir un mensaje de texto, decidir
cómo responder y luego publicar un mensaje en la sala de chat. Para probar este comportamiento,
no es suficiente que su doble de prueba proporcione un valor de retorno fijo en el paso 3. Debe
asegurarse de que el objeto desencadenó el efecto secundario de publicar un mensaje correctamente.
Para usar un objeto simulado, lo programará previamente con un conjunto de mensajes que se
supone que debe recibir. Estas se denominan expectativas de mensaje. Los declara de la misma
manera que escribiría una expectativa normal en sus especificaciones: combinando el método de
expectativa con un comparador:
>> esperar (libro mayor). recibir (: registro)
=> #<RSpec::Mocks::MessageExpectation #<Double "Ledger">.record(any arguments)>
Una vez que haya creado un objeto simulado, normalmente lo pasará al código que está probando.
Al final de cada ejemplo de RSpec, RSpec verifica que todos los simulacros recibieron los
mensajes esperados.
Dado que está utilizando rspecmocks en modo independiente, deberá iniciar el paso de verificación
manualmente. Puede hacerlo llamando a RSpec::Mocks.verify:
>> RSpec::Mocks.verify
RSpec::Mocks::MockExpectationError: (Double "Ledger").record(*(any args))
esperado: 1 vez con cualquier argumento
recibido: 0 veces con cualquier argumento «
retroceso truncado »
Debido a que el libro mayor simulado no recibió los mensajes que esperaba, genera un mensaje
MockExpectationError . Si este código se ejecutara dentro de un ejemplo de RSpec, el ejemplo
fallaría.
También puede especificar el comportamiento opuesto: que un objeto simulado no debería recibir
un mensaje. Para hacerlo, niega la expectativa de recepción con not_to, tal como lo harías con
cualquier otra expectativa:
>> esperar(libro mayor).no_recibir(:restablecer)
=> #<RSpec::Mocks::MessageExpectation #<Double "Ledger">.reset(any
informar fe de erratas • discutir
Machine Translated by Google
Modos de uso: Mocks, Stubs y Spies • 239
argumentos)>
>> ledger.reset
RSpec::Mocks::MockExpectationError: (Doble "Ledger").reset(sin argumentos)
esperado: 0 veces con cualquier argumento
recibido: 1 vez «
backtrace truncado »
Aquí, vemos una falla porque el objeto simulado recibió un mensaje que específicamente
esperaba no recibir. Con rspecmocks, puede deletrear expectativas mucho más detalladas que
simplemente recibir o no recibir mensajes. Más tarde, verás cómo.
Objetos nulos
Los dobles de prueba que ha definido hasta ahora son estrictos: requieren que declare por
adelantado qué mensajes están permitidos. La mayor parte del tiempo, esto es lo que quieres.
Pero cuando su prueba doble necesita recibir varios mensajes, tener que deletrear cada uno
puede hacer que sus pruebas sean frágiles.
En estas situaciones, es posible que desee un doble de prueba que sea un poco más indulgente.
Ahí es donde entran los objetos nulos. Puede convertir cualquier doble de prueba en un objeto
nulo llamando a as_null_object en él:
>> yoshi = doble('Yoshi').as_null_object => #<Doble
"Yoshi">
>> yoshi.comer(:manzana)
=> #<Doble "Yoshi">
Este tipo de objeto nulo se conoce como agujero negro; responde a cualquier mensaje que se le
envíe y siempre se devuelve a sí mismo. Esto significa que puede encadenar una llamada de
método tras otra, para tantas llamadas como desee:
>> yoshi.eat(:manzana).then_shoot(:shell).then_stomp => #<Doble
"Yoshi">
Los objetos nulos son los placebos del mundo de las pruebas. Son objetos benignos que no
hacen nada, pueden representar cualquier cosa y pueden satisfacer cualquier interfaz.
Esta flexibilidad es útil para probar objetos que tienen varios colaboradores.
Si tiene una clase de ChatBot que interactúa con una sala y un usuario, es posible que desee
probar estas colaboraciones por separado. Mientras se enfoca en las especificaciones
relacionadas con el usuario, puede usar un objeto nulo para la sala.
espías
Una desventaja de los simulacros tradicionales es que interrumpen la secuencia normal Organizar/
Actuar/Afirmar a la que está acostumbrado en sus pruebas. Para ver lo que queremos decir,
escriba la siguiente definición de una clase Game :
informar fe de erratas • discutir
Machine Translated by Google
Capítulo 13. Comprensión de los dobles de prueba • 240
>> juego de clase
>> def auto.reproducir(personaje)
>> personaje.saltar
>> final >>
final
=> :reproducir
Cuando esté probando esta clase, primero organizará su prueba doble:
>> mario = doble('Mario')
=> #<Doble "Mario">
…afirma que recibirá el : mensaje de salto:
>> esperar(mario).recibir(:saltar)
=> #<RSpec::Mocks::MessageExpectation #<Doble "Mario">.jump(cualquier argumento)>
…y finalmente actúa jugando el juego:
>> Game.play(mario) => nulo
Se siente un poco retrógrado tener que afirmar antes de actuar. Los espías son una
forma de restaurar el flujo tradicional. Todo lo que tiene que hacer es cambiar la
expectativa de recepción a have_received, y luego puede mover su expectativa hasta el final:
>> mario = doble('Mario').as_null_object => #<Doble "Mario">
>> Juego.jugar(mario)
=> #<Doble "Mario">
>> esperar(mario).haber_recibido(:saltar) => nil
Tenga en cuenta que hemos tenido que definir mario como un objeto nulo. Si hubiera
sido un doble normal y estricto, habría fallado cuando llamó al método de reproducción
(porque la reproducción le habría enviado un mensaje de salto inesperado ).
Cuando espíe objetos con have_received, deberá usar objetos nulos o permitir
explícitamente los mensajes esperados:
>> mario = doble('Mario')
=> #<Doble "Mario">
>> permitir(mario).recibir(:saltar)
=> #<RSpec::Mocks::MessageExpectation #<Double "Mario">.jump(any arguments)> >> Game.play(mario) => nil
>> esperar(mario).haber_recibido(:saltar) => nil
Tener que deletrear el mismo mensaje dos veces (una vez antes de llamar a Game.play
y otra después) anula un poco el propósito de espiar. Es mas fácil
informar fe de erratas • discutir
Machine Translated by Google
Orígenes: dobles puros, parciales y verificadores • 241
solo para usar un objeto nulo y, de hecho, RSpec proporciona un buen método de espionaje
para este propósito:
>> mario = espia('Mario')
=> #<Doble "Mario">
>> Juego.jugar(mario)
=> #<Doble "Mario">
>> esperar(mario).haber_recibido(:saltar) => nil
Este alias no solo le ahorra un poco de código, sino que también expresa mejor su intención.
Estás declarando desde el principio que vas a utilizar este doble de prueba como espía.
Orígenes: dobles puros, parciales y verificadores
Ahora que hemos visto los diferentes modos de uso de los dobles de prueba, veamos de dónde
vienen.
Dobles puros
Todos los dobles de prueba que ha escrito hasta ahora en este capítulo son dobles puros: están
diseñados específicamente por rspecmocks y consisten completamente en el comportamiento
que les agrega. Puede pasarlos al código de su proyecto como si fueran reales.
Los dobles puros son flexibles y fáciles de usar. Son mejores para probar código donde puede
pasar dependencias. Desafortunadamente, los proyectos del mundo real no siempre son tan
fáciles de probar, y deberá recurrir a técnicas más poderosas.
Dobles parciales
A veces, el código que está probando no le brinda una manera fácil de inyectar dependencias.
Un nombre de clase codificado de forma rígida puede estar al acecho tres capas de profundidad
en ese método API que está llamando. Por ejemplo, muchos proyectos de Ruby llaman a
Time.now sin proporcionar una forma de anular este comportamiento durante la prueba.
Para probar este tipo de bases de código, puede usar un doble parcial. Estos agregan un
comportamiento de simulación y creación de apéndices a los objetos Ruby existentes. Eso
significa que cualquier objeto en su sistema puede ser un doble parcial. Todo lo que tiene que
hacer es esperar o permitir un mensaje específico, tal como lo haría con un doble puro:
>> aleatorio = Aleatorio.nuevo
=> #<Aleatorio:0x007ff2389554e8>
>> permitir(aleatorio) .recibir(:rand).and_return(0.1234)
=> #<RSpec::Mocks::MessageExpectation #<Random:0x007ff2389554e8>.rand(cualquier argumentos)>
>> random.rand
=> 0.1234
informar fe de erratas • discutir
Machine Translated by Google
Capítulo 13. Comprensión de los dobles de prueba • 242
En este fragmento, creó una instancia del generador de números aleatorios de Ruby y luego
reemplazó su método rand con uno que devuelve un valor enlatado.
Todos sus otros métodos se comportarán normalmente.
También puedes usar un doble parcial como espía, usando el formulario expect(...).to have_received
que viste antes:
>> permitir(Dir).para recibir(:mktmpdir).and_yield('/ruta/a/tmp')
=> #<RSpec::Mocks::MessageExpectation #<Dir (clase)>.mktmpdir(cualquier argumento)> >>
Dir.mktmpdir { |dir| pone "Dir es: #{dir}" }
Dir es: /ruta/a/tmp => nil
>> esperar(Dir).to have_received(:mktmpdir) => nil
Cuando usaba un doble puro como espía, tenía la opción de especificar por adelantado qué mensajes
debería permitir el espía. Puede permitir cualquier mensaje (usando spy o as_null_object), o permitir
explícitamente solo los mensajes que desee. Con los dobles parciales, solo puedes hacer lo último.
RSpec no admite la noción de un "espía parcial", porque no puede espiar todos los métodos de un
objeto real de manera eficaz.
Cuando usa dobles parciales dentro de sus especificaciones, RSpec revertirá todos sus cambios al
final de cada ejemplo. El objeto Ruby volverá a su comportamiento original. De esa manera, no
tendrás que preocuparte de que el comportamiento doble de la prueba se filtre a otras especificaciones.
Dado que está experimentando en modo independiente, deberá llamar a RSpec::Mocks.teardown
explícitamente para que ocurra esta misma limpieza:
>> RSpec::Mocks.teardown =>
#<RSpec::Mocks::RootSpace:0x007ff2389bccb0> >>
random.rand
=> 0.9385928886462153
Esta llamada también sale del modo independiente en el que ha estado experimentando.
Si desea seguir explorando en la misma sesión de IRB, deberá llamar a RSpec::Mocks.setup para
volver al modo independiente.
Los dobles de prueba tienen vidas cortas
RSpec derriba todos sus dobles de prueba al final de cada ejemplo.
Eso significa que no funcionarán bien con las características de RSpec que se
encuentran fuera del alcance típico por ejemplo, como los ganchos anteriores (: contexto) .
Puede solucionar algunas de estas limitaciones con un método
1
llamado with_temporary_scope.
1. https://relishapp.com/rspec/rspecmocks/v/36/docs/basics/scope
informar fe de erratas • discutir
Machine Translated by Google
Orígenes: dobles puros, parciales y verificadores • 243
Los dobles parciales son útiles, pero los consideramos un olor a código, una señal superficial
que podría conducirlo a un problema de diseño más profundo.2 En Uso efectivo de dobles
parciales, en la página 271, explicaremos algunos de estos problemas subyacentes y cómo a
ellos.
Verificación de dobles
La ventaja de los dobles de prueba es que pueden reemplazar una dependencia que no desea
arrastrar a su prueba. La desventaja es que el doble y la dependencia pueden desincronizarse
entre sí.3 Verificar los dobles puede protegerlo de este tipo de desviación.
En Dobles de prueba: simulacros, resguardos y otros, en la página 67, creó un doble de prueba
para ayudarlo a probar una API de alto nivel cuando su clase Ledger de nivel inferior aún no
existía. Más tarde explicamos que estaba usando un doble de verificación para esa especificación;
echemos un vistazo más de cerca a por qué era importante hacerlo.
Aquí hay una versión simplificada de un doble similar, sin verificación:
13comprensiónpruebadobles/02/expense_tracker/spec/unit/ledger_double_spec.rb
ledger = double('ExpenseTracker::Ledger')
allow(ledger).to receive(:record)
Cuando probó la API pública de su sistema, su código de enrutamiento se llamó Ledger#record:
13comprensiónpruebadobles/02/expense_tracker/app/
api.rb publicar '/gastos' hacer
gasto = JSON.parse(solicitud.cuerpo.leer)
resultado = @ledger.record(gasto)
JSON.generate('expense_id' => resultado.expense_id) end
La clase Ledger aún no existía; el doble de prueba proporcionó una implementación suficiente
para que pasaran las especificaciones de enrutamiento. Más tarde, construiste la cosa real.
Considere lo que sucedería si en algún momento cambiara el nombre del método Ledger#record
a Ledger#record_expense pero olvidara actualizar el código de enrutamiento. Sus especificaciones
aún pasarían, ya que todavía proporcionan un método de registro falso . Pero su código fallaría
en el uso del mundo real, porque está tratando de llamar a un método que ya no existe. Este tipo
de falsos positivos puede acabar con la confianza en las especificaciones de su unidad.
Evitó esta trampa en el proyecto de seguimiento de gastos mediante el uso de un doble de
verificación. Para hacerlo, llamó a instance_double en lugar de double, pasando el nombre de la
clase Ledger . Aquí hay una versión simplificada del código:
2. https://martinfowler.com/bliki/CodeSmell.html
3. https://www.thoughtworks.com/insights/blog/mockistsaredeadlongliveclassicists
informar fe de erratas • discutir
Machine Translated by Google
Capítulo 13. Comprensión de los dobles de prueba • 244
13comprensiónpruebadobles/02/expense_tracker/spec/unit/
ledger_double_spec.rb ledger =
instance_double('ExpenseTracker::Ledger') allow(ledger).to receive(:record)
Con este doble en su lugar, RSpec verifica que la clase Ledger real (si está cargada) realmente
responda al mensaje de registro con la misma firma. Si cambia el nombre de este método a
record_expense, o agrega o elimina argumentos, sus especificaciones fallarán correctamente hasta
que actualice su uso del método y su configuración doble de prueba.
Use la verificación de dobles para detectar
problemas antes Aunque las especificaciones de su unidad habrían tenido un falso
positivo aquí, sus especificaciones de aceptación aún habrían detectado esta regresión.
Eso es porque usan las versiones reales de los objetos, en lugar de contar con
dobles de prueba.
Al usar la verificación de dobles en las especificaciones de su unidad, obtiene lo mejor
de ambos mundos. Detectará errores antes y a menor costo, mientras escribe
especificaciones que se comportan correctamente cuando cambian las API.
RSpec le brinda algunas formas diferentes de crear dobles de verificación, en función de lo que
utilizará como plantilla de interfaz para el doble:
instancia_doble('AlgunaClase')
Restringe la interfaz del doble usando los métodos de instancia de SomeClass
class_double('AlgunaClase')
Restringe la interfaz del doble usando los métodos de clase de SomeClass
objeto_doble(algún_objeto)
Restringe la interfaz del doble utilizando los métodos de some_object, en lugar de una clase; útil
para objetos dinámicos que usan method_missing
Además, cada uno de estos métodos tiene una variante _spy (como instancia_espía) como una
conveniencia para usar un doble verificador como espía.
Constantes recortadas
Los dobles de prueba tienen que ver con controlar el entorno en el que se ejecutan sus
especificaciones: qué clases están disponibles, cómo se comportan ciertos métodos, etc. Una pieza
clave de ese entorno es el conjunto de constantes de Ruby disponibles para su código.
Con las constantes auxiliares, puede reemplazar una constante con otra diferente durante la duración
de un ejemplo.
Por ejemplo, los algoritmos de hashing de contraseñas son lentos por motivos de seguridad, pero es
posible que desee acelerarlos durante las pruebas. Algoritmos como
informar fe de erratas • discutir
Machine Translated by Google
Tu turno • 245
bcrypt toma un factor de costo ajustable para especificar qué tan costoso será el cálculo del hash.
Si su código define este número como una constante:
13comprensiónpruebadobles/03/stubbed_constants.rb
class PasswordHash
COSTO_FACTOR = 12
#...
fin
…sus especificaciones pueden redefinirlo a 1:
13comprensiónpruebadobles/03/stubbed_constants.rb
stub_const('PasswordHash::COST_FACTOR', 1)
Puede usar stub_const para hacer varias cosas:
• Definir una nueva constante
• Reemplazar una constante existente •
Reemplazar un módulo o clase completo (porque también son constantes) • Evitar cargar
una clase costosa, usando una falsificación liviana en su lugar
A veces, controlar su entorno de prueba significa eliminar una constante existente en lugar de
bloquearla. Por ejemplo, si está escribiendo una biblioteca que funciona con o sin ActiveRecord,
puede ocultar la constante ActiveRecord para un ejemplo específico:
13comprensiónpruebadobles/03/stubbed_constants.rb
hide_const('ActiveRecord')
Ocultar la constante ActiveRecord de esta manera cortará el acceso a todo el módulo, incluidas las
constantes anidadas como ActiveRecord::Base. Su código no podrá usar accidentalmente
ActiveRecord. Al igual que con los dobles parciales, cualquier constante que haya cambiado u
ocultado se restaurará al final de cada ejemplo.
Tu turno
En este capítulo, discutimos las diferencias entre stubs, simulacros, espías y objetos nulos. En
particular, viste cómo se enfrentan a las siguientes situaciones:
• Recibir mensajes esperados • Recibir
mensajes inesperados • No recibir mensajes
esperados
También analizamos las diferentes formas de crear dobles de prueba. Los dobles puros son
completamente falsos, mientras que los dobles parciales son objetos Ruby reales que tienen un
comportamiento falso agregado. Los dobles de verificación se encuentran en el medio y tienen las
ventajas de ambos con algunas de las desventajas de cualquiera. Son los que usamos con más frecuencia.
informar fe de erratas • discutir
Machine Translated by Google
Capítulo 13. Comprensión de los dobles de prueba • 246
Ahora que comprende los dobles de prueba, estará listo para abordar el siguiente capítulo, donde
configurará cómo y cuándo sus dobles responden a los mensajes. Pero primero, tenemos un
ejercicio simple que demuestra algunos matices de la verificación de dobles.
Ejercicio
En este ejercicio guiado, probará una clase Skier que colabora con una clase TrailMap .
Comenzando en un directorio nuevo, coloque el siguiente código en lib/skier.rb:
13comprensiónpruebadobles/ejercicios/montaña/lib/esquiador.rb
módulo Montaña
esquiador de clase
def initialize(trail_map) @trail_map
= final del trail_map
def ski_on(nombre_de_la_pista)
dificultad = @trail_map.difficulty(trail_name) @cansado =
verdadero si dificultad == : final experto
definitivamente cansado?
@cansado fin fin
fin
Ahora, cree un archivo llamado lib/trail_map.rb con el siguiente contenido:
13understandingtestdoubles/exercises/mountain/lib/trail_map.rb
pone 'Cargando nuestra biblioteca de consulta de base de
datos...' sleep(1)
módulo Montaña
clase TrailMap def
dificultad_de(trail_name)
# Busque el rastro en el final de la base de datos
fin
fin
La clase TrailMap tiene un método de dificultad_de , pero la clase Esquiador está tratando
incorrectamente de llamar a dificultad en su lugar. Si usamos un doble verificador para reemplazar
un TrailMap, debería poder detectar este tipo de error; intentemos eso.
Probar el doble de verificación
Cree un archivo llamado spec/skier_spec.rb y coloque la siguiente especificación en él:
informar fe de erratas • discutir
Machine Translated by Google
Tu turno • 247
13comprensiónpruebadobles/ejercicios/montaña/spec/skier_spec.rb
requiere 'esquiador'
módulo Montaña
RSpec.describe Esquiador hacer
se 'cansa después de esquiar una pendiente difícil' do
trail_map = instance_double('TrailMap', dificultad: :experto)
esquiador = Esquiador.nuevo(trail_map)
esquiador.ski_on('Último pitido')
esperar(esquiador).estar_cansado
end
end
end
Esta especificación comete el mismo error que cometió la clase Skier con los nombres de los métodos. Abre el
método de dificultad en lugar de la dificultad_de. Sin embargo, está utilizando instance_double, por lo que
RSpec debería detectar el problema, ¿verdad?
Intente ejecutar su especificación:
$ especificación
Sorprendentemente, las especificaciones pasan. RSpec solo puede verificar contra una clase real si esa clase
está realmente cargada. Sin nada contra lo que verificar, el doble verificador actúa como un doble normal que
no verifica. Entonces, intente ejecutarlo nuevamente con la clase TrailMap cargada; simplemente pase
rtrail_map en la línea de comando:
$ rspec rtrail_map
Las especificaciones aún pasan. Además, se ejecutan mucho más lentamente (¡casi 10 veces más lento en
nuestras computadoras!) debido al tiempo dedicado a cargar una dependencia pesada. Antes de continuar, vea
si puede adivinar por qué RSpec no compara su trail_map doble con la clase Mountain::TrailMap real .
El problema
El problema es que el nombre de la constante pasado a instance_double no coincide con la clase real. El
nombre completo de la clase TrailMap , incluido el módulo en el que está anidado, es 'Mountain::TrailMap'.
Cambie la llamada instance_double para usar el nombre correcto y luego vuelva a ejecutar sus especificaciones
(nuevamente, con rtrail_map). Esta vez, deberían fallar de la forma esperada: con un mensaje de error sobre
el uso de un método de dificultad inexistente .
Hay dos formas de detectar este tipo de problemas de nombres antes de que sucedan:
• Usar clases de Ruby en lugar de cadenas • Configurar
RSpec para verificar que el nombre de la clase existe
informar fe de erratas • discutir
Machine Translated by Google
Capítulo 13. Comprensión de los dobles de prueba • 248
Vas a tener la oportunidad de probar ambas opciones. Deshaga la corrección que acaba de hacer antes
de comenzar el siguiente paso del ejercicio.
Uso de constantes de Ruby
Primero, intentemos usar una constante de Ruby para indicar qué clase estás fingiendo.
En la llamada a instance_double, cambie la cadena 'TrailMap' a la clase TrailMap (sin comillas).
Ahora, ejecute sus especificaciones de la misma manera que lo hizo al comienzo de este ejercicio: rspec
simple sin argumentos de línea de comandos. La primera vez que intentó esto, RSpec dio un resultado
de aprobación incorrecto. Ahora, obtendrá un error Mountain::TrailMap constante no inicializado , porque
la clase TrailMap no está cargada.
Para usar la clase Ruby directamente de esta manera, deberá asegurarse de que la dependencia esté
cargada antes de que se ejecute la especificación. Si sus especificaciones usan la clase directamente
(como lo hace ahora), normalmente solo agregará require 'trail_map' en la parte superior de su archivo de
especificaciones.
Sin embargo, hay momentos en los que es posible que no desee cargar sus dependencias explícitamente
de esta manera:
• Sus dependencias tardan mucho en cargarse, como lo hace trail_map • Necesita usar
un doble de prueba antes de que exista la dependencia, ya que
hizo con el doble de Ledger en el proyecto de seguimiento de gastos
Ahora, deshazte del cambio que acabas de hacer y veremos la otra forma de detectar los problemas de
nombres de clase.
Configuración de RSpec para verificar
nombres En Configuración de biblioteca, en la página 161, usó un bloque RSpec.configure para configurar
rspecmocks. Usando el mismo tipo de bloque, puede configurar RSpec para asegurarse de que todos
sus dobles de verificación se basen en clases cargadas reales.
La configuración que necesita se llama verificar_dobled_constantes_nombres. Probablemente no quiera
activarlo incondicionalmente en spec_helper.rb. Si lo hiciera, ¡nunca podría usar un doble verificador antes
de que existiera su clase! En su lugar, coloque la configuración en un archivo que pueda cargar a pedido;
llamémoslo spec/support/verify_doubled_constants.rb:
13comprensióntestdoubles/exercises/mountain/spec/support/verify_doubled_constants.rb
RSpec.configure do |c|
c.mock_with :rspec do |simulacros|
mocks.verify_doubled_constant_names = final
verdadero
fin
informar fe de erratas • discutir
Machine Translated by Google
Tu turno • 249
Cuando desee que RSpec sea estricto con respecto a la verificación de dobles, simplemente
pase rsupport/verify_doubled_constants en la línea de comando:
$ rspec rtrail_map rsupport/verify_doubled_constants
Sus especificaciones fallarán correctamente y RSpec le advertirá que el nombre de la clase no
existe. Si utiliza este enfoque, le recomendamos que desarrolle con esta configuración
desactivada, pero configure su servidor de integración continua (CI) para que se ejecute con la
configuración activada.
Facilite la replicación de su configuración de CI
La repetibilidad es importante cuando está configurando un sistema CI.
Pocas cosas son más frustrantes que una especificación que pasa en su máquina
local pero falla en el servidor CI.
Si va a usar ciertas opciones solo con CI, como la configuración de
verificar_dobles_constantes_nombres , le recomendamos que coloque todas
estas opciones en una secuencia de comandos o una tarea Rake que pueda
ejecutar localmente. De esa manera, cuando falla una especificación en CI,
puede ejecutar algo como ./script/ci_build y diagnosticar el problema en su máquina.
Hablaremos más sobre la integración con Rake en el Apéndice 1, RSpec y el
ecosistema más amplio de Ruby, en la página 293.
Conclusión
Mientras terminamos, veamos las ventajas y desventajas que hemos visto. La verificación de
dobles hace lo siguiente:
• Generan errores cuando su código llama a una dependencia incorrectamente. •
Solo pueden hacerlo cuando la dependencia realmente existe. • Revierten
silenciosamente a dobles regulares si la dependencia no existe.
Para lidiar con ese último elemento, puede crear sus dobles a partir de nombres de clases de
Ruby en lugar de cadenas. Solo es práctico hacerlo si ya ha escrito código para la dependencia
y si no es demasiado costoso cargarlo. Si no puede usar una clase de Ruby, puede volver a
verificar sus nombres de constantes configurando verify_doubled_constant_names cuando
ejecuta toda su suite.
El uso correcto de la verificación de dobles requiere un poco más de cuidado por adelantado.
Pero los beneficios para su proyecto valen la pena.
informar fe de erratas • discutir
Machine Translated by Google
En este capítulo, verá:
• Cómo devolver, aumentar o generar un valor de su doble •
Cómo proporcionar un comportamiento personalizado para
su doble • Cómo asegurarse de que su doble se llame con los argumentos
correctos • Cómo asegurarse de que su doble se llame la cantidad
correcta de veces y en el orden correcto
CAPÍTULO 14
Personalización de dobles de prueba
Ahora que conoce los tipos básicos de dobles de prueba y cuándo usarlos, vamos a profundizar
un poco en la API de rspecmocks. Nuestro objetivo no es brindarle una referencia API exhaustiva
aquí; para eso están los documentos.1
En cambio, le mostraremos los conceptos básicos y luego le daremos algunas recetas para
situaciones específicas.
Configuración de respuestas
Dado que un doble de prueba está destinado a sustituir a un objeto real, debe actuar como tal.
Debe poder configurar cómo responde al código que lo llama.
Cuando permite o espera un mensaje en un doble de prueba sin especificar cómo responde,
RSpec proporciona una implementación simple que simplemente devuelve nil. Sus dobles de
prueba a menudo necesitarán hacer algo más interesante: devolver un valor dado, generar un
error, ceder ante un bloque o lanzar un símbolo. RSpec proporciona formas para que sus dobles
hagan cada uno de estos:
14customizingtestdoubles/01/configuring_responses.rb
permitir (doble). recibir ( : un_mensaje) . para
recibir(:un_mensaje).y_rendimiento(un_valor_para_un_bloque)
permitir(doble).para recibir(:un_mensaje).y_lanzar(:un_símbolo, valor_opcional)
permitir(doble).para recibir(:un_mensaje) { |arg| hacer_algo_con(arg) }
# Estos dos últimos son solo para dobles parciales:
permitir(objeto) .recibir(:un_mensaje).y_llamar_original
permitir(objeto) .recibir(:un_mensaje).y_envolver_original { |original| }
Ahora, veamos un par de situaciones específicas con las que te puedes encontrar cuando estás
especificando cómo se comportarán tus dobles de prueba.
1. https://relishapp.com/rspec/rspecmocks/v/36/docs/configuringresponses
informar fe de erratas • discutir
Machine Translated by Google
Capítulo 14. Personalización de dobles de prueba • 252
Las expectativas del método reemplazan sus originales
Las personas nuevas en RSpec a menudo se sorprenden con el comportamiento
de esperar en un doble parcial. El siguiente código:
esperar(algún_objeto_existente).recibir (:un_mensaje)
…no solo crea una expectativa. También cambia el comportamiento del objeto
existente. Las llamadas a some_existing_object.a_message devolverán nil y no
harán nada más. Si desea agregar una expectativa de mensaje manteniendo la
implementación original, deberá usar and_call_original.
Devolver valores múltiples
Ya usó and_return, en Dobles de prueba: simulacros, talones y otros, en la página 67. Allí,
configuró su doble de prueba para devolver el mismo elemento de gasto enlatado cada vez que
recibió el mensaje de registro .
A veces, necesita su método stubbed para hacer algo más sofisticado que devolver el mismo
valor cada vez que se llama. Es posible que desee devolver un valor para la primera llamada, uno
diferente para la segunda llamada y así sucesivamente.
Para estos casos, puede pasar varios valores a and_return:
>> permitir(aleatorio) .recibir(:rand).and_return(0.1, 0.2, 0.3)
=> #<RSpec::Mocks::MessageExpectation #<Double "Random">.rand(any arguments)> >> random.rand => 0.1
>> azar.rand
=> 0,2
>> azar.rand
=> 0,3
>> azar.rand
=> 0.3
>> aleatorio.rand
=> 0,3
Aquí damos tres valores de retorno y el método aleatorio devuelve cada uno en secuencia.
Después de la tercera llamada, el método continúa devolviendo 0.3, el valor final se pasa a
and_return.
Rendimiento de valores múltiples
Los bloques son omnipresentes en Ruby y, a veces, sus dobles de prueba deberán reemplazar
una interfaz que usa bloques. El método and_yield , acertadamente llamado, configurará su doble
para producir valores.
Para especificar una secuencia de valores para producir, encadene varias llamadas a and_yield:
informar fe de erratas • discutir
Machine Translated by Google
Configuración de respuestas • 253
14customizingtestdoubles/01/configuring_responses.rb
extractor = double('TwitterURLExtractor')
permitir (extractor). recibir (: extraer_urls_de_twitter_firehose)
.and_yield('https://rspec.info/', 93284234987) .and_yield('https://
github.com/', 43984523459) .and_yield('https://pragprog.com/',
33745639845)
Hemos encadenado tres llamadas and_yield . Cuando el código que estamos probando llama a
extract_urls_from_twitter_firehose con un bloque, el método cederá al bloque tres veces. Cada
vez, el bloque recibirá una URL y una ID de tweet numérica.
Generación de excepciones con flexibilidad
Cuando está probando el código de manejo de excepciones, puede generar excepciones de sus
dobles de prueba usando el modificador and_raise . Este método tiene una API flexible que
refleja el método de aumento de Ruby.2 Eso significa que todas las siguientes llamadas funcionarán:
14customizingtestdoubles/01/configuring_responses.rb
allow(dbl).to receive(:msg).and_raise(AnExceptionClass) allow(dbl).to
receive(:msg).and_raise('un mensaje de error') allow (dbl).para
recibir(:msg).and_raise(AnExceptionClass, 'con un mensaje')
an_exception_instance = AnExceptionClass.new
allow(dbl).para recibir(:msg).and_raise(an_exception_instance)
En los ejemplos que le mostramos hasta ahora, hemos estado trabajando con dobles de prueba
puros. A estos dobles se les debe decir exactamente cómo responder, porque no tienen una
implementación existente para modificar.
Los dobles parciales son diferentes. Dado que comienzan como un objeto real con
implementaciones de métodos reales, puede basar la versión falsa en la real. Veamos cómo
hacerlo.
Volver a la implementación original
Cuando usa un doble parcial para reemplazar un método, a veces solo desea reemplazarlo
condicionalmente. Es posible que desee utilizar una implementación falsa para ciertos valores de
parámetros, pero recurrir al método real el resto del tiempo.
En estos casos, puede esperar o permitir dos veces: una como lo haría normalmente y una vez
con and_call_original para proporcionar el comportamiento predeterminado.
14customizingtestdoubles/01/configuring_responses.rb
# implementación falsa para argumentos específicos:
allow(File).to receive(:read).with('/etc/passwd').and_raise('HAHA NOPE')
# retroceder:
permitir(Archivo).recibir (:leer).y_llamar_original
2. https://rubydoc.org/core2.4.1/Kernel.html#methodiraise
informar fe de erratas • discutir
Machine Translated by Google
Capítulo 14. Personalización de dobles de prueba • 254
Aquí, hemos usado with(...) para restringir a qué valores de parámetro se aplica este código auxiliar.
Hablaremos más sobre esto más adelante en Argumentos restrictivos, en la página 256.
Modificación del valor de retorno
A veces, desea cambiar ligeramente el comportamiento del método que está agregando, en lugar
de reemplazarlo por completo. Es posible que, por ejemplo, deba modificar su valor de retorno.
Para hacerlo, llame al método and_wrap_original de RSpec y pásele un bloque que contenga su
comportamiento personalizado. Su bloque tomará la implementación original como argumento, al
que puede llamar en cualquier momento.
Aquí, usamos esta técnica para crear una API de CustomerService para devolver un subconjunto
de clientes:
14customizingtestdoubles/01/configuring_responses.rb
allow(CustomerService).to receive(:all).and_wrap_original do |original|
all_customers = original.call
all_customers.sort_by(&:id).take(10) end
Esta técnica puede ser útil para las especificaciones de aceptación, en las que desea probar con un
servicio en vivo. Si el proveedor no proporciona una API de prueba que solo devuelva algunos
registros, puede llamar a la API real y reducir los registros usted mismo.
Al trabajar solo en un subconjunto de los datos, sus especificaciones se mantendrán ágiles.
Ajuste de argumentos
También puede usar and_wrap_original para ajustar los argumentos que pasa a un método. Esta
técnica es útil cuando el código que está probando usa muchos valores codificados.
En Stubbed Constants, en la página 244, usamos stub_const para llamar a un algoritmo hash con
un factor de costo más bajo para que nuestras especificaciones siguieran funcionando rápidamente.
Ese enfoque solo funcionó porque el costo se definió como una constante.
Si, en cambio, el número hubiera sido un valor de argumento codificado de forma rígida, podríamos
haberlo anulado usando and_wrap_original:
14customizingtestdoubles/01/configuring_responses.rb
allow(PasswordHash).to
receive(:hash_password) .and_wrap_original do |original, cost_factor|
final.llamada(1)
original
informar fe de erratas • discutir
Machine Translated by Google
Establecer restricciones • 255
Si el método que estás agregando toma argumentos (como cost_factor), RSpec los pasa como
parámetros adicionales a tu bloque.
Dado que tanto and_call_original como and_wrap_original necesitan una implementación
existente para llamar, solo tienen sentido para dobles parciales.
Cuando necesita más flexibilidad Hasta
ahora, hemos visto varias formas diferentes de personalizar el comportamiento de sus
dobles de prueba. Puede devolver o generar una secuencia específica de valores,
generar una excepción, etc.
A veces, sin embargo, el comportamiento que necesita está ligeramente fuera de lo que
proporcionan estas técnicas. Si no está seguro de cómo configurar un doble para hacer lo que
necesita, puede proporcionar un bloque que contenga el comportamiento personalizado que
necesite. Simplemente pase el bloque a la última llamada de método en la expresión de recepción .
Por ejemplo, es posible que desee simular una falla de red intermitente mientras realiza la
prueba. Aquí hay un ejemplo de un doble de prueba de API meteorológica que tiene éxito el
75 por ciento de las veces:
14personalizaciónpruebadobles/01/configuración_respuestas.rb
contador = 0
allow(weather_api).to receive(:temperature) do |zip_code| contador = (contador + 1)
% 4 contador.cero? ? aumentar (Tiempo
de espera :: Error): 35.0 fin
Cuando su código llame a weather_api.temperature(some_zip_code), RSpec ejecutará este
bloque y, según la cantidad de llamadas que haya realizado, devolverá un valor o generará
una excepción de tiempo de espera.
No te dejes llevar por los bloques
Si su bloque se vuelve más complejo que el ejemplo de API meteorológica aquí,
es mejor que lo mueva a su propia clase Ruby.
3 Las falsificaciones son
Martin Fowler se refiere a este tipo de sustituto como falso.
particularmente útil cuando necesita conservar el estado en varias llamadas a
métodos.
Establecer restricciones
La mayoría de los dobles de prueba que ha creado aceptarán cualquier entrada. Si agrega un
método llamado salto sin otras opciones, RSpec usará su código auxiliar cada vez que su
código llame a saltar, saltar (: con,: argumentos) o saltar {con_un_bloque }.
3. https://www.martinfowler.com/bliki/TestDouble.html
informar fe de erratas • discutir
Machine Translated by Google
Capítulo 14. Personalización de dobles de prueba • 256
En esta sección, veremos formas de establecer restricciones en un doble de prueba, de
modo que RSpec solo lo use si su código lo llama de cierta manera.
Argumentos restrictivos En sus
proyectos, a menudo querrá verificar que su código esté llamando a un método con los
parámetros correctos. Para restringir qué argumentos aceptará su objeto simulado,
agregue una llamada a su expectativa de mensaje:
14customizingtestdoubles/02/setting_constraints.rb
esperar(película).recibir (:record_review).with('¡Excelente película!')
esperar(película).recibir (:record_review).with(/Genial/ )
expect(película).para recibir(:record_review).with('¡Gran película!', 5)
Si su código llama al método con argumentos que no coinciden con la restricción,
entonces la expectativa permanece insatisfecha. RSpec lo tratará igual que cualquier otra
expectativa no satisfecha. En este ejemplo, estamos usando esperar, lo que significa que
RSpec informará una falla:
>> esperar(película) .recibir(:record_review).with('Good')
=> #<RSpec::Mocks::MessageExpectation #<Double "Jaws">.record_review("Bueno")> >>
movie.record_review('Malo')
RSpec::Mocks::MockExpectationError: #<Double "Jaws"> recibió :record_review
con argumentos inesperados esperados: ("Bueno")
obtuvo: ("Malo") «
retroceso
truncado »
Si hubiéramos utilizado allow en su lugar, RSpec habría buscado otra expectativa que se
ajustara a los argumentos pasados:
>> allow(imdb).to receive(:rating_for).and_return(3) # default =>
#<RSpec::Mocks::MessageExpectation #<Double "IMDB">.rating_for(cualquier
argumento)>
>> allow(imdb ).to receive(:rating_for).with('Jaws').and_return(5)
=> #<RSpec::Mocks::MessageExpectation #<Double "IMDB">.rating_for("Jaws")> >>
imdb.rating_for('Fin de semana en Bernies') => 3
>> imdb.rating_for('Tiburón') =>
5
RSpec le brinda muchas formas de restringir los argumentos del método. Sus dobles de
prueba pueden requerir algo tan simple como un valor específico o tan sofisticado como
cualquier lógica personalizada que pueda diseñar.
informar fe de erratas • discutir
Machine Translated by Google
Establecer restricciones • 257
No daremos una referencia exhaustiva aquí, pero nos gustaría mostrarle algunas situaciones con las
que es probable que se encuentre. Para obtener una lista completa de todo lo que puede especificar
para sus argumentos, consulte los documentos.4
Marcadores de posición de
argumentos Cuando un método toma varios argumentos, es posible que le importen más algunos que
otros. Por ejemplo, puede agregar el método add_product de un carrito de compras que toma un
nombre, una identificación numérica y un código específico del proveedor. Si solo le importa el nombre,
puede pasar el marcador de posición de cualquier cosa para los demás:
14customizingtestdoubles/02/setting_constraints.rb
expect(cart).to receive(:add_product).with('Sudadera con capucha', cualquier cosa, cualquier cosa)
También puede representar una secuencia de cualquier marcador de posición con any_args:
14customizingtestdoubles/02/setting_constraints.rb
expect(cart).to receive(:add_product).with('Hoodie', any_args)
El marcador de posición any_args es un poco como el operador "splat" que usa para definir un método
Ruby con un número flexible de argumentos. Puede ir a cualquier parte de la lista de argumentos, pero
solo puede aparecer una vez. Cualquiera de las siguientes llamadas satisfaría esta restricción particular:
14customizingtestdoubles/02/setting_constraints.rb
cart.add_product('Hoodie')
cart.add_product('Hoodie', 27182818)
cart.add_product('Hoodie', 27182818, 'HOODIESERIAL123')
La contraparte de any_args es no_args:
14customizingtestdoubles/02/setting_constraints.rb
esperar(base de datos).recibir (:eliminar_todas_las_cosas).con(sin_argumentos)
Como puede adivinar por el nombre, esta restricción solo coincide cuando llama al método sin
argumentos.
Hash y argumentos de palabras clave
Muchas API de Ruby, especialmente las escritas antes de que saliera Ruby 2.0, usan un hash de
opciones para proporcionar una interfaz flexible para las personas que llaman:
14customizingtestdoubles/02/setting_constraints.rb
class BoxOffice
def find_showtime(opciones) # ...
fin
fin
4. https://relishapp.com/rspec/rspecmocks/v/36/docs/settingconstraints/matchingarguments
informar fe de erratas • discutir
Machine Translated by Google
Capítulo 14. Personalización de dobles de prueba • 258
box_office.find_showtime(película: 'Tiburón')
box_office.find_showtime(película: 'Tiburón', código postal: 97204)
box_office.find_showtime(película: 'Tiburón', ciudad: 'Portland', estado: 'OR')
Cuando está probando un código que llama a dicho método, puede usar el hash_incluyendo de
RSpec para especificar qué claves deben estar presentes. Las tres llamadas a find_showtime
coincidirían con la siguiente restricción:
14customizingtestdoubles/02/setting_constraints.rb
expect(box_office).to receive(:find_showtime) .with(hash_incluir(movie:
'Jaws'))
Especifique exactamente el nivel de restricción que necesita
Las restricciones flexibles como hash_inclusive hacen que sus especificaciones sean menos frágiles.
En lugar de tener que dar todas las claves de tu hash, puedes dar solo las que te
interesan. Si cambia el valor de una clave sin importancia, sus especificaciones no
tienen por qué fallar.
Ruby 2.0 agregó argumentos de palabras clave al lenguaje, lo que proporciona una sintaxis específica
para este estilo de API:
14customizingtestdoubles/02/setting_constraints.rb
class BoxOffice
def find_showtime(película:, código postal: nil, ciudad: nil, estado: nil)
#...
fin
fin
La buena noticia es que la restricción hash_incluye funciona igual de bien con argumentos de
palabras clave que con hashes de opciones de estilo antiguo.
RSpec también proporciona una restricción de exclusión de hash para especificar que un hash no
debe incluir una clave en particular.
Lógica
personalizada Cuando haya escrito un montón de restricciones, inevitablemente se encontrará
repitiendo la misma restricción compleja en varias especificaciones. Ocasionalmente, necesitará una
lógica demasiado complicada para expresarla como una simple restricción. En ambas situaciones,
puede proporcionar su propia lógica personalizada.
Por ejemplo, si tiene varias especificaciones que deberían llamar específicamente a find_showtime
con ciudades de Oregón, puede incluir esta restricción en un comparador RSpec personalizado como
el que escribió en Un comparador personalizado mínimo, en la página 221:
14customizingtestdoubles/02/setting_constraints.rb
RSpec::Matchers.define :a_city_in_oregon do match { |
options| opciones[:estado] == 'O' && opciones[:ciudad] } fin
informar fe de erratas • discutir
Machine Translated by Google
Establecer restricciones • 259
Luego puede pasar su comparador personalizado a cualquiera con restricción:
14customizingtestdoubles/02/setting_constraints.rb
esperar(taquilla).recibir (:encontrar_hora del espectáculo).con(una_ciudad_en_oregon)
RSpec compara argumentos usando ===
Puede restringir los argumentos utilizando un valor de Ruby normal, una expresión
regular, una de las restricciones proporcionadas por RSpec o cualquier comparador
integrado o personalizado. Detrás de escena, rspecmocks compara argumentos
de métodos usando el operador === . Cualquier cosa que admita === se puede usar
como una restricción de argumento.
Las restricciones de argumentos personalizados pueden reducir la repetición y hacer que sus
expectativas sean más fáciles de entender.
Restricción de cuántas veces se llama a un método Además de restringir los
argumentos de un método, también puede especificar cuántas veces se debe llamar. Por ejemplo,
algunas aplicaciones usan un disyuntor que deja de intentar llamadas de red después de una cierta
cantidad de fallas.5 Así es como puede probar una clase de tablero de cotizaciones que protege a su
cliente de red con un disyuntor:
14customizingtestdoubles/02/stock_ticker.rb
client = instance_double('NasdaqClient')
expect(client).to receive(:current_price).thrice.and_raise(Timeout::Error) stock_ticker =
StockTicker.new(cliente) 100.veces
{ stock_ticker.price('AAPL') }
Aunque estamos llamando a stock_ticker.price muchas veces, esperamos que el disyuntor deje de
conectarse a la red después del tercer error de tiempo de espera simulado.
Como puede adivinar por el nombre tres veces, RSpec también proporciona modificadores una y dos
veces . Dado que el idioma inglés no proporciona ningún adverbio multiplicativo después de 3, deberá
cambiar a la restricción más detallada exactamente (n) veces para otros números:
14customizingtestdoubles/02/stock_ticker.rb
esperar(cliente).recibir (:precio_actual).exactamente(4).veces
Cuando no le importa la cantidad exacta de llamadas, pero tiene en mente un mínimo o máximo
determinado, puede usar at_least o at_most:
14customizingtestdoubles/02/stock_ticker.rb
esperar(cliente).recibir (:precio_actual).al_menos(3).veces esperar(cliente).recibir
(:precio_actual).como_máximo(10).veces
5. https://martinfowler.com/bliki/CircuitBreaker.html
informar fe de erratas • discutir
Machine Translated by Google
Capítulo 14. Personalización de dobles de prueba • 260
esperar(cliente) .recibir(:precio_actual).al_menos(:una vez)
esperar(cliente).recibir (:precio_actual).como_máximo(:tres veces)
Si su código llama al método demasiadas o muy pocas veces, obtendrá una expectativa
insatisfecha:
>> esperar(cliente) .recibir(:precio_actual).a lo sumo(:dos veces) \
>> .and_return(130.0)
=> #<RSpec::Mocks::VerifyingMessageExpectation
#<InstanceDouble(NasdaqClient) (anónimo)>.current_price(any arguments)> >> stock_ticker =
StockTicker.new(cliente)
=> #<Ticker de acciones>
>> stock_ticker.precio('AAPL') =>
130.0
>> cotización_valor.precio('AAPL')
=> 130.0
>> cotización_valor.precio('AAPL')
RSpec::Mocks::MockExpectationError: (InstanceDouble(NasdaqClient)
(anónimo)).current_price("AAPL") esperado:
como máximo 2 veces con cualquier argumento recibido:
3 veces con argumentos: ("AAPL") « retroceso
truncado »
Vale la pena señalar que no puede combinar las restricciones at_least y at_most .
ordenar
Normalmente, a RSpec no le importa en qué orden envía mensajes a un doble de prueba:
14customizingtestdoubles/02/setting_constraints.rb
esperar(saludar).recibir(:hola)
esperar(saludar).to recibir(:adiós)
# Pasará lo siguiente: saludo.adiós
saludo.hola
Si necesita hacer cumplir una orden específica, agregue el modificador ordenado :
14customizingtestdoubles/02/setting_constraints.rb
esperar(saludar) .recibir(:hola).pedido esperar(saludar).to
recibir(:adiós).pedido
# Lo siguiente fallará: saludo.adiós
saludo.hola
El uso de ordered es una señal de que sus especificaciones pueden estar demasiado acopladas
a una implementación en particular. En el próximo capítulo, hablaremos más sobre los olores
de código como este y cómo lidiar con ellos.
informar fe de erratas • discutir
Machine Translated by Google
Tu turno • 261
Puede combinar los tres tipos de restricciones
Puede usar todos los tipos de restricciones que hemos visto aquí (argumentos, recuentos
de llamadas y ordenamiento) juntos en una expectativa:
esperar(catálogo).recibir (:buscar).
con(/término/).al_menos(:dos veces).pedido
¡Simplemente no te excedas! A menos que todas estas restricciones sean realmente
importantes, sus especificaciones pueden fallar incluso cuando el comportamiento
externo del código no ha cambiado.
Tu turno
En este capítulo, vimos varias formas diferentes de configurar cómo sus dobles de prueba responden a los
mensajes. Hablamos sobre cómo devolver, aumentar o producir valores específicos. Vimos cómo reemplazar
un método de una clase real pero aún usamos la implementación original detrás de escena.
También discutimos cómo restringir si un doble debe responder en absoluto para ciertos argumentos del
mensaje. Al restringir qué argumentos acepta la expectativa de un mensaje y cuántas veces debe llamarse,
puede ser tan específico o flexible como necesite en sus especificaciones.
Ejercicios
Para estos ejercicios, profundizará un poco más en las implementaciones de bloques de Cuando necesita
más flexibilidad, en la página 255. Las implementaciones de bloques son el comodín de los simulacros de
RSpec: dado que puede ponerles cualquier lógica arbitraria, siempre puede usar un bloque si no puede
recordar qué API integrada hace lo que necesita.
Implementaciones de bloques
Abra los ejercicios/block_implementation_spec.rb en el código fuente de este capítulo. Contiene siete
ejemplos, cada uno de los cuales llama a allow(test_double).toreceive(:message) con un bloque.
Los primeros cuatro ejemplos tratan sobre la configuración de respuestas a través de un bloque:
14customizingtestdoubles/exercises/block_implementation_spec.rb
RSpec.describe "Bloquear implementaciones que proporcionan respuestas" do
let(:test_double) { double }
" puede devolver un valor" permitir
(prueba_doble). recibir (: mensaje) hacer
# HACER
fin
expect(test_double.message).to eq(17) end
informar fe de erratas • discutir
Machine Translated by Google
Capítulo 14. Personalización de dobles de prueba • 262
" puede generar un error" hacer
permitir (prueba_doble). recibir (: mensaje) hacer
# HACER
fin
esperar { test_double.message }.to raise_error(/boom/) end
" puede producir un valor" do
permitir (prueba_doble). recibir (: mensaje) hacer | & bloquear |
# HACER
fin
esperar { |b| test_double.message(&b) }.to yield_with_args(1) end
" puede arrojar un símbolo" permitir
(test_double). recibir (: mensaje) hacer
# HACER
fin
esperar { test_double.message }.to throw_symbol(:foo) end
fin
Los últimos tres ejemplos tratan sobre la restricción de llamadas a métodos a través de un bloque:
14customizingtestdoubles/exercises/block_implementation_spec.rb
RSpec.describe "Bloquear implementaciones que verifican llamadas" do let(:test_double)
{ double }
" puede restringir los argumentos "
permitir (prueba_doble). recibir (: mensaje) hacer |arg|
# HACER
fin
esperar { test_double.message(:valid_arg) }.not_to raise_error esperar
{ test_double.message(:invalid_arg) }.to raise_error(/invalid_arg/) end
" puede contar cuántas veces se recibió el mensaje" do receive_count = 0
permitir (prueba_doble). recibir (: mensaje) hacer | & bloquear |
# TODO
fin
prueba_doble.mensaje
prueba_doble.mensaje
expect(receive_count).to eq(2) end
informar fe de erratas • discutir
Machine Translated by Google
Tu turno • 263
" puede restringir el orden en que se recibieron los mensajes" hacer
secuencia = []
permitir (prueba_doble). recibir (: mensaje_1) hacer
# HACER
fin
permitir (prueba_doble). recibir (: mensaje_2) hacer
# HACER
fin
prueba_doble.mensaje_1
prueba_doble.mensaje_2
prueba_doble.mensaje_1
expect(secuencia).to eq([:mensaje_1, :mensaje_2, :mensaje_1]) end
fin
Su tarea es completar el cuerpo de cada bloque (marcado con # TODO) para hacer el
pasan las especificaciones.
Interfaz fluida
Su solución hasta ahora ha demostrado cuán flexibles pueden ser sus dobles de prueba cuando usa una
implementación de bloque. Los bloques son lo suficientemente generales como para proporcionar cualquier
comportamiento que necesite.
Ahora, explorará la interfaz fluida de RSpec para dobles de prueba. Cada uno de los bloques que escribió
en el primer ejercicio tiene un equivalente rspecmocks más simple. Use las API que aprendió en este
capítulo para reemplazar cada bloque con una expresión más simple de permitir o esperar . Para dos de los
ejemplos de restricciones, también deberá editar o eliminar parte del código circundante.
informar fe de erratas • discutir
Machine Translated by Google
En este capítulo, verá:
• La importancia de construir cuidadosamente un
entorno para cada
especificación • Cómo proporcionar dobles de prueba al
código que está probando • Cuáles son los errores más comunes
y cómo evitarlos • Cómo mejorar su código aplicando comentarios
CAPÍTULO 15
de diseño de sus dobles de prueba
Uso efectivo de los dobles de prueba
En los dos capítulos anteriores, probó simulacros, stubs, espías y objetos nulos. Has aprendido para qué
situaciones es mejor cada una. También ha visto cómo configurar su comportamiento y cómo verificar que
un doble de prueba se llame correctamente.
Ahora nos gustaría hablar de las compensaciones. Aunque con frecuencia usamos dobles en nuestras
especificaciones, seremos los primeros en reconocer que hacerlo conlleva cierto riesgo.
Estos son algunos de los problemas con los que te puedes encontrar:
• Código que pasa las pruebas pero falla en producción, porque la prueba se duplica
no se comporte lo suficiente como la cosa real
• Pruebas frágiles que fallan después de una refactorización, aunque el nuevo código funciona
correctamente
• Pruebas sin hacer nada que solo terminan verificando sus dobles
En este capítulo, le mostraremos cómo usar los dobles de prueba de manera efectiva, lo que significa que
los beneficios para su proyecto superan estos riesgos.
Construcción de su entorno de prueba
Las personas nuevas en probar dobles a menudo preguntan cuánto comportamiento fingir. Después de todo,
si una prueba está demasiado alejada de la realidad, no le dará una idea clara de cómo se ejecutará el
código en el mundo real. Definitivamente hemos visto pruebas que se han pasado de la raya con simulacros,
stubs, espías y objetos nulos. Puede ser difícil saber dónde trazar la línea.
Para eliminar la confusión, nos gusta pensar en un conjunto de pruebas como un laboratorio para ejercitar
cuidadosamente el código. Sus dobles de prueba son sus instrumentos científicos.
Le ayudan a crear el entorno necesario para cada experimento. Los usa para controlar los factores que le
interesan y nada más.
informar fe de erratas • discutir
Machine Translated by Google
Capítulo 15. Uso efectivo de los dobles de prueba • 266
Si estuviera realizando un experimento de química, probablemente le preocuparía la temperatura y
la composición de su muestra, pero no la disposición de las sillas en el pasillo exterior. El mismo
principio se aplica a sus experimentos de software. Cuando configura el entorno de prueba para un
planificador de viajes, es posible que deba controlar la zona horaria pero no los detalles de la base
de datos de bajo nivel. Los dobles de prueba lo ayudan a controlar solo los factores que le interesan.
Hagamos estas ideas más concretas con un ejemplo: probar el proceso de registro para una
aplicación web. Como muchas de estas aplicaciones, esta requiere nuevas contraseñas para cumplir
con un estándar mínimo de seguridad. Así es como se ve nuestro validador de fuerza:
15usandopruebadoblesefectivamente/01/password_strength_validator/lib/
password_strength_validator.rb class
PasswordStrengthValidator def strong_enough?
devuelve falso a menos que contraseña.longitud >= Acme::Config.min_password_length
# ... más validaciones ...
fin
fin
Para verificar este código, podríamos escribir un par de especificaciones como las siguientes:
15usandopruebadoblesefectivamente/01/password_s … lidator/spec/
password_strength_validator_spec.rb RSpec.describe PasswordStrengthValidator do
'rechaza contraseñas de menos de 8 caracteres ' hacer
validador = PasswordStrengthValidator.new('a8E^rd2')
expect(validator.strong_enough?).to eq false end
' acepta contraseñas de 8 caracteres o más '
validador = PasswordStrengthValidator.new('a8E^rd2i')
expect(validator.strong_enough?).to eq true end end
Suponiendo que la aplicación web esté configurada para requerir contraseñas de ocho caracteres,
estos dos casos de prueba ejercerán ambos lados del condicional. Pero si luego
reconfigure la aplicación para que requiera doce caracteres (quizás para cumplir con una nueva
directriz de la empresa), una de las especificaciones comenzará a fallar:
$ rspec .F
Fallas:
1) PasswordStrengthValidator acepta contraseñas de 8 caracteres o más Falla/Error: esperar
(validador.fuerte_suficiente?)
esperado: verdadero
obtenido: falso
informar fe de erratas • discutir
Machine Translated by Google
Construcción de su entorno de prueba • 267
(comparado usando ==)
# ./spec/password_strength_validator_spec.rb:11:in ̀bloque (2 niveles) en <superior
(obligatorio)>'
Terminado en 0.01002 segundos (los archivos tardaron 0.08962 segundos en
cargarse) 2 ejemplos, 1 falla
Ejemplos fallidos:
rspec ./spec/password_strength_validator_spec.rb:9 #
PasswordStrengthValidator acepta contraseñas de 8 caracteres o más
La prueba está rota, aunque el código funciona correctamente. Lo que debería haber sido un cambio de
configuración fácil ahora requiere que dediquemos tiempo a luchar contra una prueba rota.
Desacople sus pruebas de detalles incidentales y modificables
En Test Doubles: Mocks, Stubs, and Others, en la página 67, construyó su entorno de
prueba utilizando un libro mayor falso que devolvía un estado "válido" o "no válido"
enlatado. De esa forma, podría verificar cómo su API informa los resultados, sin quedar
atrapado en las reglas de validación que probablemente cambien.
Para evitar especificaciones quebradizas, use dobles de prueba para desacoplarlas
de las reglas de validación, la configuración y otros detalles específicos de su aplicación
que cambian constantemente.
En su lugar, podemos optar por configurar los requisitos de contraseña explícitamente como parte del
entorno de prueba:
15usandopruebadoblesefectivamente/02/password_s … lidator/spec/password_strength_validator_spec.rb
RSpec.describe PasswordStrengthValidator hacer antes de
hacer
allow(Acme::Config).para recibir(:min_password_length).and_return(6) end
' rechaza contraseñas más cortas que la longitud configurada' do validator =
PasswordStrengthValidator.new('a8E^r')
expect(validator.strong_enough?).to eq false end
' acepta contraseñas que satisfacen la longitud configurada' do validator =
PasswordStrengthValidator.new('a8E^rd')
expect(validator.strong_enough?).to eq true end
fin
La especificación continuará pasando, sin importar cuántas veces modifiquemos la configuración de la
aplicación. Ya no estamos acoplados al valor actual de min_password_length.
informar fe de erratas • discutir
Machine Translated by Google
Capítulo 15. Uso efectivo de los dobles de prueba • 268
No use esperar cuando permitir es suficiente
En este ejemplo, permitimos el mensaje :min_password_length en lugar de esperarlo .
Hay un par de razones para esta elección:
• Preferimos usar expect solo cuando revela el propósito de la especificación; aquí, el
punto es verificar el valor de retorno de strong_enough?, no verificar
que consultamos una opción de configuración específica.
• Si usa expect en un enlace anterior , un mensaje no recibido significa que todos los
ejemplos en el contexto fallarán, oscureciendo su comportamiento real.
Paradójicamente, la introducción de un comportamiento falso mejoró la corrección de estas
especificaciones. Después de todo, nuestra definición de un verificador de seguridad de contraseña que
funciona correctamente no es "rechazar contraseñas de menos de ocho caracteres". Es "rechaza
contraseñas más cortas que la longitud configurada".
También podemos ver otro resultado feliz: al traer un doble de prueba, hemos hecho que las
especificaciones sean más fáciles de entender. Nuestros ejemplos de código ahora indican claramente la
conexión entre las contraseñas de muestra y la configuración.
No siempre es tan fácil construir un entorno de prueba. En la siguiente sección, veremos un ejemplo que
desdibuja la línea entre el sujeto de prueba y el entorno.
Esta incomodidad en nuestro conjunto de pruebas hará surgir un problema de diseño en nuestro código.
Stubject (Aplastar al Sujeto)
Ocasionalmente, puede ver un caso de prueba que usa permitir o esperar en el mismo objeto que está
probando. Sam Phippen se refiere a este antipatrón como el olor del código stubject , ya que estás
aplicando métodos stubject al sujeto de prueba.1
Por ejemplo, considere el siguiente código para un foro de discusión que envía a los usuarios un resumen
de lo que sucedió durante el último día:
15usandopruebadoblesefectivamente/03/daily_summary_email/lib/
daily_summary.rb clase DailySummary
def send_daily_summary(user_email, todays_messages)
message_count = todays_messages.count
thread_count = todays_messages.map { |m| m[:thread_id] }.uniq.count cuerpo del sujeto
= 'Su resumen diario de mensajes'
= "Te perdiste #{message_count} mensajes "en " \
#{thread_count} hilos hoy"
1. https://samphippen.com/introducingrspecsmells/#smell1stubject
informar fe de erratas • discutir
Machine Translated by Google
Stubject (Stubbing the Subject) • 269
entregar (correo electrónico: usuario_correo electrónico, asunto: asunto, cuerpo:
cuerpo) fin
def deliver(email:, subject:, body:) # enviar el
mensaje a través del extremo SMTP
fin
Aquí hay un ejemplo de RSpec para esta clase que verifica el contenido del correo electrónico.
No queremos enviar un correo electrónico real desde nuestras especificaciones, por lo que en las líneas
resaltadas estamos simulando el método de entrega . Nuestra versión verificará que lo estamos llamando
con el cuerpo correcto, pero en realidad no enviará un correo electrónico:
15usandopruebadoblesefectivamente/03/daily_summary_email/spec/
daily_summary_spec.rb RSpec.describe
DailySummary do let(:todays_messages) do
[
{ thread_id: 1, content: 'Hello world' }, { thread_id: 2,
content: 'Creo que los foros son geniales' }, { thread_id: 2, content: '¡Yo
también!' }
] fin
" envía un resumen de los mensajes e hilos de hoy" do summary =
DailySummary.new
esperar (resumen). recibir (: entregar). con ( hash_incluido
(cuerpo: 'Te perdiste 3 mensajes en 2 hilos hoy')
)
resumen.send_daily_summary('[email protected]', mensajes_de_hoy) end
fin
Por desgracia, esta especificación exhibe el olor del código stubject. Estamos falsificando el método de
entrega pero probando el método real send_daily_summary en el mismo objeto.
Los dobles de prueba están destinados a ayudarlo a construir un entorno de prueba para un objeto real.
Espectáculos como este desdibujan la línea entre el sujeto y su entorno. La tentación de falsificar parte
de un objeto con permitir o esperar es una señal de diseño. En otras palabras, es una pista de que un
objeto tiene dos responsabilidades y podría resultar en un mejor diseño si lo dividimos.
Primero, podemos poner la lógica SMTP en su propia clase EmailSender :
15usandopruebadoblesefectivamente/04/daily_summary_email/lib/
email_sender.rb class EmailSender
def deliver(email:, subject:, body:) # enviar el
mensaje a través del extremo SMTP
fin
informar fe de erratas • discutir
Machine Translated by Google
Capítulo 15. Uso efectivo de los dobles de prueba • 270
Luego, esta clase se puede proporcionar a DailySummary como colaborador mediante una inyección de
dependencia simple:
15usandopruebadoblesefectivamente/04/daily_summary_email/lib/
daily_summary.rb class
DailySummary def initialize(email_sender: EmailSender.new)
@email_sender = email_sender end
def send_daily_summary(user_email, todays_messages)
message_count = todays_messages.count
thread_count = todays_messages.map { |m| m[:thread_id] }.uniq.count cuerpo del sujeto
= 'Su resumen diario de mensajes'
= "Te perdiste #{message_count} mensajes "en " \
#{thread_count} hilos hoy"
@email_sender.deliver(email: user_email, asunto: asunto, cuerpo: cuerpo)
fin
fin
No estamos sugiriendo que divida las clases de esta manera solo para cumplir con la regla de "no cerrar la
asignatura". Más bien, estamos diciendo que esta guía nos ha dicho algo sobre el código. Si colocamos la
lógica de entrega de correo en su propia clase, obtenemos los siguientes beneficios:
• Podemos usar EmailSender para enviar otros correos además de los resúmenes diarios. •
Podemos probar la lógica SMTP de forma independiente, además de cualquier correo electrónico
específico. • Ahora tenemos un solo lugar para agregar otras funciones de correo electrónico, como correo electrónico
preferencias de suscripción.
Una vez que se realiza la refactorización, ya no necesitamos falsificar métodos en el propio Resumen
diario . En su lugar, podemos activar un doble de verificación para EmailSender:
15usingtestdoubleseffectly/04/daily_summary_email/spec/daily_summary_spec.rb
" envía un resumen de los mensajes e hilos de hoy" do
email_sender = instancia_doble(EmailSender)
resumen = DailySummary.new(email_sender: email_sender)
expect(email_sender).to receive(:deliver).with( hash_inclusive(body:
'Te perdiste 3 mensajes en 2 hilos hoy')
)
resumen.send_daily_summary('[email protected]', mensajes_de_hoy) end
Con este cambio, el límite entre el entorno de prueba construido (el falso EmailSender) y el objeto que
estamos probando (el DailySummary) es mucho más claro. Un olor a código en nuestras especificaciones
nos ha dado información que podemos usar para mejorar nuestro diseño.
informar fe de erratas • discutir
Machine Translated by Google
Uso eficaz de dobles parciales • 271
Además, alejarnos del doble parcial nos permite mejorar también la prueba. Mantener el flujo
Arrange/Act/Assert ayuda a mantener nuestras pruebas fáciles de seguir cuando volvemos a
ellas meses después. Convirtamos el doble en un espía para que podamos restaurar este flujo:
15usandopruebadoblesefectivamente/05/daily_summary_email/spec/
daily_summary_spec.rb " envía un resumen de los mensajes e hilos
de hoy" do email_sender = instance_spy(EmailSender)
summary = DailySummary.new(email_sender: email_sender)
resumen.send_daily_summary('[email protected]', mensajes_de_hoy)
expect(email_sender).to have_received(:deliver).with(
hash_inclusive(body: 'Te perdiste 3 mensajes en 2 hilos hoy') ) end
Si bien podríamos haber usado el doble parcial como espía, es más engorroso, porque habríamos
tenido que permitir que el mensaje de entrega lo espiara y lo apagara. Pasar a un doble puro nos
permitió usarlo como espía con poco esfuerzo.
Uso efectivo de dobles parciales
Los dobles parciales son realmente fáciles de usar: ¡simplemente apunte o espere un mensaje
en cualquier objeto! Sin embargo, dijimos en Dobles parciales, en la página 241 , que
consideramos que su uso es un olor a código. Nos gustaría desarrollar un poco esa afirmación ahora.
La mayoría de las pruebas unitarias implican una combinación de dos tipos de objetos:
• Objetos reales: normalmente el tema del ejemplo. • Objetos
falsos: dobles de prueba en colaboración que se utilizan para construir un entorno.
para el sujeto de prueba
Los dobles parciales no encajan perfectamente en esta jerarquía. Son parcialmente reales y
parcialmente falsos. ¿Son parte de lo que está probando o parte del entorno que está
construyendo? Cuando las funciones de un objeto no están claras, sus pruebas pueden ser más
difíciles de razonar.
Preferimos no mezclar estos roles en un mismo objeto. En algunos casos, puede tener algunas
consecuencias graves. Veamos un ejemplo.
Muchas aplicaciones de software como servicio (SaaS) utilizan un modelo de suscripción
mensual, donde los clientes pagan cada mes. Aquí hay una implementación típica, usando una
API de facturación hipotética llamada CashCow:
informar fe de erratas • discutir
Machine Translated by Google
Capítulo 15. Uso efectivo de los dobles de prueba • 272
15usandopruebadoblesefectivamente/06/servicio_suscripción/lib/
pago_recurrente.rb clase PagoRecurrente
def self.process_subscriptions(suscripciones) suscripciones.each
do |suscripción|
CashCow.charge_card(subscription.credit_card, subscribe.amount) # ...enviar recibo y
otras cosas... fin
fin
fin
La especificación de la unidad para esta clase verifica que estamos cobrando las cantidades correctas:
15usandopruebadoblesefectivamente/07/subscription_service/spec/
recurring_payment_spec.rb RSpec.describe RecurringPayment do
' carga la tarjeta de crédito por cada suscripción' do card_1 =
Card.new(:visa, '1234 5678 9012 3456') card_2 =
Card.new(:mastercard, '9876 5432 1098 7654')
suscripciones =
[ Subscription.new('John Doe', card_1, 19.99),
Subscription.new('Jane Doe', card_2, 29.99)
]
esperar(CashCow) .recibir(:tarjeta_de_carga).con(tarjeta_1, 19,99)
esperar(CashCow) .recibir(:tarjeta_de_carga).con(tarjeta_2, 29,99)
RecurringPayment.process_subscriptions(suscripciones) fin
fin
Aquí, estamos usando CashCow como un objeto simulado. Esperamos que reciba el
mensaje :charge_card para cada suscripción. Fundamentalmente, esta expectativa de mensaje
también sirve para bloquear el método, evitando que se produzca una carga real en nuestras pruebas.
Cientos de clientes más tarde, es posible que descubramos que nuestra clase RecurringPayment
dedica mucho tiempo a realizar una llamada a la API por separado para cada suscripción. En este
caso, podemos cambiar a la interfaz masiva de CashCow , que nos permite realizar una sola llamada
API para cargar todas las tarjetas de nuestros clientes.
Aquí hay una clase RecurringPayment actualizada que usa la llamada API masiva:
15usandopruebadoblesefectivamente/08/servicio_suscripción/lib/
pago_recurrente.rb clase PagoRecurrente
def self.process_subscriptions(suscripciones)
cards_and_amounts = suscripciones.each_with_object({}) do |sub, data|
data[sub.credit_card] = sub.cantidad final
CashCow.bulk_charge_cards(cards_and_amounts) # ...enviar
recibos y otras cosas... fin
fin
informar fe de erratas • discutir
Machine Translated by Google
Uso eficaz de dobles parciales • 273
Todavía no hemos actualizado nuestras especificaciones. Si los ejecutamos ahora, la expectativa del
mensaje fallará; el código está llamando a una API diferente a la que hemos especificado.
Sin embargo, tenemos un problema mayor. Nuestras especificaciones anteriores no excluyen el
método bulk_charge_cards . Terminaremos enviando una solicitud de cargo real , ¡algo que no
queremos hacer desde una especificación de unidad!
El doble parcial facilitó que nuestro código realizara una operación costosa de verdad, aunque el
objetivo del doble de prueba era evitar este problema.
Podemos evitar fácilmente este problema usando un doble de prueba puro o de verificación en su
lugar. Para hacerlo, agregaremos un nuevo argumento a process_subscriptions que nos permita
inyectar un objeto de facturación que no sea CashCow:
15usandopruebadoblesefectivamente/09/servicio_suscripción/lib/
pago_recurrente.rb clase
PagoRecurrente def self.process_subscriptions(suscripciones, banco: CashCow)
suscripciones.cada uno hace |suscripción|
bank.charge_card(subscription.credit_card, subscribe.amount) # ...enviar recibo
y otras cosas... fin
fin
fin
Ahora, en lugar de aplicar métodos auxiliares en la clase CashCow real , nuestras especificaciones
pueden simplemente crear un doble de verificación y pasarlo en su lugar:
15usandopruebadoblesefectivamente/09/subscription_service/spec/
recurring_payment_spec.rb RSpec.describe RecurringPayment do
' carga la tarjeta de crédito por cada suscripción '
suscripciones =
[ Subscription.new('John Doe', card_1, 19.99),
Subscription.new('Jane Doe', card_2, 29.99)
]
banco = class_double(CashCow)
esperar(banco).recibir (:tarjeta_de_cargo).con(tarjeta_1, 19.99)
esperar(banco) .recibir(:tarjeta_de_cargo).con(tarjeta_2, 29.99)
RecurringPayment.process_subscriptions(suscripciones, banco: banco) end
fin
Este tipo de sustitución solo es factible si desea un objeto completamente falso.
Si necesita mezclar comportamiento real y falso en el mismo objeto, considere dividirlo en varios
objetos, como hicimos en la sección anterior.
informar fe de erratas • discutir
Machine Translated by Google
Capítulo 15. Uso efectivo de los dobles de prueba • 274
Dicho esto, romper objetos no siempre mejorará su diseño. En Construcción de su
entorno de prueba, en la página 265, el objeto Acme::Config tenía un trabajo simple.
Dividirlo no lo habría ayudado a hacer mejor su trabajo, por lo que, en su lugar,
descartamos un método de configuración para nuestras especificaciones.
Use los dobles parciales con cuidado
Nuestro consejo para usted no es "evitar los dobles parciales", sino
"escuchar los comentarios que le dan sus dobles y conocer los riesgos".
Te recomendamos que configures tus proyectos con una verificación de seguridad
adicional para dobles parciales, en caso de que termines usándolos. La opción se llama
2
verificar_partial_dobles:
15usandopruebadoblesefectivamente/09/subscription_service/spec/
spec_helper.rb RSpec.configure
do |config| config.mock_with :rspec do |simulacros|
mocks.verify_partial_doubles = final verdadero
fin
Esta opción aplicará las mismas comprobaciones que utiliza RSpec para verificar dobles
puros, lo que proporciona un poco de seguridad adicional. RSpec lo establecerá por
usted en su spec_helper.rb si usa rspec init para iniciar su proyecto.
Si bien no habría evitado el problema de la tarjeta de crédito que encontramos en esta
sección, la verificación al menos lo protegerá de la expectativa de un mensaje mal
escrito o desactualizado. Si accidentalmente agrega un método con el nombre incorrecto
o la cantidad incorrecta de argumentos, RSpec detectará esta situación e informará una
falla.
En esta sección, proporcionamos el doble de prueba CashCow pasándolo como un
parámetro, una forma de inyección de dependencia. Esta técnica es una de las muchas
formas diferentes de conectar cada sujeto de prueba con su entorno. A continuación,
exploraremos algunas de estas opciones.
Conexión del sujeto de prueba a su entorno
Cuando construye su entorno de prueba, también necesita conectarlo a su sujeto de
prueba. En otras palabras, debe hacer que sus dobles estén disponibles para el código
que está probando. Hay varias formas de hacerlo, incluidas las siguientes:
• Comportamiento de creación de apéndices en cada instancia de una clase. •
Métodos de fábrica de creación de apéndices.
2. https://relishapp.com/rspec/rspecmocks/v/36/docs/verifyingdoubles/partialdoubles
informar fe de erratas • discutir
Machine Translated by Google
Conexión del sujeto de prueba a su entorno • 275
• Tratar una clase como un doble parcial • Usar las
constantes auxiliares de RSpec • Inyección de
dependencia, en sus múltiples formas
Cada uno de estos enfoques tiene sus ventajas y desventajas. A medida que los analicemos, trabajaremos
con el mismo ejemplo simple: una clase APIRequestTracker que ayuda a los desarrolladores de API a
rastrear estadísticas de uso simples para cada punto final. Este tipo de información es útil para averiguar
con qué funciones los clientes interactúan más.
Por ejemplo, en el registro de gastos que creó en Creación de una aplicación con RSpec 3, es posible
que desee contar las siguientes estadísticas:
• Con qué frecuencia los clientes ENVÍAN a /gastos, rastreados como post_gastos •
Con qué frecuencia los clientes OBTENEN de /gastos/:fecha, rastreados como obtener_gastos_en_fecha
Esta es una forma de implementar APIRequestTracker:
15usandopruebadoblesefectivamente/10/api_request_tracker/lib/
api_request_tracker.rb class
APIRequestTracker def
proceso(solicitud) endpoint_description = Endpoint.description_of(solicitud)
reporter = MetricsReporter.new
reporter.increment("api.requests.#{ endpoint_description}") fin
fin
Primero, obtenemos una descripción del punto final (basado en la ruta y si fue un GET o POST) para usar
con fines de seguimiento. A continuación, creamos una nueva instancia de MetricsReporter, que enviará
estadísticas a un servicio de métricas.
Finalmente, le decimos al reportero que incremente el conteo de llamadas para este punto final de la API.
En nuestro ejemplo de seguimiento de gastos, aumentaríamos la métrica api.requests.post_expense o
api.requests.get_expenses_on_date .
El colaborador de MetricsReporter es un buen candidato para reemplazarlo con un objeto simulado en
nuestra prueba. Nos gustaría ejecutar una especificación de unidad sin necesidad de una conexión de
red y una cuenta de prueba en un servicio de métricas en vivo.
Sin embargo, tenemos mucho trabajo por delante si queremos usar un doble de prueba con esta clase.
Crea una instancia del objeto reportero a partir de un nombre de clase codificado de forma rígida, sin que
una prueba controle fácilmente qué reportero se utiliza.
Esperar un mensaje en cualquier instancia RSpec puede
sacarnos de situaciones difíciles como esta, a través de su función de cualquier instancia . En lugar de
permitir o esperar un mensaje en una instancia específica de una clase, puede hacerlo en cualquiera de
sus instancias:
informar fe de erratas • discutir
Machine Translated by Google
Capítulo 15. Uso efectivo de los dobles de prueba • 276
15usandopruebadoblesefectivamente/10/api_request_tracker/spec/
api_request_tracker_spec.rb RSpec.describe
APIRequestTracker do let(:request) { Request.new(:get, '/users') }
' incrementa el contador de solicitudes'
espera_cualquier_instancia_de (MetricsReporter).para recibir(:incrementar).con(
'api.requests.get_users'
)
APIRequestTracker.nuevo.proceso (solicitud) fin
fin
Aquí llamamos a expect_any_instance_of en lugar de expect simple, con la clase como
argumento. (Del mismo modo, allow tiene una contraparte allow_any_instance_of .) Esta
técnica nos ayuda a probar nuestra clase. Pero definitivamente tiene inconvenientes significativos.
Primero, esta herramienta es un martillo muy desafilado. No tiene un control detallado sobre
cómo se comportan las instancias individuales. Cada instancia de la clase nombrada (y sus
subclases) se verá afectada, incluidas las que quizás no conozca dentro de bibliotecas de
terceros. Volviendo a nuestra metáfora del laboratorio, cuando pones a hervir una solución
para un experimento, ¡probablemente no quieras hervir todo el líquido del edificio!
En segundo lugar, hay muchos casos extremos. Podría preguntarse, por ejemplo, si
esperar_cualquier_instancia_de(MetricsReporter).para recibir(:incrementar).dos veces
significa que una instancia debe recibir ambas llamadas para incrementar, o si dos instancias
diferentes pueden recibir cada una una llamada. La respuesta es la primera, pero los
lectores futuros pueden asumir la última y malinterpretar su especificación. La creación de
subclases es otra situación en la que es fácil confundirse, en particular cuando la subclase
anula un método que ha bloqueado.
Finalmente, esta técnica tiende a calcificar los diseños subóptimos existentes, en lugar de
ayudarlo a mejorar el diseño de su código.
Crear un método de fábrica
Una técnica de menor alcance es crear un método de fábrica, generalmente SomeClass.new,
para devolver un doble de prueba en lugar de una instancia normal. Así es como se ve para
nuestra prueba APIRequestTracker :
15usandopruebadoblesefectivamente/11/api_request_tracker/spec/
api_request_tracker_spec.rb ' incrementa el
contador de solicitudes' do reporter =
instance_double(MetricsReporter) allow(MetricsReporter).to
receive(:new).and_return(reporter ) esperar (reportero). recibir (: incremento). con ('api.requests.get_users')
APIRequestTracker.nuevo.proceso (solicitud) fin
informar fe de erratas • discutir
Machine Translated by Google
Conexión del sujeto de prueba a su entorno • 277
Esta versión nos da resultados similares a expect_any_instance_of, pero nos salva de algunos de sus
inconvenientes. Hemos reducido el alcance a instancias recién creadas de MetricsReporter (sin incluir
ninguna subclase). También podemos usar un doble puro ahora, en lugar de uno parcial.
Además, este código de prueba es más honesto. APIRequestTracker obtiene su instancia de
MetricsReporter a través del nuevo método y nuestra especificación hace explícita esa dependencia .
La prueba es incómoda porque la interacción en nuestro código es incómoda.
Nuestra clase instancia un objeto solo para llamar a un método, luego lo descarta.
Usar la clase como un doble parcial
Echemos un vistazo más de cerca a esa instancia de reportero de corta duración . No lo estamos
usando para rastrear ningún estado. Podríamos evitar fácilmente la creación de una instancia
promoviendo el método de incremento para que sea un método de clase en la clase MetricsReporter .
Una vez que hayamos actualizado la clase MetricsReporter , podemos simplificar nuestro APIRequestTracker:
15usandopruebadoblesefectivamente/12/api_request_tracker/lib/
api_request_tracker.rb class
APIRequestTracker def
proceso(solicitud) endpoint_description = Endpoint.description_of(solicitud)
MetricsReporter.increment("api.requests.#{endpoint_description}")
fin
fin
Defina los métodos de clase con
cuidado No todos los métodos son buenos candidatos para pasar a un método de clase.
En particular, si necesita trasladar el estado de una llamada a la siguiente, necesitará
un método de instancia.
Si su método no tiene ningún efecto secundario, o al menos no utiliza ningún estado
interno, puede convertirlo de forma segura en un método de clase.
Aquí, nuestro método de incremento hace una llamada a un servicio externo, pero eso
es todo lo que hace. Está bien hacer de esto un método de clase, y al hacerlo se podría
decir que mejorará la interfaz para las personas que llaman.
Ahora que la interfaz de MetricsReporter no requiere que creemos una instancia desechable, podemos
usar la clase como un doble parcial:
15usingtestdoubleseffectly/12/api_request_tracker/spec/api_request_tracker_spec.rb
' incrementa el contador de solicitudes '
esperar(MetricsReporter).para recibir(:incrementar).con(
'api.requests.get_users' )
APIRequestTracker.nuevo.proceso (solicitud) fin
informar fe de erratas • discutir
Machine Translated by Google
Capítulo 15. Uso efectivo de los dobles de prueba • 278
Con esta versión de nuestra prueba, ya no necesitamos lidiar con las instancias de
MetricsReporter . En cambio, tenemos una interfaz más simple para incrementar las métricas.
Potencialmente, podríamos limpiar el código de conteo de métricas en todo nuestro sistema.
Sin embargo, volvemos a usar dobles parciales, algo que generalmente evitamos.
Rellenar una constante
Como hemos visto en este capítulo, tener un comportamiento real y falso en el mismo objeto
puede causar problemas. Preferiríamos usar un reportero puramente falso. Podemos lograr
este objetivo creando un class_double y agregando la constante MetricsReporter para
devolver ese doble:
15usandopruebadoblesefectivamente/13/api_request_tracker/spec/
api_request_tracker_spec.rb ' incrementa el
contador de solicitudes' do reporter =
class_double(MetricsReporter)
stub_const('MetricsReporter', reporter) expect(reporter).to recibir(:incremento).with('api.requests.get_users')
APIRequestTracker.nuevo.proceso (solicitud) fin
Este patrón es lo suficientemente útil como para que RSpec proporcione una forma de
implementarlo en una línea de código. Simplemente agregue as_stubbed_const al final de su
doble de clase y automáticamente reemplazará la clase original, solo durante la duración del
ejemplo:
15usingtestdoubleseffectly/14/api_request_tracker/spec/api_request_tracker_spec.rb
' incrementa el contador de solicitudes' do
reporter = class_double(MetricsReporter).as_stubbed_const
esperar (reportero). recibir (: incremento). con ('api.requests.get_users')
APIRequestTracker.nuevo.proceso (solicitud) fin
Nos gusta la forma en que las constantes auxiliares nos permiten usar un doble puro. La
desventaja es que agregan implícitamente su comportamiento falso. Alguien que lea el código
APIRequestTracker no sospechará que la constante MetricsReporter podría hacer referencia
a un doble de prueba.
Las constantes añadidas pueden revelar dependencias ocultas en nuestro código. Cuando
codificamos el nombre de una clase, estamos acoplando estrechamente nuestro código a esa
clase específica. Sandi Metz analiza formas de lidiar con este antipatrón en Diseño práctico
orientado a objetos en Ruby [Met12].
Para evitar este estrecho acoplamiento, preferimos depender de roles abstractos en lugar de
clases concretas. Nos gustaría que este rastreador de solicitudes de API funcione con
cualquier objeto que pueda informar métricas (o pretender informar métricas), no solo las
instancias de MetricsReporter .
informar fe de erratas • discutir
Machine Translated by Google
Conexión del sujeto de prueba a su entorno • 279
Steve Freeman, Nat Pryce y sus coautores se refieren a esto como "Roles simulados, no objetos"
en su artículo del mismo nombre.3 Steve y Nat exploran ideas relacionadas en su libro, Growing
ObjectOriented Software, Guided by Tests [FP09].
Para obtener más información sobre la importancia de simular roles en lugar de objetos, consulte
4
la charla RubyConf 2011 de Gregory Moeck, "Por qué no obtiene objetos simulados".
Inyección de dependencia
Para refactorizar la clase para que dependa de roles abstractos, podemos usar la inyección de
dependencia. Encontramos este concepto por primera vez en Conexión al almacenamiento, en la
página 65. La técnica puede tomar algunas formas diferentes, la más simple de las cuales es la
inyección de argumentos:
15usandopruebadoblesefectivamente/15/api_request_tracker/lib/
api_request_tracker.rb clase
APIRequestTracker def proceso(solicitud, reportero: MetricsReporter)
endpoint_description = Endpoint.description_of(solicitud)
reporter.increment("api.requests.#{endpoint_description}")
fin
fin
Ahora que el método de proceso acepta un argumento de reportero adicional , nuestra prueba
puede inyectar fácilmente un doble:
15usandopruebadoblesefectivamente/15/api_request_tracker/spec/
api_request_tracker_spec.rb ' incrementa el
contador de solicitudes' do reporter =
class_double(MetricsReporter) expect(reporter).to receive(:increment).with('api .requests.get_users')
APIRequestTracker.new.process(solicitud, reportero: reportero)
fin
Esta técnica es simple y versátil. El principal inconveniente es la repetición. Si necesita usar el
mismo colaborador de varios métodos, agregar el mismo parámetro adicional a todos ellos puede
ser engorroso. En su lugar, puede usar la inyección de constructor para pasar a su colaborador
como parte del estado inicial del objeto:
15usandopruebadoblesefectivamente/16/api_request_tracker/lib/
api_request_tracker.rb clase APIRequestTracker
def initialize(reporter: MetricsReporter.new) @reporter =
reporter end
3. http://www.jmock.org/oopsla2004.pdf
4. https://www.youtube.com/watch?v=R9FOchgTtLM
informar fe de erratas • discutir
Machine Translated by Google
Capítulo 15. Uso efectivo de los dobles de prueba • 280
def proceso(solicitud)
endpoint_description = Endpoint.description_of(solicitud)
@reporter.increment("api.requests.#{endpoint_description}")
final
final
Ahora, solo tenemos que pasar el reportero colaborador cuando creamos un
Instancia de APIRequestTracker , en lugar de pasarla como un argumento para procesar:
15usingtestdoubleseffectly/16/api_request_tracker/spec/api_request_tracker_spec.rb
' incrementa el contador de solicitudes '
reporter = class_double(MetricsReporter)
expect(reporter).para recibir(:incremento).with('api.requests.get_users')
APIRequestTracker.new(reportero: reportero).proceso(solicitud)
fin
La inyección de constructores es nuestra técnica de referencia para proporcionar dobles de prueba a
nuestro código. Es simple y explícito, y documenta muy bien de qué colaboradores depende una
clase en el constructor. Debemos agregar que este estilo no es del agrado de todos. Para obtener
una mirada matizada a las ventajas y desventajas de la inyección de dependencia, consulte la
publicación de blog de Tom Stuart "Cómo puede ayudar la capacidad de prueba".5
A veces, el constructor no está disponible para que lo modifiquemos. Por ejemplo, los marcos web
como Ruby on Rails a menudo controlan la duración de los objetos, incluidos los argumentos de los
constructores. En estas situaciones, podemos recurrir a la inyección de setter:
15usandopruebadoblesefectivamente/17/api_request_tracker/lib/
api_request_tracker.rb class
APIRequestTracker
attr_writer :reporter
def reporter @reporter ||= MetricsReporter.new
end
def proceso(solicitud)
endpoint_description = Endpoint.description_of(solicitud)
reporter.increment("api.requests.#{endpoint_description}") end end
Aquí hemos expuesto un setter (a través de attr_writer) para nuestro colaborador que se puede usar
desde nuestra prueba para inyectar la dependencia:
15usingtestdoubleseffectly/17/api_request_tracker/spec/api_request_tracker_spec.rb
' incrementa el contador de solicitudes '
reporter = class_double(MetricsReporter)
expect(reporter).para recibir(:incremento).with('api.requests.get_users')
5. http://codon.com/howtestabilitycanhelp
informar fe de erratas • discutir
Machine Translated by Google
Los riesgos de burlarse del código de terceros • 281
tracker = APIRequestTracker.new
tracker.reporter = reporter
tracker.process(request)
fin
Cada una de estas técnicas tiene sus usos. La inyección de dependencia es la técnica más común
que usamos, y le recomendamos que también la prefiera. Si desea utilizar una biblioteca para esta
tarea, eche un vistazo al proyecto dryauto_inject.6
A veces, la inyección de dependencia no es práctica. Cuando está probando un grupo de objetos
juntos, es posible que no tenga acceso directo al objeto donde le gustaría inyectar una dependencia.
En su lugar, puede usar un método de fábrica o una constante.
Tenga cuidado, sin embargo, con la tentación de tratar cada situación difícil como un rompecabezas
para resolver utilizando funciones RSpec cada vez más potentes. Cuando el tiempo lo permita,
obtendrá mejores resultados al refactorizar su código para que sea más fácil de probar con técnicas
simples.
Por supuesto, la refactorización tiene costos, que pueden o no valer la pena pagar por su proyecto.
Si refactoriza, es probable que primero desee probar su código. Buscamos herramientas
contundentes como la función "cualquier instancia" de RSpec en momentos como estos, pero
preferimos usarlas temporalmente. Una vez que hayamos limpiado el código, podemos soltar las
muletas y cambiar a la inyección de dependencia.
En estos ejemplos, nos hemos estado burlando de MetricsReporter, una clase que nos pertenece.
Al hacerlo, hemos estado siguiendo el consejo de prueba común: "Solo simula los tipos que posees".
En la siguiente sección, veremos por qué esa advertencia es tan importante.
Los riesgos de burlarse del código de terceros
Los dobles de prueba son excelentes para proporcionar versiones falsas de sus API. No solo le
permiten probar a las personas que llaman de forma aislada, sino que también brindan comentarios
sobre el diseño de su API.
Cuando intenta falsificar la API de otra persona, se pierde los beneficios de diseño de usar dobles
de prueba. Además, incurre en riesgos adicionales cuando se burla de una interfaz que no es de su
propiedad. Específicamente, puede terminar con pruebas que fallan cuando no deberían, o peor
aún, pasan cuando no deberían.
Un ejemplo aclarará estos riesgos. La siguiente clase TwitterUserFormatter crea una cadena simple
que describe a un usuario del servicio. Las personas que llamen obtendrán una instancia de
Twitter::User de la gema de Twitter (por ejemplo, mediante una búsqueda) y se la pasarán a nuestro
formateador:7
6. http://dryrb.org/gems/dryauto_inject/
7. https://github.com/sferik/twitter
informar fe de erratas • discutir
Machine Translated by Google
Capítulo 15. Uso eficaz de los dobles de prueba • 282
15usandopruebadoblesefectivamente/18/twitter_user_formatter/lib/twitter_user_formatter.rb
class TwitterUserFormatter
def inicializar (usuario)
@usuario =
usuario final
formato de definición
@user.name + "el sitio web es" end end + @usuario.url
Los métodos name y url provienen de una instancia de la clase Twitter::User .
La construcción de un usuario real requiere múltiples pasos y diferentes colaboradores.
Puede ser tentador proporcionar una implementación falsa en su lugar, así:
15usandopruebadoblesefectivamente/18/twitter_user_formatter/spec/twitter_user_formatter_spec.rb
RSpec.describe TwitterUserFormatter do it 'describe
su página de inicio' do
usuario = instancia_doble(Twitter::Usuario, nombre:
'RSpec', url: 'http://
rspec.info')
formateador = TwitterUserFormatter.nuevo (usuario)
expect(formatter.format).to eq("El sitio web de RSpec es http://rspec.info") end
fin
Este código funcionaría bien en versiones anteriores de la gema de Twitter. Sin embargo, a partir
de la versión 5.0, el método Twitter::User#url devuelve un objeto URI en lugar de una cadena
simple.
Si tuviéramos que actualizar a la última versión de la gema y ejecutar esta especificación, aún
pasaría. Nuestro código espera una cadena, y eso es lo que le da nuestro método de URL falso .
Sin embargo, una vez que intentamos usar nuestro TwitterUserFormatter en producción,
comenzamos a ver excepciones. Específicamente, cuando tratamos de concatenar la URL del
usuario (que ahora es una instancia de URI ) en la cadena de descripción:
@user.name + "el sitio web es " + @usuario.url
…obtendríamos un TypeError con el mensaje sin conversión implícita de URI::HTTPS en String.
Este es el escenario de pesadilla para las pruebas, donde las especificaciones pueden dar una
falsa confianza. Nuestros dobles de prueba fueron diseñados para imitar una interfaz que no
está bajo nuestro control, y esa interfaz se movió debajo de nosotros. (Los mantenedores de
gemas de Twitter son, de hecho, cuidadosos con las obsolescencias, pero como desarrolladores
no siempre recordamos leer las notas de la versión).
Una forma de reducir este riesgo es usar dobles verificadores, como lo hicimos en este ejemplo
con un doble_instancia. Estos detectarán cuando una clase o método se vuelve
informar fe de erratas • discutir
Machine Translated by Google
Falsificaciones de alta fidelidad • 283
renombrado, o cuando un método gana o pierde argumentos. Pero en este caso, el tipo de devolución
de un método cambió y la verificación de dobles no puede detectar ese tipo de incompatibilidad en
absoluto.
Puede renunciar a que las especificaciones de su unidad detecten este tipo de cambio importante y
depender de sus especificaciones de aceptación de principio a fin. Estos utilizarán dependencias
reales tanto como sea posible y es más probable que fallen por el uso incorrecto de la API.
Sin embargo, recurrir a las especificaciones de aceptación no es la única opción. Tiene un par de
opciones para hacer que las especificaciones de su unidad sean más sólidas:
• Use una falsificación de alta fidelidad para la API de terceros, si hay una disponible. •
Escriba su propio envoltorio alrededor de la API y use un doble de prueba en lugar de su envoltorio.
En las próximas dos secciones, veremos estos dos enfoques.
Falsificaciones de alta fidelidad
Cuando trabaja con rspecmocks, está utilizando un comportamiento falso proporcionado por RSpec.
Sin embargo, hay otros tipos de dobles de prueba. Puede utilizar una implementación alternativa
diseñada para incluirse en sus pruebas en lugar de la aplicación real. Debido a que estos imitan de
cerca la interfaz y el comportamiento de la biblioteca original , los llamamos falsificaciones de alta
fidelidad.
Por ejemplo, la gema FakeFS facilita la prueba del código que interactúa con el sistema de archivos.8
FakeRedis actúa como el almacén de datos de Redis, pero no requiere una conexión de red ni un
servidor en ejecución.9 Si usa Braintree para el procesamiento de tarjetas de crédito , Fake Braintree
puede intervenir durante la prueba.10
A veces, una biblioteca se envía con su propia falsificación de alta fidelidad. La gema Fog, que
envuelve servicios en la nube como los de Amazon, viene con un modo simulado incorporado.11 Si
está desarrollando una biblioteca que llama a servicios externos, sus usuarios estarán agradecidos
si proporciona una falsificación de alta fidelidad. Puede escribir un conjunto de especificaciones para
verificar las implementaciones reales y falsas utilizando los ejemplos compartidos de RSpec.
Discutimos esto en Compartir ejemplos, en la página 124.
Las falsificaciones son particularmente útiles para las API de HTTP. Por desgracia, la mayoría de los
clientes de API no incluyen una falsificación de alta fidelidad. Afortunadamente, puede armar uno
con bastante rapidez utilizando la gema VCR (escrita por Myron, uno de los coautores de este libro).12
8. https://github.com/fakefs/fakefs
9. http://guilleiguaran.github.io/fakeredis/
10. https://github.com/highfidelity/fake_braintree
11. http://www.rubydoc. info/gems/fog/1.40.0#Mocks
12. https://relishapp.com/vcr/vcr/v/303/docs
informar fe de erratas • discutir
Machine Translated by Google
Capítulo 15. Uso efectivo de los dobles de prueba • 284
Cuando utilice VCR por primera vez en sus pruebas, registrará las solicitudes de red a la
API real junto con sus respuestas. Las ejecuciones de prueba posteriores utilizarán los
datos registrados en lugar de realizar llamadas API reales.
En la siguiente sección, nos gustaría mostrarle cómo es probar con una falsificación de
alta fidelidad, específicamente, la clase StringIO que se envía con Ruby. Con él, puede
simular E/S a la consola, un archivo de disco, la red, tuberías y más.
Falsificación de E/S con StringIO
Mucho antes de que apareciera Ruby on Rails, las primeras aplicaciones web eran
simples scripts de línea de comandos que escribían su contenido en la consola. Esta
arquitectura de interfaz de puerta de enlace común (CGI) hizo posible la creación de
sitios web dinámicos en casi cualquier idioma.13 Todo lo que tenía que hacer era leer la
entrada de las variables de entorno y escribir la página web resultante en la salida estándar.
Aquí hay un script CGI que funciona como un pequeño y simple servidor de
documentación de Ruby. Si conectara este código a un servidor web local y visitara
http://localhost/String/each, devolvería una matriz JSON de todos los métodos String
que comienzan con cada uno: ["each_byte", "each_char", ...].
15usandopruebadoblesefectivamente/19/ruby_doc_server/lib/
ruby_doc_server.rb requiere 'json'
class RubyDocServer def
initialize(salida: $stdout)
@salida = final de
salida
def process_request(ruta)
nombre_clase, prefijo_método = ruta.sub(%r{^/}, '').split('/') klass =
Object.const_get(nombre_clase) métodos =
klass.métodos_instancia.grep(/\A#{prefijo_método}/). ordenar respond_with (métodos)
final
privado
def responder_con(datos)
@output.puts 'ContentType: application/json' @output.puts
@output.puts
JSON.generate(data) end
fin
si __FILE__.end_with?($PROGRAM_NAME)
RubyDocServer.new.process_request(ENV['PATH_INFO']) end
13. https://en.wikipedia.org/wiki/Common_Gateway_Interface
informar fe de erratas • discutir
Machine Translated by Google
Simulación de E/S con StringIO • 285
El servidor web coloca cualquier ruta que visite, como /String/each, en la variable de entorno
PATH_INFO . Dividimos este texto en la clase String y cada prefijo, obtenemos una lista de los
métodos de instancia que pertenecen a String y finalmente los reducimos a los que comienzan con
cada uno.
Ya aplicamos las lecciones anteriores en este capítulo e inyectamos el colaborador de salida a través
de la inyección del constructor. Podría ser tentador pasar un espía de nuestras pruebas para que
podamos verificar que el script CGI estaba escribiendo los resultados correctos:
15usandopruebadoblesefectivamente/19/ruby_doc_server/spec/
ruby_doc_server_spec.rb requiere 'ruby_doc_server'
RSpec.describe RubyDocServer hacer
' encuentra métodos Ruby coincidentes' do
fuera = get('/Array/max')
esperar (fuera). tener_recibido (: pone). con ('Tipo de contenido: aplicación/json') esperar (fuera).
tener_ recibido (: pone). con ('["max","max_by"]') fin
def get(ruta)
salida = object_spy($stdout)
RubyDocServer.new(salida: salida).process_request(ruta) final de salida
fin
Desafortunadamente, esta especificación es bastante frágil. Está acoplado no solo al contenido de la
respuesta web, sino también a cómo se escribe exactamente.
La interfaz IO de Ruby es grande. Proporciona varios métodos solo para escribir resultados: puts,
print, write y más. Si refactorizamos nuestra implementación para llamar a escribir, o incluso llamar
a puts solo una vez con la respuesta completa, nuestras especificaciones se romperán. Recuerde
que uno de los objetivos de TDD es admitir la refactorización. En cambio, estas especificaciones se
interpondrán en nuestro camino.
Los dobles de prueba son mejores para interfaces pequeñas,
simples y estables Las interfaces grandes no son las únicas que son difíciles de
reemplazar con un doble. También vemos problemas en los siguientes casos:
• Interfaces complejas: cuanto más compleja es la interfaz, más
más difícil es imitar con precisión; nos gusta usar dobles de prueba para dirigir
nuestro diseño hacia la simplicidad.
• Interfaces inestables: cada mensaje que espera o permite es un detalle al que se
acoplan sus especificaciones; cuanto más cambie la interfaz, más tendrá que
actualizar sus dobles.
informar fe de erratas • discutir
Machine Translated by Google
Capítulo 15. Uso efectivo de los dobles de prueba • 286
En lugar de esperar llamadas de método IO específicas , podemos usar la falsificación de alta
fidelidad StringIO de la biblioteca estándar de Ruby. Los objetos StringIO existen en la memoria,
pero actúan como cualquier otro objeto Ruby IO , como un archivo abierto o una tubería Unix.
Puede probar el código de manejo de entrada inicializando un StringIO con datos y dejando que
su código se lea. O puede probar su código de salida dejando que su código escriba en un
StringIO y luego inspeccionando el contenido a través de su método de cadena . Así es como se
ve esta prueba con un objeto StringIO inyectado en RubyDocServer:
15usandopruebadoblesefectivamente/20/ruby_doc_server/spec/
ruby_doc_server_spec.rb
requiere 'ruby_doc_server' requiere 'stringio'
RSpec.describe RubyDocServer hacer
' encuentra métodos ruby coincidentes' do result
= get('/Array/min')
expect(result.split("\n")).to eq [ 'ContentType:
application/json', '',
'["min","min_by","minmax","minmax_by"]'
] fin
def get(ruta)
salida = StringIO.new
RubyDocServer.new(salida: salida).process_request(ruta) salida.string end
fin
Ahora, estamos estableciendo expectativas sobre el contenido de la respuesta, en lugar de cómo
se produjo. Esta práctica da como resultado especificaciones mucho menos frágiles.
Envolviendo una dependencia de terceros
Si bien las falsificaciones de alta fidelidad pueden evitar muchos problemas, aún pueden dejar su
lógica acoplada a una API de terceros. Este acoplamiento tiene algunas desventajas:
• Su código estará expuesto a las complejidades de una API (potencialmente) grande. • Los
cambios en la interfaz de la dependencia se reflejarán en todo el sistema. • La prueba es difícil,
ya que no se puede reemplazar de manera fácil y segura la tercera
código de fiesta con un doble.
Para evitar estos peligros, puede envolver la dependencia, es decir, escribir su propia capa que
la delegue internamente. El uso de un contenedor (también conocido como puerta de enlace o
adaptador cuando está empaquetando una API) le brinda un par de ventajas clave:
• Puede crear una interfaz pequeña y sencilla adaptada exactamente a sus necesidades. •
Puede reemplazar con seguridad su envoltorio con un doble puro en sus especificaciones.
informar fe de erratas • discutir
Machine Translated by Google
Envolviendo una dependencia de terceros • 287
• Tiene un lugar donde puede agregar fácilmente funciones relacionadas, como
almacenamiento en caché, seguimiento de métricas o registro.
Aunque envolver una dependencia minimiza el riesgo de que su interfaz cambie por debajo de ti, este
riesgo sigue presente. Para abordarlo, puede escribir una pequeña cantidad de especificaciones de
integración que prueben su contenedor con la biblioteca o el servicio real.
En el siguiente ejemplo, estamos probando una clase Invoice que usa el cliente de la API TaxJar Ruby
para calcular el impuesto sobre las ventas para un sitio de compras.14 La versión inicial llama
directamente a TaxJar y luego refactorizaremos nuestra clase para usar un contenedor. Aquí está la
interfaz pública de la clase:
15usandopruebadoblesefectivamente/21/impuestos_de_ventas/lib/
factura.rb clase Factura
def initialize(dirección, artículos, tax_client: MyApp.tax_client) @address = dirección
@items = items
@tax_client =
tax_client end
def calcular_total
subtotal = @items.map(&:cost).inject(0, :+) impuestos =
subtotal * tax_rate
subtotal + impuestos
fin
#...
fin
Una nueva Factura necesita una dirección de envío y una lista de artículos. Para respaldar las pruebas,
usamos la inyección de dependencia para pasar una implementación real o falsa de TaxJar al
inicializador. El método de cálculo_total tabula el costo total de los artículos en el carrito y luego aplica
la tasa de impuestos.
La búsqueda de tasas impositivas parece simple en la superficie; una vez que sepamos el código postal
al que estamos enviando, deberíamos poder obtener la tarifa correcta de TaxJar. Sin embargo, la lógica
para hacerlo es un poco complicada:
15usandopruebadoblesefectivamente/22/impuesto_ventas/lib/
factura.rb def
tasa_impuesto @cliente_impuesto.tasas_para_ubicación(@dirección.zip).tasa_combinada
final
El cliente de TaxJar requiere más de nosotros que la invocación de un solo método. Primero, llamamos
a rates_for_location para obtener un objeto que contenga todas las tasas de impuestos aplicables
(ciudad, estado, etc.). Luego, buscamos su tasa_combinada para obtener el impuesto total sobre las ventas.
14. https://github.com/taxjar/taxjarruby
informar fe de erratas • discutir
Machine Translated by Google
Capítulo 15. Uso efectivo de los dobles de prueba • 288
Esta interfaz proporciona mucha flexibilidad, pero no la necesitamos mucho en nuestra
clase Factura . Sería bueno tener una API más simple que se ajuste a las necesidades de
nuestro proyecto.
Mira lo que sucede cuando tratamos de probar esta clase. Terminamos creando un doble
de prueba, tax_client, que devuelve un segundo doble, tax_rate:
15usandopruebadoblesefectivamente/22/impuesto_ventas/spec/unidad/
especificación_factura.rb requiere 'factura'
RSpec.describe Invoice do
let(:address) { Address.new(zip: '90210') } let(:items)
{ [Item.new(cost: 30), Item.new(cost: 70)] }
' calcula el total' hacer
tasa_impuesto = doble_instancia(Taxjar::Tasa, tasa_combinada: 0,095) cliente_impuesto =
doble_instancia(Taxjar::Cliente, tasas_para_ubicación: tasa_impuesto)
factura = Factura.nueva(dirección, artículos, tax_client: tax_client)
esperar(factura.calcular_total).to eq(109.50) fin
fin
Nuestra compleja estructura de simulación (un doble que devuelve un doble) ha revelado
un par de problemas con nuestro proyecto:
• La clase Factura está estrechamente relacionada con la jerarquía de objetos del cliente
TaxJar. • La complejidad dificultará la refactorización y el mantenimiento.
En su publicación de blog, "El aislamiento de pruebas se trata de evitar simulacros", Gary
Bernhardt recomienda que desenredemos las relaciones de objetos que se vuelven obvias
por este tipo de dobles anidados.15 Podemos seguir su consejo escribiendo un contenedor
de impuestos de ventas que separe la factura de TaxJar :
15usandopruebadoblesefectivamente/23/impuesto_ventas/lib/
impuesto_ventas.rb requiere 'mi_aplicación'
clase Impuesto sobre las ventas
RateUndisponibleError = Class.new(StandardError)
def inicializar (cliente_impuestos = MiAplicación.cliente_impuestos)
@tax_client = final tax_client
def rate_for(zip)
@tax_client.rates_for_location(zip).combined_rate rescate
Taxjar::Error::NotFound
aumentar RateUnavailableError, "Tasa de impuestos sobre las ventas no disponible para zip: #{zip}"
end
fin
15. https://www.destroyallsoftware.com/blog/2014/testisolationisaboutavoidingmocks
informar fe de erratas • discutir
Machine Translated by Google
Envolviendo una dependencia de terceros • 289
Nuestro objetivo es aislar la clase Invoice por completo de TaxJar, incluidas sus clases de excepción
específicas. Es por eso que transformamos cualquier error de Taxjar::Error::NotFound que vemos en
un tipo de error que hemos definido.
El resto de la aplicación ya no necesita tener ningún conocimiento de TaxJar.
Si la API de TaxJar cambia, esta clase es la única pieza de código que tendremos que actualizar.
Cuando creamos una nueva Factura, ahora pasamos el contenedor en lugar de la clase TaxJar original:
15usandopruebadoblesefectivamente/24/impuesto_ventas/lib/
factura.rb def inicializar(dirección, artículos, impuesto_ventas: Impuestoventas.nuevo)
@address = dirección
@items = artículos
@sales_tax = sales_tax fin
Así es como se ve el nuevo y más simple método Invoice#tax_rate :
15usandopruebadoblesefectivamente/24/impuesto_ventas/lib/
factura.rb def
tasa_impuesto
@impuesto_ventas.tasa_para(@dirección.zip) end
Nuestra especificación de unidad para Factura también es mucho más fácil de entender y mantener.
Ya no necesitamos construir una estructura destartalada de dobles de prueba. Un doble de instancia
única hará:
15usandopruebadoblesefectivamente/24/impuesto_ventas/spec/unidad/
especificación_factura.rb requiere 'factura'
RSpec.describe Invoice do
let(:address) { Address.new(zip: '90210') } let(:items)
{ [Item.new(cost: 30), Item.new(cost: 70)] }
' calcula el total' hacer
impuesto_ventas = instancia_doble(Impuesto_ventas, tarifa_por: 0,095)
factura = Factura.nueva(dirección, artículos, impuesto_ventas: impuesto_ventas)
esperar(factura.calcular_total).to eq(109.50) final final
Al envolver la API de TaxJar, hemos mejorado nuestro código y especificaciones de muchas maneras:
• La API contenedora es más sencilla de llamar porque omite detalles que no necesitamos.
• Podemos cambiar a un servicio de impuestos sobre las ventas de un tercero diferente cambiando
solo una clase, dejando intacto el resto de nuestro proyecto.
informar fe de erratas • discutir
Machine Translated by Google
Capítulo 15. Uso efectivo de los dobles de prueba • 290
• Debido a que controlamos la interfaz del contenedor, no cambiará sin nuestro
conocimiento.
• Las especificaciones de la unidad para Factura no se interrumpirán si cambia la API de TaxJar.
Sin embargo , necesitamos tener alguna forma de detectar cambios en TaxJar. El mejor lugar para eso es una
especificación de integración para nuestro contenedor SalesTax , que es fácil de escribir porque hemos
mantenido nuestro contenedor delgado:
15usandopruebadoblesefectivamente/24/sales_tax/spec/integration/
sales_tax_spec.rb requiere 'sales_tax'
RSpec.describe SalesTax do
let(:sales_tax) { SalesTax.new }
' puede obtener la tasa de impuestos para un código postal dado '
rate = sales_tax.rate_for('90210') expect(rate).to
be_a(Float).and be_ between(0.01, 0.5) end
' genera un error si no se puede encontrar la tasa impositiva'
esperar
{ impuestos_ventas.rate_for('00000')
}.to raise_error(SalesTax::RateUn AvailableError) end
fin
Debido a que esta especificación se compara con las clases reales de TaxJar, fallará correctamente si hay
cambios importantes en la API. Aunque el API debería ser estable, esperamos que la tasa impositiva fluctúe un
poco. Es por eso que estamos verificando contra un rango en lugar de un valor exacto.
Como siguiente paso, podríamos usar la gema VCR aquí para almacenar en caché las respuestas. Luego,
podríamos ejecutar estas especificaciones sin necesidad de credenciales de API reales, lo que será útil en
nuestro servidor de integración continua (CI). Todavía querríamos revalidar contra el servicio real periódicamente,
lo que podemos hacer fácilmente eliminando las grabaciones de VCR.
Tu turno
¡Uf! Le dimos mucho que analizar en este capítulo, desde amplios consejos sobre dobles de prueba hasta
matices de diseño de grano fino. Estos son solo algunos de los principios que exploramos en varios ejemplos
de código:
• Construya su entorno de prueba con cuidado. • Tenga cuidado
con el "objeto". • Conocer los riesgos de los
dobles parciales. • Favorecer la inyección de
dependencia explícita sobre técnicas más implícitas. • Evite falsificar una interfaz que no controla.
informar fe de erratas • discutir
Machine Translated by Google
Tu turno • 291
• Busque falsificaciones de alta
fidelidad. • Envolver interfaces de terceros.
Eso es mucho para tener en mente, ¡pero definitivamente no te estamos pidiendo que lo hagas!
Todos estos consejos se derivan de una práctica clave: construir su entorno de prueba con
cuidado. Recuerde la metáfora del laboratorio cuando esté escribiendo sus especificaciones, y
debería estar bien.
Por encima de todo, cuando encuentre que sus dobles de prueba se desvían de estos principios,
escuche lo que le dicen sobre el diseño de su código. En lugar de buscar una forma diferente
de escribir la prueba, busque una mejor forma de estructurar su código. Por "mejor" no queremos
decir "más comprobable", aunque, como explica Michael Feathers, el buen diseño y la capacidad
de prueba a menudo se refuerzan mutuamente.16 Queremos decir más fácil de mantener, más
flexible, más fácil de entender y más fácil de hacer bien.
Justin Searls analiza estas compensaciones para los dobles de prueba en su "Dobles de prueba"
17,18
página wiki y en su charla SCNA 2012, "To Mock or Not to Mock".
¡Gracias por acompañarnos en este viaje! Por última vez, únase a nosotros en la siguiente
sección para un ejercicio.
Ejercicio
Este ejercicio será un poco más abierto que algunos de los capítulos anteriores. No hay una
sola mejor respuesta. Estimularemos su creatividad con algunas preguntas y luego le daremos
rienda suelta al código.
La siguiente clase de Ruby implementa un juego de adivinanzas. Guarde este código en un
nuevo directorio como lib/guessing_game.rb:
15usandopruebadoblesefectivamente/ejercicios/juego_de_adivinanzas/lib/
juego_de_adivinanzas.rb
clase Juego
de adivinanzas def play @number
= rand(1..100) @guess = nil
5.downto(1) hacer |remaining_guesses| break if
@adivinar == @number pone "Elige
un número del 1 al 100 (#{remaining_guesses} adivinanzas restantes):" @guess = gets.to_i
check_guess end
anuncio_resultado fin
16. https://vimeo.com/15007792
17. https://github.com/testdouble/contributingtests/wiki/TestDouble
18. https://vimeo.com/54045166
informar fe de erratas • discutir
Machine Translated by Google
Capítulo 15. Uso eficaz de los dobles de prueba • 292
privado
def check_guess if
@guess > @number
pone "#{@guess} es demasiado alto!"
elsif @guess < @number
pone "#{@guess} es demasiado
bajo!" final final
def anunciar_resultado
if @conjetura == @número
pone '¡Ganaste!'
demás
pone "¡Perdiste! El número era: #{@number}" end
fin
fin
# jugar el juego si este archivo se ejecuta directamente
GuessingGame.new.play if __FILE__.end_with?($PROGRAM_NAME)
Ahora, ejecute el código con ruby lib/guessing_game.rb e intente jugar el juego. Obtenga una
idea de cómo funciona.
Tu misión es poner a prueba esta clase. Aquí hay algunas preguntas que quizás desee
considerar:
• ¿Cuáles son los colaboradores de esta clase y cómo puede proporcionarlos su prueba? •
¿Qué tipos de dobles de prueba funcionarían mejor aquí?
• ¿Qué casos extremos necesita cubrir en sus especificaciones? •
¿Alguna de las responsabilidades de esta clase debe recaer en un colaborador?
Si se queda atascado, puede echar un vistazo a las especificaciones y la clase refactorizada
que escribimos para este ejercicio. Están en el código fuente del libro.19,20
Una vez que tenga una solución, considere publicarla en los foros.21 Nos encantaría ver qué
se le ocurrió.
¡Feliz prueba!
19. https://github.com/rspec3book/bookcode/blob/v1.0/15usingtestdoubleseffectly/solutions/guessing_game/
spec/adivinanzas_juego_spec.rb
20. https://github.com/rspec3book/bookcode/blob/v1.0/15usingtestdoubleseffectly/solutions/guessing_game/
lib/juego_de_adivinanzas.rb
21. https://forums.pragprog.com/forums/385
informar fe de erratas • discutir
Machine Translated by Google
APÉNDICE 1
RSpec y el ecosistema Ruby más amplio
RSpec no existe en el vacío. Es parte del ecosistema Ruby más amplio y está diseñado para
funcionar bien con las herramientas Ruby existentes. En este apéndice, verá cómo usar RSpec de
manera efectiva con dos de las herramientas más importantes de Ruby: Bundler y Rake. También le
mostraremos cómo usar partes de RSpec con otros marcos de prueba.
empaquetador
Cuando usa Bundler para administrar sus dependencias, hay algunas formas diferentes de asegurarse
de que se carguen las versiones correctas de todas sus bibliotecas:
• Llame a Bundler.require desde su código Ruby •
Envuelva cada programa Ruby con bundle exec en la línea de comando • Use el
modo independiente de Bundler
Todas estas técnicas funcionan con RSpec. Echemos un vistazo a cada uno a su vez.
La primera opción, Bundler.require, es conveniente: carga todas las gemas de su proyecto, por lo
que no tiene que recordar solicitar cada gema individualmente antes de usarla.
Pero tiene implicaciones para el tiempo de arranque y la capacidad de mantenimiento de su
aplicación, como señala Myron en su publicación de blog.1
La segunda opción, bundle exec, es más rápida y evita algunos de estos problemas de mantenimiento,
pero sigue siendo ineficiente. Cada vez que ejecuta (por ejemplo) bundle exec rspec, Bundler dedica
tiempo a validar que tiene instaladas todas las versiones de gemas correctas, aunque esta validación
solo es necesaria en las ocasiones poco frecuentes en que cambian las gemas de su proyecto.
1. http://myronmars.to/n/devblog/2012/12/5reasonstoavoidbundlerrequire
informar fe de erratas • discutir
Machine Translated by Google
Apéndice 1. RSpec y el ecosistema Ruby más amplio • 294
La opción final, el modo independiente, ahorra tiempo al usar Bundler solo cuando instala gemas.
En tiempo de ejecución, solo está usando Ruby simple, con Bundler completamente fuera de la
ecuación. Para usar este modo, pase la opción standalone a Bundler:
$ paquete de instalación independiente
Este comando genera un archivo en el directorio de su proyecto llamado bundle/bundler/set up.rb,
que configura $LOAD_PATH de Ruby con las versiones de gemas exactas que su proyecto está
configurado para usar. Todo lo que tiene que hacer es requerir este archivo de su código, antes de
cargar cualquier gema. De hecho, incluso puede omitir este paso obligatorio utilizando la opción
binstubs junto con standalone:
$ instalación del paquete independiente binstubs
Este comando genera binstubs, envoltorios alrededor de los comandos Ruby de sus gemas, como
rspec y rake, dentro del directorio bin de su proyecto . Cada uno de estos cargará bundle/bundler/
setup.rb por usted y luego ejecutará su comando original.
Esta técnica permite que RSpec comience notablemente más rápido, especialmente en proyectos
grandes, lo que nos brinda una respuesta casi instantánea cuando ejecutamos archivos de
especificaciones individuales. Sin embargo, deberá recordar volver a ejecutar este comando cada
vez que cambie su Gemfile o Gemfile.lock . Recomendamos crear un alias en su shell, como bisb,
y ejecutarlo cuando agregue o elimine gemas, obtenga código de otra persona, cambie ramas, etc.
La mejora es particularmente espectacular cuando utiliza la técnica de Cómo garantizar que la
aplicación funcione de verdad, en la página 98 para ejecutar cada uno de sus archivos de
especificaciones de forma aislada. Este es el tiempo que tomó esa tarea usando el paquete ejecutivo regular:
$ hora (para el archivo en spec/**/*_spec.rb haz el
paquete exec rspec $file || exit 1
hecho) > /dev/null
1.50s usuario 0.17s sistema 98% cpu 1.707 total
Aquí está el mismo bucle, pero en su lugar se utiliza el binstub independiente:
$ tiempo (para el archivo en spec/**/*_spec.rb do bin/
rspec $file || exit 1 done) > /dev/null
0.85s usuario 0.11s sistema 97% cpu 0.983 total
¡De 1,7 segundos a menos de un segundo! El modo independiente es casi el doble de rápido,
porque el tiempo de arranque para cada invocación de rspec es más rápido. En un proyecto con
muchas gemas (¡o muchos archivos de especificaciones!), la diferencia es aún más dramática.
informar fe de erratas • discutir
Machine Translated by Google
Rastrillo • 295
Rastrillo
A lo largo de este libro, ha ejecutado su conjunto de especificaciones a través del comando rspec . Pero hay
otra forma común de ejecutar sus especificaciones: usar la herramienta de compilación Rake.
La gema rspeccore se envía con una tarea Rake fácil de configurar. Para habilitarlo, agregue las siguientes
dos líneas al Rakefile de su proyecto:
A1rspecandwiderecosystem/02/expense_tracker/
Rakefile requiere 'rspec/core/rake_task'
RSpec::Core::RakeTask.new(:spec)
Este fragmento define una tarea de especificación simple que ejecutará rspec con sus valores predeterminados
configurados. A continuación, puede ejecutarlo así:
especificación de rake de $
Ejecutar RSpec de esta manera agrega algunos gastos generales. Se necesita tiempo para cargar Rake,
además de cualquier biblioteca que necesite para otras tareas en su Rakefile. Una tarea Rake enlatada
también es menos flexible que el comando rspec , ya que no le permite personalizar ejecuciones individuales
a través de las opciones de la línea de comandos. Dicho esto, las pruebas a través de Rake son útiles en
algunas situaciones:
• Cuando tiene conjuntos específicos de opciones de RSpec que usa juntas con frecuencia
• Cuando desee ejecutar RSpec como un paso en un proceso de compilación de varios pasos
(como en un servidor de integración continua)
• Como una conveniencia para los nuevos desarrolladores de su proyecto, que pueden esperar ejecutar
rake sin argumentos para compilar y probar el código base por completo.
Si desea especificar opciones RSpec adicionales, puede pasar un bloque a RakeTask.new:
A1rspecandwiderecosystem/02/expense_tracker/
Rakefile requiere 'rspec/core/rake_task'
espacio de nombres :spec do
desc 'Ejecuta especificaciones de la unidad'
RSpec::Core::RakeTask.new(:unidad) do |t|
t.pattern = 'spec/unidad/**/*_spec.rb'
t.rspec_opts = ['perfil'] final
fin
Aquí, hemos definido una tarea spec:unit que ejecuta todas las especificaciones de nuestra unidad con la generación de
perfiles habilitada.
informar fe de erratas • discutir
Machine Translated by Google
Apéndice 1. RSpec y el ecosistema Ruby más amplio • 296
Sus usuarios también pueden proporcionar sus propios argumentos de línea de comandos a sus tareas
Rake configurando la variable de entorno SPEC_OPTS . Por ejemplo, pueden obtener una salida de estilo
de documentación de su tarea spec:unit llamándola así:
$ rake spec:unidad SPEC_OPTS='fd'
Si está desarrollando una aplicación web, probablemente mantenga las gemas solo de prueba como
RSpec fuera de sus servidores de producción. Para usar Rake en un entorno de este tipo, deberá manejar
el caso cuando RSpec no esté disponible:
A1rspecandwiderecosystem/02/expense_tracker/
Rakefile begin require 'rspec/core/rake_task'
RSpec::Core::RakeTask.new(:spec)
rescatar LoadError
pone el extremo 'Tareas de especificaciones no definidas ya que RSpec no está
disponible'
Ahora, podrá usar Rake para ejecutar especificaciones en su máquina de desarrollo y ejecutar tareas de
implementación (como compilar activos) en su entorno de producción.
Uso de partes de RSpec con otros marcos de prueba
A veces, querrá usar los potentes dobles de prueba disponibles en los simulacros de rspec, o los
comparadores componibles proporcionados por rspecexpectations, en un proyecto donde el resto de
RSpec no encaja bien. Por ejemplo, es posible que ya tenga un extenso conjunto de pruebas escrito en
Minitest, o que esté escribiendo pruebas de aceptación en Cucumber.2,3
Ambas partes de RSpec son fáciles de usar con otros marcos de prueba, o incluso sin un marco de prueba.
De hecho, ya lo ha hecho en Partes de una expectativa y en Comprensión de los dobles de prueba.
Si está utilizando Minitest, RSpec le ofrece un par de ventajas, incluidas las siguientes:
• Informar correctamente las expectativas insatisfechas como fallos de aserción de Minitest
• Asegurarse de que el método de expectativa de RSpec no entre en conflicto con el método de Minitest
del mismo nombre
• Verificar las expectativas de mensajes que ha establecido en objetos simulados
2. https://github.com/seattlerb/minitest
3. https://cucumber.io
informar fe de erratas • discutir
Machine Translated by Google
Uso de partes de RSpec con otros marcos de prueba • 297
Para aprovechar esta integración, solicite uno o ambos de los siguientes archivos en sus
pruebas:
A1rspecandwiderecosystem/03/minitest_with_rspec/dinosaur_test.rb
requiere 'rspec/mocks/minitest_integration' requiere
'rspec/expectations/minitest_integration'
Luego, puede usar los dobles de prueba y las expectativas de RSpec libremente en su
Paquete minitest:
A1rspecandwiderecosystem/03/minitest_with_rspec/dinosaur_test.rb
class DinosaurTest < Minitest::Test
def prueba_dinosaurios_volar_cohetes
dinosaurio = Dinosaurio.nuevo
cohete = instancia_doble(Cohete)
esperar(cohete).recibir (:¡lanzar!)
dinosaurio.volar(cohete)
esperar(dinosaurio).estar_excitado fin
fin
Si está escribiendo pruebas de aceptación usando Cucumber, no necesita hacer nada
especial para usar las expectativas de estilo RSpec. Cucumber detectará cuando hayas
instalado la gema rspecexpectations y la habilitará automáticamente.
Por lo general, no recomendamos el uso de dobles de prueba con los tipos de pruebas de
aceptación para las que Cucumber está diseñado, pero Cucumber ofrece integración de
rspecmocks para aquellos casos excepcionales en los que lo necesite. Para habilitarlo,
requiere 'cucumber/rspec/doubles' en la configuración de su entorno.
Para usar partes de RSpec con otro marco de prueba, eche un vistazo dentro de los dos
archivos minitest_integration de este ejemplo. Le darán un buen punto de partida para la
integración con su marco de prueba.
informar fe de erratas • discutir
Machine Translated by Google
APÉNDICE 2
Uso de RSpec con rieles
A lo largo de este libro, apenas hemos mencionado Rails, a pesar de que es el framework
de Ruby más popular. Esta falta de énfasis es intencional. Descubrimos que si sus
fundamentos de prueba son sólidos y sabe cómo usar RSpec en un contexto que no es
Rails, es fácil aplicar ese conocimiento a una aplicación Rails, pero no al revés. En resumen,
todo lo que hemos cubierto en este libro se aplica a las aplicaciones de Rails.
Dicho esto, nos gustaría mostrarle algunas ventajas que ofrece RSpec para probar
aplicaciones de Rails. En este apéndice, le mostraremos cómo configurar una aplicación
de Rails para realizar pruebas con RSpec. También hemos preparado algunas hojas de
trucos que catalogan las funciones proporcionadas para trabajar con las aplicaciones de Rails.
Para obtener consejos de prueba específicos de Rails más detallados, recomendamos el
libro Rails 4 Test Prescriptions: Build a Healthy Codebase [Rap14] de Noel Rappin. Aunque
el título se refiere a Rails 4, el consejo es atemporal y aún se aplica a Rails 5. Mientras
escribimos este apéndice, Noel está trabajando en una edición actualizada para Rails 5.1.
Instalación
Rails proporciona infraestructura para probar directamente piezas específicas de su
aplicación: modelos, vistas, controladores, etc. También admite pruebas que integran
varias capas y pruebas de aceptación que integran todas las capas.
La gema rspecrails adapta la infraestructura de prueba de Rails para su uso desde RSpec.
Para usarlo, agregue una entrada como la siguiente a los grupos :desarrollo y :prueba en
su Gemfile:
A2usingrspecwithrails/01/rails_app/
Gemfile group :desarrollo, :test do
gem 'rspecrails', '~> 3.6'
fin
1. https://pragprog.com/book/nrtest3/rails5testprescriptions
informar fe de erratas • discutir
Machine Translated by Google
Apéndice 2. Uso de RSpec con Rails • 300
Luego, ejecute la instalación del paquete para instalar rspecrails. Finalmente, puede
configurar su proyecto para usar rspecrails con el siguiente comando:
$ rieles generan rspec:instalar
crear .rspec crear
especificaciones
crear especificaciones/spec_helper.rb
crear especificaciones/rails_helper.rb
El archivo .rspec configura los argumentos de la línea de comandos para pasar a RSpec
implícitamente, tal como lo discutimos anteriormente en Configuración de los valores
predeterminados de la línea de comandos, en la página 149. El archivo generado por rspecrails
cargará spec_helper.rb en cada ejecución de RSpec. El archivo spec_helper.rb , a su vez,
establece una serie de útiles valores predeterminados de RSpec, como la opción
verificar_partial_dobles que recomendamos en Uso efectivo de dobles parciales, en la página
271. También proporciona algunas sugerencias de configuración en una sección comentada.
Este proceso de instalación también crea un segundo archivo de configuración, rails_helper.rb, pero
no configura RSpec para que lo requiera de manera predeterminada. Este archivo carga tanto Rails
como rspecrails, lo que configura funciones de prueba como accesorios de modelo y base de datos.
actas.
Estas funciones de prueba adicionales son útiles o incluso esenciales para muchas
especificaciones, pero no para todas. En particular, nos esforzamos por crear objetos de dominio
que tengan límites claros y que no dependan directamente de Rails. Cuando probamos estos
objetos, queremos la retroalimentación más rápida posible. En consecuencia, no requerimos
rails_helper de un archivo de especificaciones a menos que realmente necesite Rails.
Usando rspecrieles
Una vez que rspecrails esté instalado, podrá ejecutar su paquete de especificaciones
utilizando el paquete exec rspec o bin/rake spec. Los comandos en el directorio bin , como
rake o rails, son binstubs generados por Rails (scripts de envoltura) que le ahorran la molestia
de recordar escribir bundle exec antes de cada comando.2
Si está acostumbrado a crear binstubs a través de la opción binstubs de Bundler como se
describe en Bundler, en la página 293, tenga en cuenta que esta opción puede no funcionar
bien con Spring, un precargador de Rails diseñado para acelerar los tiempos de arranque.3
Es posible que vea Rails los comandos cuelgan o imprimen mensajes de advertencia de los
binstubs generados por Bundler. Para resolver el problema, deberá eliminar Spring o cambiar
a los binstubs proporcionados por Rails:
2. https://github.com/rbenv/rbenv/wiki/Understandingbinstubs
3. https://github.com/rails/spring
informar fe de erratas • discutir
Machine Translated by Google
Tipos de especificaciones • 301
$ configuración del paquete delete bin $
aplicación de rieles: actualización: contenedor
Cuando genera un objeto Rails, como un controlador o andamio, RSpec creará un archivo de especificaciones
correspondiente para usted:
$ rieles generar modelo pterodactyl invocar
active_record create db/
migrate/20170613203526_create_pterodactyls.rb app/models/pterodactyl.rb
crear
invocar rspec
crear spec/modelos/pterodactyl_spec.rb
También puede generar solo el archivo de especificaciones, si la clase que está probando ya existe o si no
desea crearla todavía; simplemente anteponga el elemento que está generando con rspec:, como en los
rieles genera rspec: model pterodactyl.
A continuación, echemos un vistazo a los diferentes tipos de especificaciones que puede escribir para probar
su aplicación Rails.
Tipos de especificaciones
Para la API de seguimiento de gastos que creó en este libro, escribió tres tipos diferentes de especificaciones:
• Especificaciones de aceptación para probar toda la aplicación de principio
a fin • Especificaciones de unidad para probar una capa
de forma aislada • Especificaciones de integración para probar objetos con colaboradores reales y externos
servicios
Una aplicación de Rails es más compleja que una pequeña API. En consecuencia, Rails proporciona
infraestructura para varios tipos diferentes de pruebas, incluidas las siguientes:
• Pruebas de integración que controlan su aplicación como una caja negra a través de su interfaz HTTP •
Pruebas funcionales para ver cómo responden sus controladores a las solicitudes • Pruebas
unitarias para controlar un solo objeto o capa • Pruebas
específicas para modelos, anuncios publicitarios y trabajos en segundo plano; cualquier prueba dada
aquí puede haber una unidad o prueba de integración
Para probar uno de estos aspectos de su aplicación en RSpec, etiquete su grupo de
ejemplo con :type metadata, pasándole uno de los tipos de especificaciones del gráfico
en la siguiente sección (:model, :request, :helper, etc.) . Por ejemplo, aquí está el archivo
de especificaciones generado por el comando Rails generate model pterodactyl de la
sección anterior:
informar fe de erratas • discutir
Machine Translated by Google
Apéndice 2. Uso de RSpec con Rails • 302
A2usingrspecwithrails/01/rails_app/spec/models/pterodactyl_spec.rb
requiere 'rails_helper'
RSpec.describe Pterodactyl, tipo: :model do
#...
fin
Algunos de estos tipos de especificaciones, como :request y :model, serán el pan y
mantequilla de su prueba. Otros están allí principalmente para casos extremos o para versiones anteriores.
compatibilidad, ya que rspecrails funciona con todas las versiones de Rails desde 3.0 hasta
el último lanzamiento. (Rails 5.1 salió cuando estábamos terminando
toca este libro, y rspecrails 3.6+ lo admite).
Hoja de referencia de tipos de especificaciones
• • alias de función/escenario para describe/it
:característica Probando el • Requiere la
toda la aplicación, Carpincho
• Acceso a la API de Capybara, que incluye
incluyendo el joya
visit, fill_in, etc.4
interfaz de usuario en el navegador,
a través de Carpincho • Asistentes de ruta con nombre del formulario
alguna_ruta_ruta
scripts, como Estante medio
• Solicitar emparejadores; ver Rails Matchers
API pila de mercancías
Hoja de referencia, en la página 304
• Similar a
• Ejercitar todas las
• Asistentes de ruta con nombre del formulario
capas de su aceptación
alguna_ruta_ruta
código rubí especificaciones que
escribió para el
• Múltiple
gastos
solicitudes,
API de seguimiento
controladores, ses
siones
:modelo • • Transacciones y modelo de base de datos
Probando tu
Registro activo Accesorios (disponibles para todos los tipos de especificaciones,
modelos pero más relevante aquí)
4. http://equipocapybara.github.io/capybara/
informar fe de erratas • discutir
Machine Translated by Google
Hoja de referencia de tipos de especificaciones • 303
aislamiento
• Comparadores de controladores; ver Rieles
• Por diseño,
Hoja de referencia de Matchers, en la página 304 no ren
vistas por
• controlador para definir un anónimo
por defecto; llamar
controlador5
render_views si
• ruta para usar un conjunto de rutas diferente necesitas esto
comportamiento6
• bypass_rescue para evitar la conversión
errores a 500 respuestas
• Asistentes de ruta con nombre del formulario
alguna_ruta_ruta
:vista •
Probando el • asignar para hacer variables de instancia
estafa HTML disponible
tiendas de tu
puntos de vista
:ayudante •
Vista de prueba • asignar para hacer variables de instancia
modo auxiliar disponible
reglas en
• helper.some_method para llamar a métodos
aplicación/ayudantes
desde el módulo de ayuda que está
pruebas
:remitente •
Rieles de prueba • Asistentes de ruta con nombre del formulario
anuncios publicitarios alguna_ruta_ruta
•
:enrutamiento Comprobando eso • Comparadores de enrutamiento; ver Rails Matchers
Ruta de URL a Hoja de referencia, en la página 304
acciones
• Asistentes de ruta con nombre del formulario
específicas del controlador
alguna_ruta_ruta
• • Transacciones y modelo de base de datos
:trabajo Probar trabajos de
fondo Accesorios (disponibles para todos los tipos de especificaciones,
pero más relevante aquí)
5. https://relishapp.com/rspec/rspecrails/v/36/docs/controllerspecs/anonymouscontroller
6. https://relishapp.com/rspec/rspecrails/v/36/docs/controllerspecs/renderviews
informar fe de erratas • discutir
Machine Translated by Google
Apéndice 2. Uso de RSpec con Rails • 304
Hoja de referencia de Rails Matchers
Además de estos tipos de especificaciones, rspecrails proporciona algunos
emparejadores Algunos de estos están disponibles para cualquier especificación una vez que haya requerido
rieles_ayudante; otros son solo para ciertos tipos de especificaciones.
ser_un_nuevo(modelo_clase) record.is_a(model_class) && Todos los tipos de especificaciones
registro.nuevo_registro?
ser_un_nuevo(modelo_clase).con(atributo: 'valor') record.is_a(model_class) && Todos los tipos de especificaciones
registro.nuevo_registro? &&
registro.atributo == 'valor'
argumentos coincidentes
enqueue_job(job_class).with(some_args) en cola un trabajo job_class
con argumentos coincidentes
de respuesta, como
:éxito
redirect_to('http://algunaurl.ejemplo.com') La respuesta redirige a :request, :controller
URL especificada
route_to(controlador: 'nombre', acción: 'nombre'), La ruta conduce al :controlador, :routing especificado
route_to('controlador#acción') controlador/acción/
parámetros
¡No sienta que necesita usar todos estos tipos de especificaciones en la misma aplicación!
Aquí hay algunas recomendaciones para situaciones específicas.
Cuando realiza pruebas de aceptación de afuera hacia adentro:
• Para las API basadas en HTTP, utilice las especificaciones de solicitud.
• Para aplicaciones web orientadas al usuario, agregue Capybara al proyecto y use
especificaciones de funciones; consulte el artículo de Michael Crismali para obtener consejos de configuración.7
7. https://www.devmynd.com/blog/settinguprspecandcapybarainrails5fortesting/
informar fe de erratas • discutir
Machine Translated by Google
Hoja de referencia de Rails Matchers • 305
Para verificar los componentes principales de su aplicación:
• Use especificaciones de unidad e integración, sin Rails donde sea posible, para sus objetos de dominio.
• Usar especificaciones de modelo, correo y trabajo para sus respectivos tipos de objetos Rails.
Tendemos a rehuir los siguientes tipos de especificaciones:
• Ver especificaciones, que cuestan más esfuerzo que el valor que proporcionan; alientan a poner lógica
en sus puntos de vista, que nos gusta mantener al mínimo
• Especificaciones de enrutamiento, que generalmente duplican la cobertura de prueba de su aceptación
especificaciones de distancia
• Las especificaciones del controlador, que brindan una imagen demasiado simplificada del
comportamiento, tienen algunas trampas sobre cómo eluden el middleware de Rack y se están
eliminando gradualmente de la práctica actual de Rails; use especificaciones de solicitud en su lugar8
No hay necesidad de limitarse solo a los tipos de especificaciones compatibles con los rieles rspec. Al
escribir objetos de dominio que no dependen directamente de Rails, puede probar su código de la manera
que mejor le funcione.
8. https://blog.bigbinary.com/2016/04/19/changestotestcontrollersinrails5.html
informar fe de erratas • discutir
Machine Translated by Google
APÉNDICE 3
Hoja de trucos del emparejador
En Comparadores incluidos en las expectativas de RSpec, revisamos los comparadores de uso más
común incluidos en RSpec. Ahora, nos gustaría mostrarle todos los comparadores integrados en RSpec
a partir de la versión 3.6 en una sola referencia que puede tener a mano mientras escribe las
especificaciones.
La columna Pasa si... proporciona una expresión para cada comparador que es equivalente a lo que
comprueba el comparador. El fragmento no es necesariamente cómo se implementa internamente el
comparador, ya que hemos pasado por alto algunos casos extremos que no surgen durante el uso diario.
Si tiene curiosidad acerca de estos detalles de implementación, puede consultar el código fuente.1
Coincidencias de valor
Dada cualquier expresión de Ruby a, los emparejadores de valor se expresan de la siguiente forma:
esperar(a).to matcher
Para negar un comparador, use not_to o to_not en lugar de to:
expect(a).not_to matcher # o
esperar (a).to_not emparejador
Igualdad/Identidad
Matcher Passes si... Alias disponibles
ecuación (x)
un == x an_object_eq_to(x)
igual(x) a.igual?(x) ser(x)
un_objeto_igual_a(x)
1. https://github.com/rspec/rspecexpectations
informar fe de erratas • discutir
Machine Translated by Google
Apéndice 3. Hoja de referencia de Matcher • 308
Veracidad y cero
Matcher Passes si... Alias disponibles
be_truthy a != nil && a != false a_truthy_value
ser cierto un == cierto
be_falsey a == cero || un == falso ser_falso
un_valor_falso
un_valor_falso
ser falso un == falso
Tipos
emparejador pasa si… Alias disponibles
be_an_instance_of(klass) a.class == klass ser_instancia_de(clase)
una_instancia_de(clase)
Comparaciones de operadores
Matcher Passes si... Alias disponibles
ser == x un == x un_valor == x
ser === xa === x un_valor === x
informar fe de erratas • discutir
Machine Translated by Google
Igualadores de valor • 309
Comparaciones delta/rango
emparejador pasa si… Alias disponibles
estar_entre(1, 10).inclusive a >= 1 && a <= 10 estar_entre(1, 10)
un_valor_entre(1, 10).inclusive
un_valor_entre(1, 10)
estar_entre(1, 10).exclusivo a > 1 && a < 10 un_valor_entre(1, 10).exclusivo
cubrir(x, y) a.cover?(x) && a.cover?(y) a_range_covering(x, y)
Cadenas y colecciones
emparejador pasa si… Alias disponibles
contiene_exactamente(2, 1, 3) a.sort == [2, 1, 3].sort matriz_coincidencia([2, 1, 3])
una_colección_que_contiene_exactamente(2, 1, 3)
una_cadena_que_comienza_con(x, y)
una_cadena_que_comienza_con(x, y)
|| (una.clave?(x) && una.clave?(y)) una_cadena_incluyendo(x, y)
a_hash_incluido(x, y)
todos (emparejador)
matcher.coincidencias?(e) }
partido (x: emparejador, y: 3) matcher.matches?(a[:x]) && an_object_matching(x: matcher, y: 3)
a[:y] == 3
comparador.coincidencias?(a[1])
a_string_matching(/regex/)
informar fe de erratas • discutir
Machine Translated by Google
Apéndice 3. Hoja de referencia de Matcher • 310
Tipificación y atributos de Duck
emparejador pasa si… Alias disponibles
have_attributes(w: x, y: z) aw == x && ay == z un_objeto_que_tiene_atributos(w: x,
y:z)
a.responder_a?(:y)
Predicados dinámicos
emparejador pasa si… Alias disponibles
be_an_xyz
be_an_foo(x, y, &b)
tener_xyz a.has_xyz?
have_foo(x, y, &b) a.has_foo(x, y, &b)?
Comparadores adicionales
existir a.existe? || a.existe? un_objeto_existente
existir(x, y) a.existe(x, y)? || a.existe(x, y)? un_objeto_existente(x, y)
satisfacer { |x| ... } El bloque proporcionado devuelve verdadero an_object_satisfying { |x| ... }
satisfacer("criterios") { |x| ... } El bloque proporcionado devuelve verdadero an_object_satisfying("...") { |x| ... }
informar fe de erratas • discutir
Machine Translated by Google
Coincidencias de bloques • 311
Coincidencias de bloques
Los comparadores de bloques observan un bloque de código y se utilizan para especificar un efecto
secundario que se produce cuando se ejecuta el bloque. Toman la forma:
esperar {some_code}.to matcher
Al igual que con los emparejadores de valores, los emparejadores de bloques se pueden negar usando
not_to o to_not en lugar de to.
Mutación
El comparador de cambios captura un valor antes de ejecutar el bloque (valor_antiguo) y nuevamente
después de ejecutar el bloque (valor_nuevo). El valor se puede especificar de dos maneras:
esperar {hacer_algo}.para cambiar(obj, :attr) # o
esperar {hacer_algo}.cambiar {obj.attr}
Admite una interfaz rica y fluida para especificar más detalles sobre la mutación:
emparejador pasa si…
change { } old_value != new_value change { }.by(x)
(new_value old_value) == x change { }.by_at_least(x) (new_value
old_value) >= x change { }.by_at_most(x) (new_value
valor_antiguo) <= x cambio { }.from(x) valor_antiguo != valor_nuevo
&& valor_antiguo == x
cambiar { }.a(y) valor_antiguo != valor_nuevo && valor_nuevo == y
cambiar { }.de(x).a(y) valor_antiguo != valor_nuevo && valor_antiguo == x && valor_nuevo == y
informar fe de erratas • discutir