Советы по использованию FactoryGirl без ORM

в 0:00, , рубрики: ruby, Тестирование веб-сервисов, метки:

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, а именно:

  1. Сделаем ее не изменяемой заменив attr_accessor на attr_reader.
  2. Сделаем объект класса с конструктором — заполняемым хэшом пришедшими из 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. Данный перевод является экспериментальным и не претендует на профессиональную полноту.
Оригинальная статья

Автор: ювелир

Источник

* - обязательные к заполнению поля