Usando Blade Para Testar JavaScript no Rails

Motivação

You can check this post in english

Eu estava procurando por um executor de teste javascript com integração para o Rails, encontrei algumas opções, porém decidi escolher o Blade. Como isto não é uma comparação (eu não sou tão experiente para isto), aqui estão as razões do porque eu preferi o Blade:

  • Fácil de configurar em bibliotecas javascript
  • Suporta sprockets
  • Ativamente usado no Turbolinks, Trix e ActionCable
  • Eu acredito que ele pode se tornar o testador padrão de javascript para o Rails no futuro

Então comecei a configurar como seria de costume em uma biblioteca javascript

# Gemfile
group :development, :test do
  gem 'blade'
end

Depois de editar o Gemfile, temos:

bundle install

E adicionando um arquivo .blade.yml

load_paths:
  - test/javascripts/src
  - test/javascripts/vendor

logical_paths:
  - application.js
  - test.js

Como isto é uma aplicação do Rails, minha idéia é definir os caminhos test/javascripts/{src/vendor} para serem carregados, então Blade carrega meu teste e as bibliotecas de apoio.

O arquivo test.js é o ponto de entrada para o conjunto de testes, e application.js é o arquivo de entrada de javascript no Rails.

Consegui executar o Blade como esperado, mas não foi carregado application.js. Perguntando para o @javan no Twitter, ele respondeu que não há integração built-in no Rails, então estamos por nossa própria conta.

A primeira tentativa foi adicionar app/assets/javascripts para load_path, funcionou, mas nenhum javascript usado por gems era carregado (exemplo jquery-rails)

Então porque não entrar no arquivo bin no blade, fazer o Rails carregar o caminho dos assets e configurar o blade?

Depois de algum tempo descobrindo como as coisas funcionavam, eu alterei o binário do Blade, vamos comentar cada passo:

#!/usr/bin/env ruby
# frozen_string_literal: true
#
# This file was generated by Bundler.
#
# The application 'blade' is installed as part of a gem, and
# this file is here to facilitate running it.
#

require "pathname"
ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
  Pathname.new(__FILE__).realpath)

require "rubygems"
require "bundler/setup"

# COMENTÁRIO: aqui termina o prelúdio do _bundler_

# COMENTÁRIO: Inicializar a aplicação rails
require_relative File.join('..', 'config/application.rb')
Rails.application.initialize!

# COMENTÁRIO: Inicializar o caminhos dos assets no Rails ...
paths = Rails.application.assets.paths
cwd = `pwd`.chomp + "/"
paths = paths.map { |x| x.gsub(cwd, '') }

# COMENTÁRIO: ... E concatenar com o caminho especificado em .blade.yml
blade_config = YAML::load_file(File.join(__dir__, '..', '.blade.yml'))
paths.concat(blade_config["load_paths"])
Blade.initialize!(interface: 'runner', load_paths: paths)

# COMENTÁRIO: bundler executa blade
load Gem.bin_path("blade", "blade")

Eu não tenho certeza se está é a melhor abordagem, mas tive um feedback positivo do @javan, então o próximo passo é fazer um plugin para o blade :)

Bonus: Fixtures

Uma das minhas principais motivações em obter um javascript rodando era pela certeza que minhas fixtures foram sempre atualizadas, minha primeira idéia foi de processar o erb e... não funcionou!

Então eu tive uma luz, é uma abordagem padrão testar a saída (view) em um controller do Rails (ok, a idéia não é discutir se esta é a melhor abordagem, o importante é que é factível) porque nós temos o response.body, então porque não escrever isto nas fixtures?

A lógica por trás é:

  1. Nós podemos acessar test/javascripts/src/fixtures no blade via http://localhost:9876/fixtures
  2. Nós podemos acessar response.body no controlador de teste do Rails
  3. Vamos escrever em arquivos durante a executação de testes do Rails
  4. Executar teste conjunto Javascript com a fixture mais atualizada

Editei test_helper.rb e adicionei

require 'fileutils'

class ActiveSupport::TestCase
  def save_fixture(name, selector = nil)
    fixture_dir = File.dirname(name)
    fixture_dir = File.join(fixtures_dir, fixture_dir)
    fixture_path = File.join(fixture_dir, File.basename(name) + ".html")

    unless File.directory?(fixture_dir)
      FileUtils.mkdir_p(fixture_dir)
    end

    # Get only body for fixture (or selector if set)
    selector = selector.nil? ? 'body' : selector
    output = Nokogiri::HTML(response.body)
    if (selector == "body")
      output = output.css(selector)[0].children.to_s
    else
      output = output.css(selector)[0].to_s
    end
    File.write(fixture_path, output)
  end
end

# Setup JavaScript Fixtures Dir
def fixtures_dir
  test_dir = File.dirname(File.expand_path(__FILE__))
  File.join(test_dir, 'javascripts', 'src', 'fixtures')
end

puts fixtures_dir
unless File.directory?(fixtures_dir)
  FileUtils.mkdir_p(fixtures_dir)
end

Em um teste do controller é necessário apenas:

require 'test_helper'

class ThingControllerTest < ActionController::TestCase
  test "index" do
    get :index
    assert :success
    # do not specify second parameter if you want the whole body
    save_fixture('things/table', 'table#things')
  end
end

Desta forma temos response.body escrita para test/javascripts/src/fixtures/things/table.html

E você está pronto para escrever seu teste JS

QUnit.test( "ThingsController#index", function( assert ) {
  assert.expect(1);
  var done = assert.async();

  $.get( "/fixtures/things/table.html", function( data ) {
    $( "#qunit-fixture" ).html( data );
    length = $("table#things tbody").find('tr').length;
    assert.equal(length, 2, "Two things listed");
    done();
  });
});

O resultado é excelente:

suite de teste do blade

Se a view for mudada, a fixture será atualizada, e se o JS quebrar, o teste falhará.

Finalizando

Para mim, atingiu todas as minhas expectativas para um teste javascript executando no Rails, porque é uma configuração poderosa nos seguintes pontos:

  1. Fácil de instalar
  2. Executa o teste em diversos navegadores
  3. Pode ser executado em CI utilizando sauce labs (talvez em blade-phantomjs no futuro?)

E o que você acha sobre essa abordagem, você tem alguma dica sobre testes JS no Rails que você gostaria de compartilhar?

ps: Esta abordagem se tornou um plugin para o Blade, aguardo feedback!

Faça ótimos testes com Rails!