📜 ⬆️ ⬇️

Tips for using FactoryGirl without ORM

FactoryGirl is one of my favorite testing tools. This is one of the first tools I choose when working outside of Ruby frameworks.

Recently I have been working on Rails projects that do not use databases.
and therefore does not use ORM such as ActiveRecord . Instead, the JSON API is used and converted to values ​​for ordinary Ruby objects.

Initially, in these projects, the values ​​for unit tests were written manually for each object. But in the future, adding values ​​has become tedious; values ​​for previous objects also had to be rewritten time after time.

But luckily, FactoryGirl can be used in regular Ruby objects,
with all the useful features.
')

A small introduction:


Note: The examples use single-line classes and Factory for short.

Let's start with a simple Factory for a regular Ruby object.

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 

Notable here is the use of the build strategy to create an object. With the create strategy, we get an error:

 NoMethodError: undefined method `save!' for #<User:0x007ff042882a90 @name="Amy"> 

As it is clear from the description of the error: it appears because - we do not have the #save method! .. If we used ORM like ActiveRecord, then when inheriting from
ActiveRecord :: Persistence it (this method) would be implemented.

Immutability


Now let's add something to our User model, namely:

  1. Let's make it unchangeable by replacing attr_accessor with attr_reader .
  2. Let's make a class object with a constructor — filled with hash that came from JSON .

By implementing this, we get something like:

 class User attr_reader :name def initialize(data = {}) @name = data[:name] end end 

And when you call without changes in the Factory we get:

 NoMethodError: undefined method `name=' for #<User:0x007fec9a9f3d08 @name=nil> 

This happened because, by default, FactoryGirl uses the #new method to initialize an object, and then assigns values ​​to an object attribute. This can be overridden using the method initialize_with method, in the description of the 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 

Recognition of nested resources


Let's imagine some JSON object with nested resources, for example:

 { "name": "Bob", "location": { "city": "New York" } } 

Let's add a class description for our nested Location object:

 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 

Now we need to add one more thing to our 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 

And now - let's check what happened:

 puts FactoryGirl.attributes_for(:user) # => {:name=>"Amy", :location=>{:city=>"London"}} 

The structure that simulates a nested object is passed to the hash in the initialize method of the User class in our initialize_with block.

Time to lint


The last part of using FactoryGirl with clean Ruby objects. Usually, Linting is called before testing begins, in order to avoid heaps of errors in a factory that is not properly described.

After using the FactoryGirl.lint method of the Rake task, we get the following:

 FactoryGirl::InvalidFactoryError: The following factories are invalid: * user - undefined method `save!' for #<User:0x007fc890ae0e88 @name="Amy"> (NoMethodError) 

#Save method not found ! because the out of the box #lint method uses a strategy with creating and saving an object. In order to change this - we have 2 options:

Option One: #skip_create Method

Let's add the #skip_create method to our Factory description:

  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 

Now our Factory will earn. The #skip_create method also allows you to call the create method in tests:

 FactoryGirl.create(:user) 

Option Two: Lint by building


We can add a rake task to check our factories:

 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 

An example of an incorrect 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 

Now after calling factory_girl: lint_by_build of the Rake task, we get:

 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' 

Total:


To use FactoryGirl + "pure" Ruby:


PS This translation is experimental and does not pretend to be professional.
Original article

Source: https://habr.com/ru/post/332100/


All Articles