Há um mês escrevi um post sobre configurações fora do padrão em Rails, onde descrevi como executar testes com models cujas tabelas não existem no banco de dados local, e sim em uma base externa. Porém, depois de postar, verifiquei que há um outro problema não resolvido com a configuração que descrevi nesse post: relacionamentos HABTM (has and belongs to many).

Nos relacionamentos HABTM, normalmente, há dois models, um correspondente a cada tabela do banco de dados. Como a relação entre eles é de muitos para muitos, há uma terceira tabela no banco de dados, que é responsável pela associação das demais tabelas. Como essa tabela só costuma ter dois campos, que são FK’s correspondentes às PK’s dessas tabelas, ela não precisa ter um model; basta criar o relacionamento dos dois models como has_and_belongs_to_many, passando como parâmetro join_table essa tabela intermediária.

A configuração descrita no post anterior carrega manualmente os fixtures de cada model, porém não carrega fixtures correspondentes à tabela intermediária. Para isso, precisei implementar um novo método na classe Test::Unit::TestCase (arquivo test/test_helper.rb):

def set_habtm_fixtures(class1, class2)
  return unless (class1.reflections && class1.reflections.values)
  id1 = nil
  id2 = nil
  table = nil

  # Verifica qual dos relacionamentos do model class1 está associado à tabela class2
  class1.reflections.values.each do |r|
    # Se a classe associada for class2 e for uma relação HABTM, le os FK's e o nome da tabela
    if (r.klass == class2 && !r.instance_values['options'][:join_table].nil?)
      id1 = r.primary_key_name
      id2 = r.association_foreign_key
      table = r.instance_values['options'][:join_table]
      break
    end
  end
  return if table.nil?
  connection = class1.connection

  data = File.open(File.join(RAILS_ROOT, 'test', 'fixtures', "#{table}.yml")).readlines.join
  result = ERB.new(data).result
  parsed = YAML.load(result)

  # Exclui todos os registros da tabela
  connection.execute "DELETE FROM #{table}"

  parsed.values.each do |value|
    value1 = value[id1] || 'NULL'
    value2 = value[id2] || 'NULL'
    connection.execute "INSERT INTO #{table} (#{id1}, #{id2}) values (#{value1}, #{value2})"
  end
end

Este método ficou bem “feio”, pois, como não existe um model correspondente a esta tabela, precisei criar a query manualmente. O método recebe dois nomes de classes (ActiveRecord) como parâmetro. Primeiramente é verificado qual dos relacionamentos do model class1 está associado a class2, para descobrir quais são as FK’s e o nome da tabela. Em seguida, os registros desta tabela são excluídos, e cada linha do arquivo de fixtures é carregada (usando a conexão de um dos ActiveRecords).

Além disso, modifiquei o método set_fixtures desta mesma classe, criado no post anterior, pois percebi que não era necessário passar o nome da tabela como parâmetro, basta usar o método table_name:

def set_fixtures (class_name)
  table = class_name.table_name
  return unless class_name.kind_of?(ActiveRecord::Base)

  # Define a conexao usada pela classe
  ActiveRecord::Base.connection = base.connection
  Fixtures.create_fixtures(File.join(RAILS_ROOT, 'test', 'fixtures'), table) { base.connection }
end

Para exemplificar como usar estes métodos, imagine um cadastro de usuários com grupos, onde um usuário pode fazer parte de mais de um grupo. Neste exemplo, teríamos um model Usuario (tabela usuarios), um model Grupo (tabela grupos) e uma tabela usuarios_grupos, sem um model correspondente. Na classe de teste do model Usuario, teríamos o seguinte:

class UsuarioTest < ActiveSupport::TestCase
  def setup
    set_fixtures(Usuario)
    set_fixtures(Grupo)
    set_habtm_fixtures(Usuario, Grupo)
  end

  # Testes
end

No método setup, que é executado automaticamente quando os testes são executados, as duas chamadas a set_fixtures carregam as fixtures das tabelas usuarios e grupos, respectivamente; a chamada a set_habtm_fixtures atualiza a tabela usuarios_grupos.