Подключение FactoryGirl к не-Rails проекту с RSpec

Как говорил на одной из конференций наш QA техлид — если не можешь что-то найти в гугле, значит нужно самому написать и туда положить.

Данная заметка вдохновлена танцами с бубном по граблям и битьем об стену в процессе первого знакомства c замечательным гемом factory_girl, которая, несмотря на отсутствие понятного мне на тот момент мануала, не потеряла в моих глазах ни грамма своей замечательности.

alt

1 . Создаем директорию для нового проекта, например, demo_factory_girl и переходим в нее.
2 . Создаем необходимые rspec файлы и директории при помощи команды rspec --init.
Получили:

create   .rspec  
create   spec/spec_helper.rb

3 . Создаем Gemfile при помощи команды bundle init.
Получили:

Gemfile
Gemfile.lock

4 . Добавим в Gemfile необходимые гемы:

source "https://rubygems.org"
gem 'rspec'
gem 'factory_girl' 
gem 'faker'  

и установим командой bundle install. Может ругаться на https в source, в этом случае меняем на время установки: source "http://rubygems.org".

5 . Редактируем /spec/spec_helper.rb:

require 'factory_girl'  
require 'faker'

RSpec.configure do |config|  

  config.expect_with :rspec do |expectations|   

    expectations.include_chain_clauses_in_custom_matcher_descriptions = true 

  end

  config.mock_with :rspec do |mocks|  
    mocks.verify_partial_doubles = true  
  end

end

6 . Создаем директорию spec/support/factories/ в которой будем хранить фабрики.

Находим в официальном мануале раздел "Configure your test suite" и следуем инструкциям для RSpec без использования Rails: /spec/spec_helper.rb

require 'factory_girl'  
require 'faker'

RSpec.configure do |config|  

  config.include FactoryGirl::Syntax::Methods   
    config.before(:suite) do
      FactoryGirl.find_definitions  
    end


  config.expect_with :rspec do |expectations|              
    expectations.include_chain_clauses_in_custom_matcher_descriptions = true  
  end

  config.mock_with :rspec do |mocks|  
    mocks.verify_partial_doubles = true  
  end

end

По-умолчанию фабрики должны лежать в определенных директориях - см. строку FactoryGirl.find_definitions

[0] = "/factories"
[1] = "/test/factories"
[2] = "/spec/factories"

В примере же используется "spec/support/factories/<файлы с фабриками.rb>", путь к которым нужно будет отдельно указать в spec_helper. Если этого не сделать, возникнет ошибка "ArgumentError: Factory not registered: < название фабрики >" .

require 'factory_girl'
require 'faker'

RSpec.configure do |config|

config.include FactoryGirl::Syntax::Methods
config.before(:suite) do
FactoryGirl.definition_file_paths = [File.expand_path('../support/factories', __FILE__)]
FactoryGirl.find_definitions
end

config.expect_with :rspec do |expectations|
expectations.include_chain_clauses_in_custom_matcher_descriptions = true
end

config.mock_with :rspec do |mocks|
mocks.verify_partial_doubles = true
end
end

7 . Создаем файл с фабрикой, например, чтобы создавать пользователей по определенному шаблону - spec/support/factories/user.rb.

Опять же, следуя официальному мануалу и добавив Faker для генерации псевдонастоящих тестовых данных, получаем примерно следующее: /user.rb

FactoryGirl.define do

sequence(:username) { |n| "#{Faker::Internet.user_name(min_length = 5, %w(.))}#{n}" }

factory :user do
Faker::Config.locale = :en
first_name Faker::Name.first_name
last_name Faker::Name.last_name
username Faker::Internet.user_name(min_length = 5, %w( - _))
password Faker::Internet.password(min_length = 8)
mobile_phone '12345'
admin false
end

end

Здесь сразу важно учесть нюанс. FactoryGirl ориентирована на Rails, в которых есть такое понятие как ActiveRecord, база данных и Model-View-Controller ( и много других умных слов).

Если попытаться сразу в лоб создать спеку и использовать в ней фабрику, например /spec/use_factory_girl_spec.rb:

require "spec_helper"

describe "test" do

it "use factory" do
user = build :user
user.first_name
user.last_name
user.username
user.password
user.mobile_phone
user.admin
end

end

и запустить тест, возникнет ошибка "NameError: uninitialized constant User"

WTF?!!

Фабрике нужно или указать явно, объекты какого типа(модели, класса) она должна создавать или следовать принятым соглашениям.

Для не-Rails проекта есть 2 способа решения проблемы:

1 Для неленивых

Создать одноименный класс (в нашем случае - User) и в attr accessors перечислить все поля из описания фабрики. В данном примере для простоты класс User находится в одном файле в фабрикой: spec/support/factories/user.rb

class User
attr_accessor :first_name, :last_name, :username, :admin, :password, :mobile_phone end

2 Для ленивых

Дать явные указания. Мне нравится OpenStruct (поэтому добавляем require 'ostruct' в spec_helper). Создадим еще фабрики, чтобы продемонстрировать подход истинных лентяев :)

FactoryGirl.define do

sequence(:username) { |n| "#{Faker::Internet.user_name(min_length = 5, %w(.))}#{n}" }

factory :user do
Faker::Config.locale = :en
first_name Faker::Name.first_name
last_name Faker::Name.last_name
username Faker::Internet.user_name(min_length = 5, %w( - _))
password Faker::Internet.password(min_length = 8)
mobile_phone '12345'
admin false
end

factory :bad_user, class: OpenStruct,parent: :user do
password Faker::Internet.password(max_length = 3)
mobile_phone '-'
end

factory :user_with_already_used_phone, parent: :user do
mobile_phone '12345'
end
end

class User
attr_accessor :first_name, :last_name, :username, :admin, :password, :mobile_phone end

8 . Попробуем запустить все и сразу в /spec/use_factory_girl_spec.rb:

require "spec_helper"

describe "test" do

  it "use factory" do
    user = build :user
    user.first_name
    user.last_name
    user.username
    user.password
    user.mobile_phone
    user.admin

    bad_user = create :bad_user
    bad_user.first_name
    bad_user.last_name
    bad_user.username
    bad_user.password
    bad_user.mobile_phone
    bad_user.admin

  end
end

Рассмотрим внимательнее созданных фабрикой user и bad_user.

user - просто инстанс класса User (, в котором ничего кроме attr_accessors и нет. Это сразу же накладывает ограничения. Например, если вместо user = build :user написать user = create :user получим...да-да опять ошибку.

"NoMethodError: undefined method save!' for #<User:0x007fa349bafd08>"

bad_user - прекрасно справится и с build, и с create, и будет представлять из себя <OpenStruct first_name="Clifton", last_name="Ferry", username="kyla_lang", password="ZiRa", mobile_phone="-", admin=false>

Проверяем работоспособность командой rspec .. Работает!

Специально сломаем тест, например, добавил заведомо падающее expect(user.mobile_phone).to eq('123456'). Запускаем тест и получаем:

   Failures:


   1) test use factory
 Failure/Error: expect(user.mobile_phone).to eq('123456')

   expected: "123456"
        got: "12345"