Uno de los aspectos más importantes de un sistema de integración contínua es la velocidad con la que se ejecutan las pruebas automáticas, pues cuanto mayor sea ese tiempo de ejecución más se tardará en ejecutar el resto de acciones asociadas: detección de fallos, despliegues o mezcla de ramas.
Por otra parte, por todos es conocido que aquellos test que interactúan con un navegador web simulando las acciones del usuario son los tests que más tardan en ejecutarse, dándose casos en los que este proceso se demora más de una hora.
En esta breve guía vamos a indicar cómo instalar y configurar nuestro proyecto la gema Knapsack, que nos va a permitir ejecutar nuestros tests en paralelo sin mucho esfuerzo.
Esta gema se encarga de dividir la ejecución de los tests (trabaja a nivel de archivo *_spec.rb
) en base a dos parámetros: el número de jobs que vamos a ejecutar y el número de job actual. Adicionalmente trabaja con un archivo knapsack_rspec_report.json
que contiene la duración en segundos de cada uno de los archivos de tests (generado previamente como veremos a continuación), que ayuda al algoritmo de reparto a dividir la carga de una forma más eficiente. Con esos dos datos más el archivo de la duración, la gema selecciona los tests que va a ejecutar en el job actual, y esa selección es siempre la misma con esos parámetros de entrada. De esta forma cada job es autónomo a la hora de repartirse los tests que debe ejecutar y todos quedan repartidos entre todos los jobs de forma automática.
La instalación de esta gema se realiza de la forma habitual. Editamos nuestro Gemfile:
group :test, :development do
gem 'knapsack'
end
Después instalamos las dependencias:
bundle install
Modificamos el archivo Rakefile
para que cargue las nuevas tareas de la gema:
Knapsack.load_tasks if defined?(Knapsack)
Y cargamos el adaptador de RSpec editando el archivo spec/spec_helper.rb
:
require 'knapsack'
Knapsack::Adapters::RSpecAdapter.bind
La primera vez que ejecutamos Knapsack no sabe la duración de cada uno de nuestros archivos de tests, por lo que tendremos que ejecutarlo en modo reporte para que genere un archivo con la duración de cada archivo que usará posteriormente para dividir los tests entre los distintos nodos. Para generar ese archivo, ejecutaremos el siguiente comando:
KNAPSACK_GENERATE_REPORT=true bundle exec rspec spec/features
Esto generará un archivo de nombre knapsack_rspec_report.json
que deberemos commitear en nuestro repositorio.
Es bastante aconsejable que la ejecución se haga en una máquina lo más parecida posible a las cuales posteriormente se usarán para ejecutar las pruebas. Si se hace en local, puede ser que los tiempos no coincidan con los que posteriormente tenga en el servidor de CI y por tanto el reparto y la duración de los tests se vea afectado.
Las siguientes veces, como ya tenemos generado el archivo, ejecutaremos los tests con el siguiente comando:
bundle exec rake knapsack:rspec
Para configurar Gitlab CI para que ejecute los tests en paralelo, haremos uso de la directiva parallel
. Esta directiva hace que nuestro job se ejecute N veces pero con las variables CI_NODE_INDEX
y CI_NODE_TOTAL
establecidas por Gitlab. La primera hace referencia al número de job (empezando por 0) y la segunda al total de jobs levantados (igual al valor de la directiva parallel
). Estas dos variables de entorno son interpretadas automáticamente por Knapsack
para calcular los archivos que debe ejecutar.
features:
stage: testing
variables:
KNAPSACK_TEST_FILE_PATTERN: "spec/features/**{,/*/**}/*_spec.rb"
parallel: 5
script:
- bundle exec rake knapsack:rspec
En muchas ocasiones usamos gemas tipo RSpec-Rerun
para que vuelva a ejecutar los tests que han fallado hasta 3 veces antes de devolver fallo. Al usar Knapsack
ya no es posible usar estas gemas, pero podemos hacer uso de un script en Bash para reintentar la ejecución y simular el comportamiento de esas librerías:
bin/rerun
#!/usr/bin/env bash
run_command() {
echo "Run command: $1"
eval "$1"
}
# Ejecutamos los tests por primera vez
run_command "$1"
# Si se han ejecutado los tests bien, finalizamos el script
if [ $? -eq 0 ]; then
exit 0
fi
# Si han fallado los tests pero no hay tests que re-ejecutar, salimos con fallo
if [ ! -s "spec/examples.txt" ]; then
exit 1
fi
# Si algún test ha fallado, lo reintentamos hasta 3 veces
count=1
st=1 # por defecto el resultado sera error
while true; do
export RERUN_COUNT="$count"
# ejecutamos solo los tests que han fallado
run_command "bundle exec rspec --only-failures"
# capturamos la salida
if [ $? -eq 0 ]; then
st=0 # si se han ejecutado bien actualizamos la salida
fi
# nos salimos si llegamos a 3 iteraciones o los tests se han ejecutado bien
if [ $count -eq 3 ] || [ $st -eq 0 ]; then
break
fi
# aumentamos el contador
count=$((count+1))
done
# nos salimos devolviendo un status
exit $st
De esta forma modificaremos el script en la configuración del Gitlab CI para que use el script:
features:
stage: testing
variables:
KNAPSACK_TEST_FILE_PATTERN: "spec/features/**{,/*/**}/*_spec.rb"
parallel: 5
script:
- bin/rerun "bundle exec rake knapsack:rspec"
Para que esto funcione es importante asegurarnos de que tenemos configurado el archivo de reintentos de RSpec:
RSpec.configure do |config|
config.example_status_persistence_file_path = 'spec/examples.txt'
end