Fixtures vs Factories

Ultimamente tenho utilizado fixtures ao invés de factories nos meus projetos para simplificar o ambiente de testes e tenho gostado do resultado.

Alguns não concordam com o uso de fixtures, considerando até mesmo como uma anti-pattern:

  1. Rails AntiPatterns
  2. Rails Testing Antipatterns: Fixtures and Factories
  3. Use factories, não fixtures

Minha opinião é que existem casos de usos para ambos, apesar de nunca ter participado em um projeto que utilizasse factories e não fosse uma completa bagunça.

O que pretendo dissertar nesse post são as vantagens e desvantagens de cada approach.

Fixtures

Prós:

  1. Extremamente rápido para carregar os dados no banco
  2. "Caching" de fixtures
  3. Rake nativa para carregar as fixtures na base de desenvolvimento similar ao db:seed - db:fixtures:load

Contras:

  1. Não permite alterar os dados antes do cenário de teste
  2. Não executa as rotinas de validação
  3. Não executa as rotinas de callback

Exemplo:

john:  
  name: John Coltrane
  email: john.coltrane@example.com
  birthdate: 1926-09-23
describe Person do  
  fixtures :people

  describe '#age' do
    it 'calculates the person age' do
      john = people(:john)
      expect(john.age).to eq 88
    end
  end
end  

Referência: http://guides.rubyonrails.org/testing.html#the-low-down-on-fixtures

Factories

Prós:

  1. Permite instanciar ou criar um stub com os dados definidos na factory
  2. Permite variar os dados em situações específicas (sequences, traits, callbacks)
  3. Possibilidade de reaproveitar os dados para outra factory similar
  4. Permitir criar uma mesma factory várias vezes com apenas uma chamada

Contras:

  1. Não possui caching de factory
  2. Relativamente lento por executar validações e callbacks dos modelos
  3. Nomes das factories são globais, isso é, não permite factories para diferentes modelos com o mesmo nome

Exemplo:

FactoryGirl.define do  
  factory :john do
    name 'John Coltrane'
    email 'john.coltrane@example.com'
    birthdate Date.new(1926, 9, 23)
  end
end  
describe Person do  
  describe '#age' do
    it 'calculates the person age' do
      john = FactoryGirl.build(:john)
      expect(john.age).to eq 88
    end
  end
end  

Referência: https://github.com/thoughtbot/factorygirl/blob/master/GETTINGSTARTED.md

Caching

Esse é um problema bem antigo também presente no machinist e no fabrication.

Por exemplo, usando fixtures:

# spec/fixtures/states.yml
mg:  
  name: Minas Gerais
  acronym: MG
# spec/fixtures/cities.yml
bh:  
  name: Belo Horizonte
  state: mg

sl:  
  name: Sete Lagoas
  state: mg

Dessa forma será criado um estado e esse mesmo estado será usado nas duas cidades.

Se você utilizar factories e definir as seguintes factories:

# spec/factories/states.rb
FactoryGirl.define do  
  factory :mg, class: State do
    name 'Minas Gerais'
    acronym 'MG'
  end
end  
# spec/factories/cities.rb
FactoryGirl.define do  
  factory :bh, class: City do
    name 'Belo Horizonte'
    association :state, factory: :mg
  end

  factory :sl, class: City do
    name 'Sete Lagoas'
    association :state, factory: :mg
  end
end  

Você receberá uma exception porque o factory girl tentará criar dois estados com o mesmo nome (assumindo que você tenha uma validação para impedir isso).

Se você precisar criar essa estrutura de dados, você tem a seguinte opção:

mg = FactoryGirl.create(:mg)  
bh = FactoryGirl.create(:bh, state: mg)  
sl = FactoryGirl.create(:sl, state: mg)  

Você já consegue imaginar o que acontece quando seus relacionamentos são mais profundos, como por exemplo País > Estado > Cidade > Bairro.

Você pode fazer isso em toda sua aplicação ou definir um factory method que possa usar sempre que precisar dessa estrutura, mas ambos me desagradam.

No machinist acabei resolvendo com essa gem e no factory girl com esse strategy.

Eu reportei essa necessidade para os criadores do factory girl e a recomendação foi justamente essa, utilizar factory method ou custom strategy.

Em resumo é possível resolver o problema de caching mas o ponto é que por não ter esse suporte nativo você depende de soluções de terceiros sendo que o próprio machinist/fabrication/factory girl já são soluções de terceiros.

Conclusão

O custo associado a usar factories no seu projeto acaba sendo maior do que o custo de usar fixtures:

Factories:

  1. Você depende de um terceiro manter o projeto de factory funcionando a cada update do rails
  2. Você depende de um outro terceiro para manter o projeto de caching funcionando a cada update do projeto de factory

Fixtures:

  1. Não existe custo envolvido por ser nativo no rails

No final cabe a você decidir se o melhor são factories ou fixtures para seu projeto mas como regra geral tenho utilizado fixtures pela organização, caching e custo zero.

Referências:

  1. Getting Friendly with Fixtures
  2. 7 Reasons I'm Sticking With Minitest and Fixtures in Rails