Convenciones sobre la estructura de directorios en RSpec y Minitest

Escrito el 11 de Julio de 2015

Al crear un nuevo proyecto en Ruby, la estructura básica es poner el código en un directorio llamado lib. Además, podemos agregar un directorio test o spec para nuestros archivos de prueba. No es obligatorio, pero es una convención que deberíamos seguir, no solo porque nuestra estructura de directorios será más intuitiva, sino también porque algunos frameworks de testing así lo asumen.

Si queremos usar Rspec, el directorio se debe llamar spec. Si queremos usar Minitest, podemos nombrarlo de cualquier forma, pero es común nombrarlo test o también spec.

RSpec

Veamos un ejemplo básico con RSpec.

Estructura de directorios:

fizz_buzz_rspec
├── lib
│   └── fizz_buzz.rb
└── spec
    └── fizz_buzz_spec.rb

El contenido de los archivos:

#spec/fizz_buzz_spec.rb
require 'fizz_buzz'

describe FizzBuzz do
  it 'returns "1" when receives 1' do
    expect(FizzBuzz.new.convert(1).to eq '1'
  end
end

#lib/fizz_buzz.rb
class FizzBuzz
  def convert(n)
    '1'
  end
end

Es sólo la primera prueba para un proyecto fizz buzz.

Ahora, simplemente podemos ejecutar rspec:

[fizz_buzz_rspec]$ rspec
.

Finished in 0.00151 seconds (files took 0.1615 seconds to load)
1 example, 0 failures

Estoy empezando con una prueba en verde porque el resultado no es tan verboso como si la prueba fallara, pero ya sabemos que debemos empezar una prueba en rojo, ¿cierto? ¿Cierto? :)

Como vemos, no necesitamos ninguna configuración extra. En nuestro archivo fizz_buzz_spec requerimos el archivo fizz_buzz y nada más. De hecho, no necesitamos requerir la biblioteca rspec y no necesitamos especificar dónde están nuestros archivos de prueba. Simplemente ejecutamos el comando rspec desde la línea de comandos dentro de nuestro proyecto y Rspec se encargó del resto.

¿Qué hubiera pasado si nuestro directorio spec fuera nombrado de manera distinta? Digamos

fizz_buzz_rspec
├── lib
│   └── fizz_buzz.rb
└── my_testing_directory
    └── fizz_buzz_spec.rb

Bien, en test caso, si ejecutamos el comando rspec, no se encontrará ninguna prueba para ejecutar.

[fizz_buzz_rspec]$ rspec
No examples found.


Finished in 0.00031 seconds (files took 0.07711 seconds to load)
0 examples, 0 failures

Debemos ejecutar el comando indicando el directorio.

[fizz_buzz_rspec]$ rspec my_testing_directory
.

Finished in 0.00151 seconds (files took 0.1615 seconds to load)
1 example, 0 failures

Además, es importante seguir la convención de nombres del archivo spec. RSpec espera que todos los archivos de pruebas termien con _spec. En nuestro caso, fizz_buzz_spec.rb. Si lo renombramos, RSpec no lo ejecutará. Veamos:

fizz_buzz_rspec
├── lib
│   └── fizz_buzz.rb
└── spec
    └── fizz_buzz_testing_file.rb

Intentemos ejecutar:

[fizz_buzz_rspec]$ rspec
No examples found.


Finished in 0.00031 seconds (files took 0.07711 seconds to load)
0 examples, 0 failures

De nuevo, RSpec no cargó nuestro archivo porque no sigue la convención de nombrado. Si lo renombramos de nuevo fizz_buzz_spec.rb, funcionará.

Si observamos el archivo de pruebas, podemos ver que no necesitamos ninguna configuración especial para cargar el archivo fizz_buzz.rb, además de requerirlo claro está. RSpec asume que nuestro código está en el directorio lib y por eso es que puede carglo correctamente. Intentemos renombrar el directorio lib y veamos qué pasa.

fizz_buzz_rspec
├── main
│   └── fizz_buzz.rb
└── spec
    └── fizz_buzz_spec.rb

Si tratamos de ejecutar rspec.

[fizz_buzz_rspec]$ rspec
[...]core_ext/kernel_require.rb:54:in `require': cannot load such file -- fizz_buzz (LoadError)
...
[...]spec/fizz_buzz_spec.rb:1:in `<top (required)>'
...

Falla por el require (la primera línea del archivo de pruebas). No encuentra un archivo fizz_buzz.rb, que presupone en el directorio lib (que nosotros acabamos de renombrar). Así que, lo renombramos de nuevo lib y todo funciona de nuevo.

Como podemos ver, es mejor apegargse a las convenciones cuando usamos RSpec.

spec_helper

Es común usar un archivo spec_helper. Cuando nuestro proyecto crece, necesitamos incluir más bibliotecas de pruebas, o agregar más tareas para hacer antes o después de ejecutar la prueba. Por ejemplo, podríamos querer incluir shoulda, o limpiar la base de datos. Estas tareas afectan muchos archivos de pruebas, así que, es mejor tenerlas en su propio archivo e incluirlo en los otros archivos. Nuestro ejemplo no necesita nada más, pero quiero mostrar cómo sería si lo necesitase.

Digamos que queremos usar la gema rspec-given. Después de instarlar, necesitamos agregar esta línea al archivo fizz_buzz_spec.rb:

#spec/fizz_buzz_spec.rb
require 'rspec/given'

En nuestro proyecto sólo tenemos un archivo, pero si tuviéramos más, tendríamos que agregar esta línea a cada archivo de pruebas. Para evitar eso, podemos unar un spec_helper.

Primero necesitamos crear un archivo spec_helper.rb en nuestro directorio spec.

fizz_buzz_rspec
├── lib
│   └── fizz_buzz.rb
└── spec
    ├── fizz_buzz_spec.rb
    └── spec_helper.rb

Ahora necesitamos cambiar el archivo de pruebas.

#spec/fizz_buzz_spec.rb
require 'spec_helper'
require 'fizz_buzz'

describe FizzBuzz do
  Given(:fizz_buzz) { FizzBuzz.new }
  When(:result) { fizz_buzz.convert(1) }
  Then { result == '1' }
end

Si lo ejecutamos ahora, se ejecuta correctamente. No importa dónde está nuestro archivo de pruebas (por ejemplo, podría estar en spec/models/inbox/fizz_buzz_spec.rb), siempre podrá requerir el archivo spec_helper con require 'spec_helper'.

En proyectos más grandes, la ventaja de un archivo spec_helper es más notorio. Veamos un archivo spec_helper de Rails, sólo como ejemplo:

#rails_project/spec/spec_helper.rb
ENV["RAILS_ENV"] ||= 'test'
require File.expand_path("../../config/environment", __FILE__)
require 'rspec/rails'
require 'shoulda-matchers'
require 'rspec/autorun'

RSpec.configure do |config|
  config.use_transactional_fixtures = true
  config.order = "random"
end

Carga todas las bibliotecas que necesita para trabajar y configura RSpec para usar fixtures transaccionales y ejecutar las pruebas en orden aleatorio.

Nota que en esta ocasión, spec_helper es sólo un archivo más, puede ser nombrado de diferente manera y seguirá funcionando siempre y cuando sea requerido con el nombre correcto.

Minitest

Minitest nos permite nombrar nuestros directorios de la forma que queramos, pero necesitamos tenerlo en cuenta porque es importante cuando ejecutemos nuestras pruebas.

Sólo para seguir las convenciones, creé el proyecto con una estructura muy similar a la de RSpec.

fizz_buzz_minitest
├── lib
│   └── fizz_buzz.rb
└── test
    └── fizz_buzz_test.rb

El contenido de los archivos:

#test/fizz_buzz_test.rb
require 'minitest/autorun'
require 'fizz_buzz'

describe FizzBuzz do
  it 'returns "1" when receives 1' do
    assert '1', FizzBuzz.new.convert(1)
  end
end

#lib/fizz_buzz.rb
class FizzBuzz
  def convert(n)
    '1'
  end
end

Lo primero que me gustaría señalar es que en Minitest es obligatorio requerir la bibilioteca Minitest, específicamente minitest/autorun, que provee de todo lo que necesitamos para ejecutar la prueba (por ejemplo, los métodos de aserciones y la capacidad de que el archivo se ejecute automáticamente). Nota que estoy usando la sintaxis spec aquí, pero también es posible usar la clásica sintaxis de minitest. Puedes ver un poco de las bases en el post de la Kata Minitest: Rompiendo la parálisis inicial.

Ahora, tristemente, no tenemos un comando minitest para ejecutar, como lo teníamos con RSpec. Minitest is un framework de pruebas muy básico (pero muy poderoso). Una de sus fortalezas es que que no es un DSL como RSpec, sino Ruby puro (aunque por la sintaxis que estamos usando nostros, sí hacemos uso de un DSL). De cualquier forma, para ejecutar nuestras pruebas, solo necesitamos ejecutar Ruby.

[fizz_buzz_minitest]$ ruby test/fizz_buzz_test.rb
[...]core_ext/kernel_require.rb:54:in `require': cannot load such file -- fizz_buzz (LoadError)
...
        from test/fizz_buzz_test.rb:1:in `<main>'

Humm, al parecer, sí ejecutó algo, pero no funcionó correctamente. Lo que pasó aquí es que no fue posible cargar el archivo fizz_buzz. Afortunadamente, es algo fácil de solucionar. Ruby incluye require_relative para requerir un archivo especificando una ruta relativa. Esta ruta debe ser construida con base en la ruta del archivo actual. En nuestro caso, nuestro archivo de pruebas está dentro de un directorio test, así que tenemos que indicar que nos moveremos un directorio atrás (esto se hace con dos puntos ..) para llegar al directorio base del proyecto y entonces agregar la ruta al archivo requerido.

Nuestro archivo de prueba ahora se ve de esta manera:

#test/fizz_buzz_test.rb
require 'minitest/autorun'
require_relative '../lib/fizz_buzz'

describe FizzBuzz do
  it 'returns "1" when receives 1' do
    assert '1', FizzBuzz.new.convert(1)
  end
end

Podemos ejecutar nuestras pruebas de nuevo.

[fizz_buzz_minitest]$ ruby test/fizz_buzz_test.rb
Run options: --seed 21470

# Running:

.

Finished in 0.000718s, 1393.3787 runs/s, 1393.3787 assertions/s.

1 runs, 1 assertions, 0 failures, 0 errors, 0 skips

Ahora funciona. Nota que el comando se debe ejecutar desde el directorio base del proyecto, no desde lib o test.

Sin embargo, hay un pequeño problema con esta aproximación. Si reordenamos la estructura de directorios, ya sea test o lib, necesitaremos cambiar también las rutas relativas que hayamos utilizado. Por ejemplo:

fizz_buzz_minitest
├── lib
│   └── models
│       └── fizz_buzz.rb
└── test
    └── models
        └── fizz_buzz_test.rb

Entonces necesitamos cambiar la ruta relativa a:

require_relative '../../lib/models/fizz_buzz'

En nuestro ejemplo, tenemos sólo un archivo, pero en un proyecto real, esto sería un gran problema.

Afortunadamente, tenemos una alternativa. Podemos ejecutar nuestra archivo de pruebas especificando en la línea de comandos dónde debe Ruby buscar los archivos requeridos. Es decir, desde la línea de comandos podemos indicar que directorios deben ser precargados. Con nuestra estructua inicial, podemos ejecutar la prueba de esta manera:

[fizz_buzz_minitest]$ ruby -I lib test/fizz_buzz_test.rb
Run options: --seed 2633

# Running:

.

Finished in 0.000820s, 1220.1178 runs/s, 1220.1178 assertions/s.

1 runs, 1 assertions, 0 failures, 0 errors, 0 skips

Usamos la opción -I, indicando que Ruby debe cargar de inicio el directorio lib.

Nota que con minitest, no importa cómo se nombran los directorios, simplemente funciona. Ejemplo:

fizz_buzz_minitest
├── my_own_directory
│   └── fizz_buzz.rb
└── my_testing_directory
    └── fizz_buzz_test.rb

Ejecutamos la prueba:

[fizz_buzz_minitest]$ ruby -I my_own_directory test/fizz_buzz_testing_file.rb
Run options: --seed 31542

# Running:

.

Finished in 0.000719s, 1389.9584 runs/s, 1389.9584 assertions/s.

1 runs, 1 assertions, 0 failures, 0 errors, 0 skips

Todo funciona. Esto es porque no estamos ejecutando el conjunto completo de pruebas, como lo hacemos con RSpec. En este aso, estamos sólo ejecutando un archivo y especificamos qué directorio debe ser cargado.

test_helper

De la misma manera que usamos un archivo spec_helper, podemos usar un test_helper en minitest. Nota que el nombre del archivo no importa, pero test_helper es un nombre ampliamente usado cuando se trabaja con minitest.

Podemos crear un archivo test_helper.rb en nuestro directorio test.

fizz_buzz_minitest
├── lib
│   └── fizz_buzz.rb
└── test
    ├── fizz_buzz_test.rb
    └── test_helper.rb

El contenido de nuestro test_helper será un poco mejor que el spec_helper, porque ahora podemos agregar un poco de código al menos.

require 'minitest/autorun'
require 'minitest/pride'

Extrajimos el rquire del autorun de Minitest al test_helper, y además requerimos pride, una pequeña biblioteca de Minitest que colorea la salida.

Ahora necesitamos cambiar el require de nuestro archivo de pruebas.

#spec/fizz_buzz_test.rb
require 'test_helper'
require 'fizz_buzz'

describe FizzBuzz do
  it 'returns "1" when receives 1' do
    assert '1', FizzBuzz.new.convert(1)
  end
end

Y tratamos de ejecutarlo:

[fizz_buzz_minitest]$ ruby -I lib test/fizz_buzz_test.rb

[...]core_ext/kernel_require.rb:54:in `require': cannot load such file -- test_helper (LoadError)
...
        from spec/fizz_buzz_test.rb:1:in `<main>'

Ups, algo falló.

La razón es que el archivo requerido no pudo se cargado. Indicamos a Ruby que debía cargar el directorio lib, pero test_helper no está ahí, sino en el directorio test. Bien, para este caso, podemos usar require_relative, pero podemos tener el mismo problema potencial: si movemos los archivos a otros directorios, todos las rutas pasadas a require_relative deben ser actualizadas también.

De nuevo, la mejor opción es especificar que Ruby debe cargar otro directorio cuando ejecute nuestro archivo de prueba. Podemos hacerlo agregando el directorio test a la lista (separado por dos puntos :).

[fizz_buzz_minitest]$ ruby -I lib:test test/fizz_buzz_test.rb
Run options: --seed 64180

# Running:

.

Fabulous run in 0.000797s, 1254.2865 runs/s, 1254.2865 assertions/s.

1 runs, 1 assertions, 0 failures, 0 errors, 0 skips

Y… todo funciona de nuevo.

Como podemos ver, Minitest no asume una estructura de directorios específica porque podemos indicar sus dependencias como opciones en línea de comandos. Sin embargo, se recomienda usar la estructura que hemos visto aquí porque es una muy difundida. Cualquier desarrollador podría imaginar dónde encontrar qué archivos y es fácil de crecer si queremos usar rake (por ejemplo) para ejecutar nuestras pruebas en lotes. Aunque esto último es un tema que dejaremos para un siguiente post.

comments powered by Disqus