FactoryGirl это один из моих любимых инструментов для тестирования. Это один из первых инструментов — который я выбираю при работе вне фреймворков Ruby.
В последнее время я работаю над Rails-проектами, которые не используют базы данных
и поэтому не использует ORM такие как ActiveRecord. Вместо этого используется JSON API и преобразовывают в значения для обычных Ruby объектов.
Изначально в этих проектах значения для юнит тестов были написаны в ручную для каждого объекта. Но в дальнейшем — добавлять значения стало утомительным; значения для предыдущих объектов так же приходилось переписывать раз за разом.
Но к счастью FactoryGirl может быть использован и в обычных Ruby объектах,
со всеми полезными функциями.
Небольшое вступление:
Примечание: В примерах используются однострочные классы и Factory для краткости.
Давайте начнем с простой Factory для обычного Ruby объекта.
it "supports building a PORO object" do
class User
attr_accessor :name
end
FactoryGirl.define do
factory :user do
name "Amy"
end
end
user = FactoryGirl.build(:user)
expect(user.name).to eq "Amy"
end
Примечательным здесь является использование build стратегии для создания объекта. При стратегии create мы получим ошибку:
NoMethodError:
undefined method `save!' for #<User:0x007ff042882a90 @name="Amy">
Как понятно из описания ошибки: она появляется потому — что у нас отсутствует метод #save!.. Если бы мы использовали ORM на подобие ActiveRecord то при наследовании от
ActiveRecord::Persistence он (данный метод) был бы реализован.
Неизменяемость.
Теперь давайте добавим кое что в нашу модель User, а именно:
- Сделаем ее не изменяемой заменив attr_accessor на attr_reader.
- Сделаем объект класса с конструктором — заполняемым хэшом пришедшими из JSON.
Реализовав это — мы получим что то вроде:
class User
attr_reader :name
def initialize(data = {})
@name = data[:name]
end
end
И при вызове без изменений в Factory мы получим:
NoMethodError:
undefined method `name=' for #<User:0x007fec9a9f3d08 @name=nil>
Произошло это потому — что по умолчания FactoryGirl использует метод #new для инициализации обьекта, а затем присваивает значения атрибутом объекта. Это можно переопределять при помощи метода метода initialize_with, в описании Factory:
t "supports custom initialization" do
class User
attr_reader :name
def initialize(data)
@name = data[:name]
end
end
FactoryGirl.define do
factory :user do
name "Amy"
initialize_with { new(attributes) }
end
end
user = FactoryGirl.build(:user)
expect(user.name).to eq "Amy"
end
Распознание вложенных ресурсов
Давайте представим некий JSON объекта с вложенными ресурсами, например:
{
"name": "Bob",
"location": {
"city": "New York"
}
}
Давайте добавим описание класса для нашего вложенного объекта Location:
class Location
attr_reader :city
def initialize(data)
@city = data[:city]
end
end
class User
attr_reader :name, :location
def initialize(data)
@name = data[:name]
@location = Location.new(data[:location])
end
end
Теперь нужно довить к нашей User Factory еще одну: Location:
it "supports constructing nested models" do
class Location
attr_reader :city
def initialize(data)
@city = data[:city]
end
end
class User
attr_reader :name, :location
def initialize(data)
@name = data[:name]
@location = Location.new(data[:location])
end
end
FactoryGirl.define do
factory :location do
city "London"
initialize_with { new(attributes) }
end
factory :user do
name "Amy"
location { attributes_for(:location) }
initialize_with { new(attributes) }
end
end
user = FactoryGirl.build(:user)
expect(user.name).to eq "Amy"
expect(user.location.city).to eq "London"
end
А теперь — давайте проверим что получилось:
puts FactoryGirl.attributes_for(:user)
# => {:name=>"Amy", :location=>{:city=>"London"}}
Структура имитирующая вложенный обьект передается хешом в метод initialize класса User в нашем initialize_with блоке.
Time to lint
Последняя часть использования FactoryGirl с «чистыми» Ruby объектами.
Обычно Linting вызывается до начала тестирования, для того что бы избежать кучи ошибок в не правильно описанной Factory.
После использования метода FactoryGirl.lint из Rake задачи мы получим следующее:
FactoryGirl::InvalidFactoryError: The following factories are invalid:
* user - undefined method `save!' for #<User:0x007fc890ae0e88 @name="Amy"> (NoMethodError)
Не найден метод #save!, потому — что метод #lint из коробки использует стратегию с созданием и сохранением объекта. Для того что бы изменить это — у нас есть 2 варианта:
Вариант первый: метод #skip_create
Давайте добавим метод #skip_create в описание наших Factory:
FactoryGirl.define do
factory :location do
city "London"
skip_create
initialize_with { new(attributes) }
end
factory :user do
name "Amy"
location { attributes_for(:location) }
skip_create
initialize_with { new(attributes) }
end
end
Теперь наши Factory заработают. Метод #skip_create также позволяет вызывать метод create в тестах:
FactoryGirl.create(:user)
Вариант второй: Lint by building
Мы можем добавить Rake задачу, для проверки наших factory:
namespace :factory_girl do
desc "Lint factories by building"
task lint_by_build: :environment do
if Rails.env.production?
abort "Can't lint factories in production"
end
FactoryGirl.factories.each do |factory|
lint_factory(factory.name)
end
end
private
def lint_factory(factory_name)
FactoryGirl.build(factory_name)
rescue StandardError
puts "Error building factory: #{factory_name}"
raise
end
end
Пример не правильной Factory:
class User
attr_reader :name
def initialize(data)
@name = data.fetch(:name) # <- Имя обязательно
end
end
FactoryGirl.define do
factory :user do
# name "Amy" <- Закомментируем
initialize_with { new(attributes) }
end
end
Теперь после вызова factory_girl:lint_by_build задачи Rake мы получим:
Error building factory: user
rake aborted!
KeyError: key not found: :name
/path/models.rb:5:in `fetch'
/path/models.rb:5:in `initialize'
/path/factories.rb:8:in `block (3 levels) in <top (required)>'
/path/Rakefile:15:in `lint_factory'
Итого:
Для использования FactoryGirl + «чистый» Ruby:
- Используйте FactoryGirl.build вместо FactoryGirl.create.
- Используйте initialize_with для изменения инициализации объекта.
- Внешние Factory могут использовать attributes_for для построения вложенных ресурсов.
- Для переопределения стратегии создания объекта используйте:
P.S. Данный перевод является экспериментальным и не претендует на профессиональную полноту.
Оригинальная статья
Автор: ювелир