0% encontró este documento útil (0 votos)
272 vistas300 páginas

Efectividad en pruebas con RSpec 3

Cargado por

Care Papa Rh
Derechos de autor
© © All Rights Reserved
Nos tomamos en serio los derechos de los contenidos. Si sospechas que se trata de tu contenido, reclámalo aquí.
Formatos disponibles
Descarga como PDF, TXT o lee en línea desde Scribd
0% encontró este documento útil (0 votos)
272 vistas300 páginas

Efectividad en pruebas con RSpec 3

Cargado por

Care Papa Rh
Derechos de autor
© © All Rights Reserved
Nos tomamos en serio los derechos de los contenidos. Si sospechas que se trata de tu contenido, reclámalo aquí.
Formatos disponibles
Descarga como PDF, TXT o lee en línea desde Scribd

Machine Translated by Google

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:

00­introduction/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/introducing­bdd/

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  ­­only­failures  y  ­­next­failure  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/whats­in­a­story/

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/effect­testing­with­rspec­3  
6.  https ://github.com/rspec­3­libro

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:
• rspec­core  es  el  arnés  de  prueba  general  que  ejecuta  sus  especificaciones.

•  rspec­expectations  proporciona  una  sintaxis  poderosa  y  legible  para  verificar  las  propiedades  
de  su  código.

•  rspec­mocks  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  rspec­support­3.6.0  Instalado  con  éxito  rspec­
core­3.6.0  Instalado  con  éxito  diff­lcs­1.3  Instalado  
con  éxito  rspec­expectations­3.6.0  Instalado  
con  éxito  rspec­mocks­3.6  .0  Instalado  con  éxito  rspec­3.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  
­  rspec­core  3.6.0  ­  
rspec­expectations  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.ruby­lang.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:

01­primeros  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:

01­primeros  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  semi­intercambiables,  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/3a­arrange­act­assert/

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:

01­primeros­pasos/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:

01­primeros  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 :

01­getting­started/04/spec/sandwich_spec.rb  '  
me  permite  agregar  ingredientes'  hacer
sandwich  =  Sandwich.new('delicioso',  [])

3.  https://webuild.envato.com/blog/making­the­most­of­bdd­part­1/

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 :

01­primeros  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:

01­getting­started/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:

01­primeros  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:

01­primeros­pasos/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 :

01­primeros­pasos/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:

01­primeros  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:

01­primeros­pasos/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/4­simple­memoization­patterns­in­ruby­and­one­gem/

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:

01­primeros  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:

02­running­specs/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:

02­running­specs/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  coderay­1.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:

$  rspec­fd

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:

02­running­specs/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/vim­rspec  
3.  https://www.emacswiki.org/emacs/RspecMode  
4.  https://github.com/SublimeText/RSpec  
5.  https://github .com/fcoury/atom­rspec  
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  ­­only­failures .  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  ̀­­only­failures`,  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:

02­running­specs/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  ­­only­failures :

$  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

02­running­specs/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  ­­only­failures :

$  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,  ­­next­failure,  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):

02­running­specs/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:

02­running­specs/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 :

02­running­specs/07/spec/coffee_spec.rb  
fcontext  'con  leche'  hacer

…  este  código  en  realidad  era  solo  una  abreviatura  de  la  siguiente  expresión:

02­running­specs/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  ­­only­failures ,  
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:

02­running­specs/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:

02­running­specs/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é :

02­running­specs/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:

02­running­specs/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  ­­next­failure  y  observe  el  resultado.  ¿En  qué  se  diferencia  de  la  técnica  ­­only­failures  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  ­­next­failure .  ¿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  ­­next­failure ,  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/tdd­straw­men­and­rhetoric

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  
Bundler­1.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:

04­specs­de­aceptación/01/expense_tracker/
Gemfile  gema  'rspec',  '3.6.0'  
gema  'coderay',   '1.1.1'
gema  'rack­test',  '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  diff­lcs  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  rspec­support  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  rack­test  0.7.0  Uso  de  
rspec­core  3.6.0  Uso  de  rspec­
expectations  3.6.0  Uso  de  
rspec­mocks  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:

04­acceptance­specs/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:

04­acceptance­specs/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'  =>  
'2017­06­10'
}

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/currency­rounding­errors/  
5.  https://ruby­doc.org/stdlib­2.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  clave­valor  
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/a­beginners­guide­to­http­and­rest­­net­16340

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 :

04­acceptance­specs/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:

04­acceptance­specs/02/expense_tracker/app/api.rb  
requiere  'sinatra/base'  requiere  
'json'

8.  https://github.com/rack­test/rack­test#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:

04­acceptance­specs/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 :

04­acceptance­specs/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:

04­acceptance­specs/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:

04­acceptance­specs/04/expense_tracker/spec/acceptance/expense_tracker_api_spec.rb  
'  registra  los  gastos  enviados'  do  coffee  =  
{ 'beneficiario'  
=>  'Starbucks',  'cantidad'  =>  
5.75,  'fecha'  =>  '2017  
­06­10'
}

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:

04­acceptance­specs/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

04­acceptance­specs/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:

04­aceptación­especificaciones/05/expense_tracker/spec/aceptación/
expense_tracker_api_spec.rb  café  =  post_gasto(
'beneficiario'  =>  'Starbucks',  
'cantidad'  =>  5,75,  
'fecha'  =>  '2017­06­10'
)

Usando  el  mismo  ayudante,  registremos  un  gasto  en  la  misma  fecha  y  otro  en  una  fecha  diferente:

04­acceptance­specs/05/expense_tracker/spec/acceptance/expense_tracker_api_spec.rb  
zoo  =  post_expense(
'beneficiario'  =>  
'Zoológico',  'cantidad'  =>  
15.25,  'fecha'  =>  '2017­06­10'
)

comestibles  =  post_expense(
'beneficiario'  =>  'Whole  Foods',  
'cantidad'  =>  95.20,  
'fecha'  =>  '2017­06­11'
)

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:

04­acceptance­specs/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/2017­06­10'     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:

04­acceptance­specs/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"=>"2017­06­10",  "id"=>42},  {"payee"=>"  Zoo",     "cantidad"=>15,25,  "fecha"=>"2017­06­10",  
"id"=>42}]  colección  real  contenida:  los  elementos  que  faltaban  
eran:  "cantidad"=>5,75,  "fecha   []  
"=>"2017­06­10",  "id"=>42},   [{"beneficiario"=>"Starbucks",
{"beneficiario"=>"Zoo",     "cantidad"=>15.25,  "fecha"=>"2017­06­10",  "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:

04­acceptance­specs/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"=>"2017­06­10",  "id"=>42},  {"payee"=>"  Zoo",     "amount"=>15.25,  "date"=>"2017­06­10",  
"id"=>42}]  la  colección  real  contenía:
[]  
los  elementos  que  faltaban  eran:   [{"beneficiario"=>"Starbucks",
"cantidad"=>5,75,  "fecha"=>"2017­06­10",  "id"=>42},  {"beneficiario"=>"Zoo",     "cantidad"=  >  
15,25 ,  "fecha"=>"2017­06­10",  "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:

04­acceptance­specs/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  
[2017­06­13  13:34:10]  INFO  WEBrick  1.3.1  [2017­06­13  
13:34:10]  INFO  ruby  2.4.1  (2017­03­22)  [x86_64­darwin15]
[2017­06­13  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/2017­06­10  ­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:

HTTP Código  de  enrutamiento   Las  especificaciones  

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/how­i­test­rails­applications/

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.

05­unit­specs/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:

05­unit­specs/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 :

05­unit­specs/01/expense_tracker/spec/spec_helper.rb  
RSpec.configure  do  |config|
  config.filter_gems_from_backtrace  'rack',  'rack­test',  '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/rspec­core/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:

05­unit­specs/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:

05­unit­specs/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:

05­unit­specs/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

05­unit­specs/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 :

05­unit­specs/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:

05­unit­specs/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:

05­unit­specs/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:

05­unit­specs/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  rspec­mocks.
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:

05­unit­specs/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:

05­unit­specs/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.

05­unit­specs/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 :

05­unit­specs/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:

05­unit­specs/  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:

05­unit­specs/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:
05­unit­specs/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:

05­unit­specs/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:

05­unit­specs/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:

05­unit­specs/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:

05­unit­specs/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í:

05­unit­specs/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/2017­06­12'

…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 :

05­unit­specs/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:

06­integration­specs/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  
diff­lcs  1.3  Uso  de  
mustermann  1.0.0  Uso  de  
rack  2.0.3  Uso  de  
rspec­support  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  rack­protection  2.0.0  Uso  
de  rack­test  0.7.0  Uso  de  rspec­core  
3.6.0  Uso  de  rspec­
expectations  3.6.0  Uso  de  
rspec­mocks  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:

06­integration­specs/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:

06­integration­specs/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,  [2017­06­13T13: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:

06­integration­specs/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:

06­integration­specs/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'  =>  '2017­06­10'

}  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ó:

06­integration­specs/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:

06­integration­specs/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:

06­integration­specs/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ó:

06­integration­specs/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:

06­integration­specs/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  1­9.  ignorando  los  ejemplos  1­5     (0.45132  
segundos)
Ronda  2:  dividir  en  dos  los  ejemplos  que  no  fallan  6­9 .  ignorando  ejemplos  6­7     (0.43739  segundos)

Ronda  3:  dividir  en  dos  los  ejemplos  que  no  fallan  8­9 .  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 :

06­integration­specs/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:

06­integration­specs/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 :

06­integration­specs/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:

06­integration­specs/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:

06­integration­specs/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:

06­integration­specs/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:

06­integration­specs/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'  =>  '2017­06­10'))  resultado_2  =  
libro  mayor.record(gastos.merge('fecha'  =>  '2017­06­10'))  resultado_3  =  libro  
mayor.record(gastos.merge('fecha'  =>  '2017­06­11'))

esperar  (libro  mayor.  gastos_en  ('2017­06­10')).  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('2017­06­10')).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('2017­06­10')).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  ('2017­06­10')).  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:

06­integration­specs/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  
[2017­06­13  13:34:47]  INFO  WEBrick  1.3.1  [2017­06­13  
13:34:47]  INFO  ruby  2.4.1  (2017­03­22)  [x86_64­darwin15]
[2017­06­13  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/2017­06­10  ­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.  ­­exclude­dir=.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:

06­integration­specs/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":"2017­06­10"}'  ­w  
"\n" {"expense_id" :  
1}  $  curl  localhost:9292/gastos  ­­data  '{"beneficiario":"Starbucks",  "cantidad":7.5,     "fecha":"2017­06­10"}'  
­w  "\n" {"  id_gastos":2}  $  curl  
localhost:9292/gastos/
2017­06­10  ­w  "\n" [{"id":1,"beneficiario":"Zoológico","cantidad":10.0,"fecha":  
"2017­06­10"},{"id":2,"beneficiario":"St  arbucks","cantidad":7.5,"fecha":"2017­06­10"}]

¡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  Content­Type  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
Content­Type  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#Content­Type  
10.  http://www.rubydoc.info/gems/rack­test/0.6.3/Rack/Test/Session#get­instance_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  rspec­core,  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:

07­structuring­code­examples/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:

07­structuring­code­examples/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:

07­structuring­code­examples/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  rspec­rails  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:

07­ejemplos­de­código­de­estructuració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:

07­structuring­code­examples/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:

07­structuring­code­examples/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/rspec­rails/v/3­6/docs/controller­specs

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.

07­structuring­code­examples/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:

07­structuring­code­examples/01/getting_the_words_right.rb  
RSpec.describe  PhoneNumberParser,  'analiza  números  de  teléfono'  hazlo  
'en  formato  xxx­xxx­xxxx'  it  'en  
formato  (xxx)  xxx­xxxx'  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:

07­structuring­code­examples/01/getting_the_words_right.rb  
RSpec.describe  PhoneNumberParser,  'analiza  números  de  teléfono'  do     
ejemplo  'en  formato  xxx­xxx­xxxx'     ejemplo  
'en  formato  (xxx)  xxx­xxxx'
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

07­structuring­code­examples/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:

07­estructuración­código­ejemplos/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 :

07­ejemplos­de­código­de­estructuració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.

07­structuring­code­examples/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:

07­estructuración­código­ejemplos/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 :

07­ejemplos­de­código­de­estructuració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):

07­ejemplos­de­código­de­estructuració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:

07­structuring­code­examples/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 :

07­estructuración­código­ejemplos/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:

07­ejemplos­de­código­de­estructuració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:

07­structuring­code­examples/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.

07­structuring­code­examples/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:

07­estructuración­código­ejemplos/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/mystery­guest

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:

07­structuring­code­examples/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:

07­structuring­code­examples/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 :

07­estructuración­código­ejemplos/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:

07­structuring­code­examples/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:

07­estructuración­código­ejemplos/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:

07­structuring­code­examples/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:

07­structuring­code­examples/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

07­estructuración­código­ejemplos/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  clave­valor.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  clave­valor,  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:

07­estructuración­código­ejemplos/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/Key­value_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) :

07­structuring­code­examples/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:

07­estructuración­código­ejemplos/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:

07­structuring­code­examples/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:

07­structuring­code­examples/11/shared_examples/spec/include_examples_twice_spec.rb  
RSpec.describe  'Almacenes  de  clave­valor'  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  clave­valor
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:

07­structuring­code­examples/11/shared_examples/spec/it_behaves_like_twice_spec.rb  
RSpec.describe  'Almacenes  de  clave­valor'  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  clave­valor  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

07­estructuración­código­ejemplos/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 .

07­estructuración­código­ejemplos/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 :

07­estructuración­código­ejemplos/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

07­estructuración­código­ejemplos/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:

08­metadata/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  pretty­print)  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=2017­06­13  
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 :

08­metadata/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:

08­metadata/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:

08­metadata/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:

08­metadata/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:

08­metadata/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

08­metadata/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:

08­metadata/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

08­metadata/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:

08­metadata/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:

08­metadata/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:

08­metadata/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 :

08­metadata/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

08­metadata/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:

08­metadata/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:

06­integration­specs/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/rspec­core/RSpec/Core/Configuration.html#extend­instance_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:

08­metadata/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/rspec­core/RSpec/Core/Configuration.html#register_ordering­instance_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) :

08­metadata/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:

$  rspec­rbyebug

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/deivid­rodriguez/byebug  
2.  http://webapps­for­beginners.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.
­­siguiente­fracaso Aplique  ̀­­only­failures`  y  cancele  después  de  una  
falla.  (Equivalente  a  ̀­­only­failures  ­­fail­fast  ­­
order  
definido`)
­P,  ­­patrón  PATRÓN Cargue  archivos  que  coincidan  con  el  patrón  (predeterminado:  
"especificación/**/*_especificación.rb").
­­exclude­patró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ó  ­­only­failures  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

­­only­failures  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.

­­siguiente­fracaso

Una  forma  más  quirúrgica  de  ­­only­failures,  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.
­­deprecation­out  ARCHIVO Escriba  advertencias  de  obsolescencia  en  un  archivo  
  en  lugar  de  $stderr.
­b,  ­­backtrace  ­­ Habilite  el  rastreo  completo.
force­color,  ­­force­color  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.

­­dry­run  
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  ­­fail­fast  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).

./.rspec­local
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  ­­no­profile  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  rspec­print_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/rspec­3­book/rspec­print_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:

09­configuring­rspec/02/configuring_rspec/.rspec  
­I<%=  ENV['HOME']  %>/rspec­print_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/ruby­the­erb­templating­system

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  rspec­print_failures_eagerly/lib/rspec/print_failures_eagerly.rb  desde
su  directorio  de  inicio,  verá  la  siguiente  clase  de  Ruby:

09­configuración­rspec/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í:

09­configuring­rspec/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/rspec­core/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) :

09­configuring­rspec/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:

09­configuring­rspec/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/what­is­monkey­patching­in­ruby/

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 :

09­configuración­rspec/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:

09­configuración­rspec/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:

09­configuring­rspec/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:

09­configuring­rspec/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.

09­configuración­rspec/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 :

09­configuración­rspec/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  ­­only­failures  y  ­­next­failure .

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 :

09­configuración­rspec/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:

09­configuración­rspec/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:

09­configuración­rspec/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  rspec­core  para  ejecutar  sus  especificaciones,  rspec­
expectations  para  expresar  los  resultados  esperados  y  rspec­mocks  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  rspec­mocks  o  rspec­expectations  
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/fuubar­the­instafailing­rspec­progress­bar­formatter/  
8.  http://gofreerange.com/mocha/docs/

informar  fe  de  erratas  •  discutir
Machine Translated by Google

Capítulo  9.  Configuración  de  RSpec  •  162

09­configuring­rspec/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  rspec­mocks'  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  rspec­mocks:

09­configuring­rspec/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  rspec­mocks  están  disponibles  en  línea.11

Expectativas
Así  como  mock_with  le  permite  configurar  una  alternativa  a  rspec­mocks,  expect_with  le  permite  elegir  un  
marco  de  aserción  diferente  en  lugar  de  rspec­expectations:

9.  http://rr.github.io/rr/  
10.  https://github.com/doudou/flexmock  
11.  http://rspec.info/documentation/3.6/rspec­mocks/RSpec/Mocks/Configuration .html

informar  fe  de  erratas  •  discutir
Machine Translated by Google

RSpec.configure  •  163

09­configuring­rspec/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:  rspec­expectations,  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  Monkey­Patching  
La  primera  opción  que  nos  gustaría  destacar  aquí  es  disabled_monkey_patching!:

09­configuración­rspec/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:

09­configuring­rspec/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:

09­configuring­rspec/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  zero­monkey­patch  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:

09­configuración­rspec/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/the­plan­for­rspec­3/#zero­monkey­patching­mode

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:

09­configuración­rspec/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í:

09­configuración­rspec/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:

09­configuring­rspec/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/nyan­cat­formatter

informar  fe  de  erratas  •  discutir
Machine Translated by Google

Parte  IV

Expectativas  RSpec

Con  rspec­expectations,  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  rspec­core  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  rspec­expectations.  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:

10­exploring­rspec­expectations/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  rspec­expectations  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:

10­exploring­rspec­expectations/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 .

sujeto emparejador mensaje  de  error  personalizado

esperar( ).a  la  baraja.cartas.contar ecuación  (52), 'no  jugar  con  un  mazo  completo'

Veamos  cómo  funcionan  estas  partes  probándolas  en  una  sesión  de  IRB:

$  irb

Para  usar  rspec­expectations,  debe  solicitar  la  biblioteca  y  luego  incluir  el  módulo  RSpec::Matchers .  
Normalmente,  rspec­core  hace  esto  por  usted,  pero  cuando  usa  rspec­expectations  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/rspecs­new­expectation­syntax/

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:

10­exploring­rspec­expectations/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:

10­exploring­rspec­expectations/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:

10­exploring­rspec­expectations/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/rspec­expectations/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:

10­exploring­rspec­expectations/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:

10­exploring­rspec­expectations/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:

10­exploring­rspec­expectations/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:

10­exploring­rspec­expectations/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)  { ... }:

10­exploring­rspec­expectations/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:

10­exploring­rspec­expectations/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:

10­exploring­rspec­expectations/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  rspec­expectations  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.

11­matchers­included­in­rspec­expectations/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é:

11­matchers­incluidos­en­rspec­expectations/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:

11­matchers­included­in­rspec­expectations/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:

11­matchers­included­in  ­  rspec­expectations/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:

11­matchers­included­in­rspec­expectations/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

11­matchers­included­in­rspec­expectations/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:

11­matchers­included­in­rspec­expectations/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:

11­matchers­included­in­rspec­expectations/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:

11­matchers­included­in­rspec­expectations/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:

11­matchers­included­in­rspec­expectations/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:

11­matchers­included­in­rspec­expectations/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/video­clips/63ite2/the­colbert­report­the­word­­­veracidad

informar  fe  de  erratas  •  discutir
Machine Translated by Google

Capítulo  11.  Comparadores  incluidos  en  las  expectativas  de  RSpec  •  192

11­matchers­included­in­rspec­expectations/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:

11­matchers­included­in­rspec­expectations/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:

11­coincidencias­incluidas­en­rspec­expectations/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  IEEE­754,  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 :

11­matchers­included­in­rspec­expectations/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:

11­matchers­included­in­rspec­expectations/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 :

11­matchers­included­in­rspec­expectations/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:

11­matchers­included­in­rspec­expectations/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:

11­matchers­included­in­rspec­expectations/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_:

11­matchers­included­in­rspec­expectations/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:

11­matchers­included­in­rspec­expectations/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:

11­matchers­included­in­rspec­expectations/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:

11­matchers­included­in­rspec­expectations/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:

11­matchers­included­in­rspec­expectations/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://ruby­doc.org/core­2.4.1/Symbol.html#method­i­to_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:

11­matchers­included­in­rspec­expectations/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  clave­valor  (pasado  como  un  
hash):

11­matchers­included­in­rspec­expectations/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  clave­valor:

11­matchers­included­in­rspec­expectations/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:

11­matchers­included­in­rspec­expectations/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

11­matchers­included­in­rspec­expectations/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:

11­matchers­included­in­rspec­expectations/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://ruby­doc.org/core­2.4.1/doc/syntax/calling_methods_rdoc.html#label­Array+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:

11­matchers­included­in­rspec­expectations/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:

11­matchers­included­in­rspec­expectations/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:

11­matchers­included­in­rspec­expectations/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

11­matchers­included­in­rspec­expectations/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:

11­matchers­included­in­rspec­expectations/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:

11­matchers­included­in­rspec­expectations/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:

11­matchers­included­in­rspec­expectations/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:

11­matchers­included­in­rspec­expectations/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 :

11­matchers­included­in­rspec­expectations/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í:

11­matchers­included­in­rspec­expectations/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:

11­matchers­included­in­rspec­expectations/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:

11­matchers­included­in­rspec­expectations/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 :

11­matchers­included­in­rspec­expectations/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í:

11­matchers­included­in­rspec­expectations/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/rspec­mocks/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:

11­matchers­included­in­rspec­expectations/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 :

11­matchers­included­in­rspec­expectations/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:

11­matchers­included­in­rspec­expectations/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:

11­matchers­included­in­rspec­expectations/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.

11­matchers­included­in­rspec­expectations/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 :
11­matchers­included­in­rspec­expectations/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:

11­matchers­included­in­rspec­expectations/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:

11­matchers­included­in­rspec­expectations/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:

11­matchers­included­in­rspec­expectations/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:

11­matchers­included­in­rspec­expectations/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):

11­matchers­included­in­rspec­expectations/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:

11­matchers­included­in­rspec­expectations/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:

11­comparadores­incluidos­en­rspec­expectations/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 :

11­comparadores­incluidos­en­rspec­expectations/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:

11­matchers­included­in­rspec­expectations/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:

11­matchers­included­in­rspec­expectations/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:

11­matchers­included­in­rspec­expectations/exercises/phone_number_extractor_spec.rb  
RSpec.describe  PhoneNumberExtractor  do
dejar(:texto)  hacer
<<­EOS
Melinda:  (202)  555­0168
Bob:  202­555­0199
Sabina:  (202)  555­0176
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)  
555­0168',  
'202­555­0199',  
'(202)  555­0175'

]  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:

11­matchers­included­in­rspec­expectations/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:

11­matchers­included­in­rspec­expectations/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 :

11­matchers­included­in­rspec­expectations/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:

11­matchers­included­in­rspec­expectations/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:

11­matchers­included­in­rspec­expectations/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

11­matchers­included­in­rspec­expectations/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:

11­matchers­included­in­rspec­expectations/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:

11­matchers­included­in­rspec­expectations/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:

11­matchers­included­in­rspec­expectations/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:

11­matchers­included­in­rspec­expectations/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:

12­creating­custom­matchers/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:

12­creating­custom­matchers/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:

12­creating­custom­matchers/02/expense_tracker/spec/integration/app/ledger_spec.rb  
expect(ledger.expenses_on('2017­06­10')).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

12­creating­custom­matchers/03/expense_tracker/spec/integration/app/ledger_spec.rb  
expect(ledger.expenses_on('2017­06­10')).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:

12­creating­custom­matchers/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:

12­creating­custom­matchers/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:

12­creating­custom­matchers/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):

12­creating­custom­matchers/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í:

12­creating­custom­matchers/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/2014­09­29­farewell­json­api­gems.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:

12­creating­custom­matchers/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í:

12­creating­custom­matchers/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:

12­creating­custom­matchers/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:

12­creating­custom­matchers/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:

12­creating­custom­matchers/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:

12­creating­custom­matchers/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/rspec­3­book/book­code/blob/v1.0/12­creating­custom­matchers/06/custom_matcher/lib/
cuenta.rb
3.  http://rspec.info/documentation/3.6/rspec­expectations/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:

12­creating­custom­matchers/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 :

12­creating­custom­matchers/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

12­creating­custom­matchers/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:

12­creating­custom­matchers/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:  2017­06­12  ((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:

12­creating­custom­matchers/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...

12­creating­custom­matchers/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:

12­creating­custom­matchers/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?):

12­creating­custom­matchers/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  == :

12­creating­custom­matchers/09/custom_matcher/spec/support/matchers.rb  
cuenta_saldo(cuenta)  ==  cuenta

En  su  lugar,  necesitaremos  usar  el  valor_coincidencia  de  RSpec.  método:

12­creating­custom­matchers/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:

12­creating­custom­matchers/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:

12­creating­custom­matchers/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/rspec­expectations/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/rspec­expectations/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:

12­creating­custom­matchers/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á:

12­creating­custom­matchers/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/shoulda­matchers

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:

12­creating­custom­matchers/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:

12­creating­custom­matchers/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  rspec­mocks,  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  rspec­mocks  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  rspec­mocks,  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  rspec­mocks  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/rspec­mocks/v/3­6/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:

13­comprensión­prueba­dobles/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:

13­comprensión­prueba­dobles/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/mockists­are­dead­long­live­classicists

informar  fe  de  erratas  •  discutir
Machine Translated by Google

Capítulo  13.  Comprensión  de  los  dobles  de  prueba  •  244

13­comprensión­prueba­dobles/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:

13­comprensión­prueba­dobles/03/stubbed_constants.rb  
class  PasswordHash
COSTO_FACTOR  =  12

#...
fin

…sus  especificaciones  pueden  redefinirlo  a  1:

13­comprensión­prueba­dobles/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:

13­comprensión­prueba­dobles/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:

13­comprensión­prueba­dobles/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:

13­understanding­test­doubles/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

13­comprensión­prueba­dobles/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  
rspec­mocks.  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:

13­comprensión­test­doubles/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  rspec­mocks.  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:

14­customizing­test­doubles/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/rspec­mocks/v/3­6/docs/configuring­responses

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

14­customizing­test­doubles/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:

14­customizing­test­doubles/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.

14­customizing­test­doubles/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://ruby­doc.org/core­2.4.1/Kernel.html#method­i­raise

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:

14­customizing­test­doubles/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:

14­customizing­test­doubles/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:

14­personalización­prueba­dobles/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:

14­customizing­test­doubles/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:

14­customizing­test­doubles/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:

14­customizing­test­doubles/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:

14­customizing­test­doubles/02/setting_constraints.rb  
cart.add_product('Hoodie')  
cart.add_product('Hoodie',  27182818)  
cart.add_product('Hoodie',  27182818,  'HOODIE­SERIAL­123')

La  contraparte  de  any_args  es  no_args:

14­customizing­test­doubles/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:

14­customizing­test­doubles/02/setting_constraints.rb  
class  BoxOffice
def  find_showtime(opciones)  # ...

fin
fin

4.  https://relishapp.com/rspec/rspec­mocks/v/3­6/docs/setting­constraints/matching­arguments

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:

14­customizing­test­doubles/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:

14­customizing­test­doubles/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:

14­customizing­test­doubles/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:

14­customizing­test­doubles/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,  rspec­mocks  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:

14­customizing­test­doubles/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:

14­customizing­test­doubles/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:

14­customizing­test­doubles/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:

14­customizing­test­doubles/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 :

14­customizing­test­doubles/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:

14­customizing­test­doubles/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:

14­customizing­test­doubles/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  rspec­mocks  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:

15­usando­prueba­dobles­efectivamente/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:

15­usando­prueba­dobles­efectivamente/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:

15­usando­prueba­dobles­efectivamente/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:

15­usando­prueba­dobles­efectivamente/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/introducing­rspec­smells/#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:

15­usando­prueba­dobles­efectivamente/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 :

15­usando­prueba­dobles­efectivamente/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:

15­usando­prueba­dobles­efectivamente/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:

15­using­test­doubles­effectly/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:

15­usando­prueba­dobles­efectivamente/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

15­usando­prueba­dobles­efectivamente/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:

15­usando­prueba­dobles­efectivamente/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:

15­usando­prueba­dobles­efectivamente/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:

15­usando­prueba­dobles­efectivamente/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:

15­usando­prueba­dobles­efectivamente/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:

15­usando­prueba­dobles­efectivamente/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/rspec­mocks/v/3­6/docs/verifying­doubles/partial­doubles

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:

15­usando­prueba­dobles­efectivamente/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

15­usando­prueba­dobles­efectivamente/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 :

15­usando­prueba­dobles­efectivamente/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:

15­usando­prueba­dobles­efectivamente/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:

15­using­test­doubles­effectly/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:

15­usando­prueba­dobles­efectivamente/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:

15­using­test­doubles­effectly/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  
Object­Oriented  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:

15­usando­prueba­dobles­efectivamente/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:

15­usando­prueba­dobles­efectivamente/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:

15­usando­prueba­dobles­efectivamente/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:

15­using­test­doubles­effectly/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:

15­usando­prueba­dobles­efectivamente/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:

15­using­test­doubles­effectly/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/how­testability­can­help

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  dry­auto_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://dry­rb.org/gems/dry­auto_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

15­usando­prueba­dobles­efectivamente/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í:

15­usando­prueba­dobles­efectivamente/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  rspec­mocks,  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/3­0­3/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", ...].
15­usando­prueba­dobles­efectivamente/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  'Content­Type:  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:

15­usando­prueba­dobles­efectivamente/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:

15­usando­prueba­dobles­efectivamente/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  [ 'Content­Type:  
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:

15­usando­prueba­dobles­efectivamente/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:

15­usando­prueba­dobles­efectivamente/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/taxjar­ruby

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:

15­usando­prueba­dobles­efectivamente/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 :

15­usando­prueba­dobles­efectivamente/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/test­isolation­is­about­avoiding­mocks

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:

15­usando­prueba­dobles­efectivamente/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 :

15­usando­prueba­dobles­efectivamente/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á:

15­usando­prueba­dobles­efectivamente/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:

15­usando­prueba­dobles­efectivamente/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:

15­usando­prueba­dobles­efectivamente/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/contributing­tests/wiki/Test­Double  
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/rspec­3­book/book­code/blob/v1.0/15­using­test­doubles­effectly/solutions/guessing_game/
spec/adivinanzas_juego_spec.rb
20.  https://github.com/rspec­3­book/book­code/blob/v1.0/15­using­test­doubles­effectly/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/dev­blog/2012/12/5­reasons­to­avoid­bundler­require

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  rspec­core  se  envía  con  una  tarea  Rake  fácil  de  configurar.  Para  habilitarlo,  agregue  las  siguientes  
dos  líneas  al  Rakefile  de  su  proyecto:

A1­rspec­and­wider­ecosystem/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:

A1­rspec­and­wider­ecosystem/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:

A1­rspec­and­wider­ecosystem/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  rspec­expectations,  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:

A1­rspec­and­wider­ecosystem/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:

A1­rspec­and­wider­ecosystem/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  rspec­expectations  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  
rspec­mocks  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  rspec­rails  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:

A2­using­rspec­with­rails/01/rails_app/
Gemfile  group :desarrollo, :test  do  
  gem  'rspec­rails',  '~>  3.6'
fin

1.  https://pragprog.com/book/nrtest3/rails­5­test­prescriptions

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  rspec­rails.  Finalmente,  puede  
configurar  su  proyecto  para  usar  rspec­rails  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  rspec­rails  
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  rspec­rails,  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  rspec­rieles
Una  vez  que  rspec­rails  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/Understanding­binstubs  
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

A2­using­rspec­with­rails/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  rspec­rails  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  rspec­rails  3.6+  lo  admite).

Hoja  de  referencia  de  tipos  de  especificaciones

Tipo  de  especificación  Usar  para... lo  que  proporciona notas

• •  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

:pedido • No  Java • Solicitar  ayudantes  como  get  '/index',  post •  Utiliza  los  rieles

Interacciones  de   '/crear' enrutador  y

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

Tipo  de  especificación  Usar  para... Lo  que  proporciona  •   notas


:controlador •  Prueba  de   Solicitar  ayudantes  como  get :index,  post •  Bastidor  de  puentes
controladores  en :crear software  intermedio

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/rspec­rails/v/3­6/docs/controller­specs/anonymous­controller
6.  https://relishapp.com/rspec/rspec­rails/v/3­6/docs/controller­specs/render­views

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,  rspec­rails  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.

emparejador pasa  si… Disponible  en…

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'

ser_nuevo_registro registro.nuevo_registro? Todos  los  tipos  de  especificaciones

Sé  valido record.valid? Todos  los  tipos  de  especificaciones

have_been_enqueued.with(some_args) El  trabajo  se  puso  en  cola  con   Todos  los  tipos  de  especificaciones

argumentos  coincidentes

have_enqueued_job(job_class).with(some_args), El  código  en  un  bloque  expect  puso   Todos  los  tipos  de  especificaciones

enqueue_job(job_class).with(some_args) en  cola  un  trabajo  job_class

con  argumentos  coincidentes

have_http_status(código) respuesta.estado  ==  código Todos  los  tipos  de  especificaciones

have_http_status(símbolo) response.status  se  asigna  a  un  tipo   Todos  los  tipos  de  especificaciones

de  respuesta,  como
:éxito

redirect_to('http://alguna­url.ejemplo.com') La  respuesta  redirige  a :request, :controller

URL  especificada

ser_enrutable La  ruta  existe :controlador, :enrutamiento

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/setting­up­rspec­and­capybara­in­rails­5­for­testing/

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/changes­to­test­controllers­in­rails­5.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)

eql(x) a.eql?(x) an_object_eql_to(x)

igual(x)  a.igual?(x) ser(x)

un_objeto_igual_a(x)

1.  https://github.com/rspec/rspec­expectations

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

be_nil ¿añil? a_nil_value

Tipos
emparejador pasa  si… Alias  disponibles

be_an_instance_of(klass)  a.class  ==  klass ser_instancia_de(clase)
una_instancia_de(clase)

be_a_kind_of(clase) a.is_a?(clase) be_a(clase)


be_kind_of(clase)
a_kind_of(clase)

Comparaciones  de  operadores

Matcher  Passes  si...  Alias  disponibles
ser  ==  x un  ==  x un_valor  ==  x

ser  <x un  <  x un_valor  <  x

ser  >  x a  >  x valor_a  >  x

ser  <=  x un  <=  x un_valor  <=  x

ser  >=  x un  >  =  x un_valor  >=  x

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

estar_dentro(0.1).de(x)   (a  ­  x).abs  <=  0.1 un_valor_dentro  de  (0.1).de  (x)

estar_dentro(5).porcentaje_de(x)   (a  ­  x).abs  <=  (0.05  *  x) un_valor_dentro  de  (5).porcentaje_de(x)

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)

empezar_con(x,  y) a[0]  ==  x  &&  a[1]  ==  y una_colección_que_comienza_con(x,  y)

una_cadena_que_comienza_con(x,  y)

fin_con(x,  y) a[­1]  ==  x  &&  a[­2]  ==  y una_colección_que_comienza_con(x,  y)

una_cadena_que_comienza_con(x,  y)

incluir(x,  y) (a.incluir?(x)  &&  a.incluir?(y)) una_colección_incluyendo(x,  y)

||  (una.clave?(x)  &&  una.clave?(y)) una_cadena_incluyendo(x,  y)

a_hash_incluido(x,  y)

incluir  (w:  x,  y:  z) a[:w]  == :x  &&  a[:y]  == :z  a.all?  { |e| a_hash_incluido(w:  x,  y:  z)

todos  (emparejador)

matcher.coincidencias?(e) }

partido  (x:  emparejador,  y:  3) matcher.matches?(a[:x])  &&  an_object_matching(x:  matcher,  y:  3)

a[:y]  ==  3

partido  ([3,  emparejador]) a[0]  ==  3  &&   an_object_matching([3,  matcher])

comparador.coincidencias?(a[1])

coincidencia("patrón")   a.match("patrón")   a_string_matching("patrón")

coincidencia(/regex/) a.match(/regex/) coincidir_regex(/regex/)

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)

responder_a(:x, :y) a.responder_a?(:x)  &&   un_objeto_respondiendo_a(:x, :y)

a.responder_a?(:y)

responder_a(:x) a.responder_a?(:x)  && un_objeto_respondiendo_a(:x)

.con(2).argumentos a.método(:x).aridad  ==  2 .con(2).argumentos

Predicados  dinámicos
emparejador pasa  si… Alias  disponibles

ser_xyz a.xyz?  ||  a.xyzs? be_a_xyz

be_an_xyz

be_foo(x,  y,  &b) a.foo(x,  y,  &b)?  ||  a.foos(x,  y,  &b)? be_a_foo(x,  y,  &b)

be_an_foo(x,  y,  &b)

tener_xyz a.has_xyz?

have_foo(x,  y,  &b)  a.has_foo(x,  y,  &b)?

Comparadores  adicionales

emparejador pasa  si… Alias  disponibles

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

También podría gustarte