[RUBY] Introduction to RSpec 4. Create test data with Factory Bot

Last time continued

I want to keep the test data setup simple even if the test becomes complicated.

Gem's ** Factory Bot ** is a well-known Ruby library that can be used in such cases, so I will summarize how to use it and notes.

Factory and fixture

As a means of generating sample data in Rails, a function called fixture is provided by default as a file in YAML format. Fixtures are a great feature, but there are some things to keep in mind that Rails doesn't use Active Record to load fixture data into the database, so it behaves differently in production.

The factory can easily generate test data and make the code short and easy to read, but if you are not aware of the generated data, unexpected data will be generated during the test and test execution will be useless It will be late.

If you understand the characteristics of each and use them properly, you will be able to write tests more smoothly.

Factory Bot installation

Gemfile


group :development, :test do
  gem "factory_bot_rails"
end
$ bundle install

By the way, when you generate a model with rails generate, we will set the factory to be generated automatically.

Just remove fixtures: false.

config/application.rb


config.generators do |g|
  g.test_framework :rspec,
  view_specs: false
  helper_specs: false,
  routing_specs: false
end

Add factory

Now that you have the Factory Bot installed, let's add a factory for the User model.

$ rails g factory_bot:model user

When you run this command, a new directory called factories will be created in the spec directory, and a file named users.rb will be created in it with the following contents.

spec/factories/users.rb


FactoryBot.define do
  factory :user do
  
  end
end

I will create test data here.

spec/factories/users.rb


FactoryBot.define do
  factory :user do
    name     { "Zeisho" }
    email    { "[email protected]" }
    password { "hogehoge" }
  end
end

Let's check user_spec.rb to see if the test data is created properly.

spec/models/user_spec.rb


require 'rails_helper'

describe User do
  #Have a valid factory
  it "has a valid factory" do
    expect(FactoryBot.build(:user)).to be_valid
  end

  #Other specs

end

Here we are using FactoryBot.build to instantiate the user and test its effectiveness. It has more compact specifications than the valid user instance test described in the previous article.

Overwrite the FactoryBot data and rewrite the validation error test.

spec/models/user_spec.rb


require 'rails_helper'

describe User do
  #Have a valid factory
  it "has a valid factory" do
    expect(FactoryBot.build(:user)).to be_valid
  end

  #Must be invalid without name
  it "is invalid without a name"
    user = FactoryBot.build(:user, name: nil)
    user.valid?
    expect(user.errors[:name]).to include("can't be blank")
  end

  #If there is no email address, it is invalid
  it "is invalid without a email"
    user = FactoryBot.build(:user, email: nil)
    user.valid?
    expect(user.errors[:email]).to include("can't be blank")
  end

  #Must be invalid without password
  it "is invalid without a password"
    user = FactoryBot.build(:user, password: nil)
    user.valid?
    expect(user.errors[:password]).to include("can't be blank")
  end

  #If the email address is duplicated, it must be invalid.
  it "is invalid with a duplicate email address" do
    FactoryBot.create(:user, email: "[email protected]")
    user = FactoryBot.build(:user, email: "[email protected]")
    user.valid?
    expect(user.errors[:email]).to include("has already been taken")
  end

end

Generate unique data in a sequence

When generating multiple test data in example

FactoryBot.create(:user)

If you use repeatedly, the attributes such as name and email will be exactly the same, and the test execution may stop due to a validation error.

Factory Bot can use sequences to generate and resolve data with unique validation.

spec/factories/users.rb


FactoryBot.define do
  factory :user do
    name             { "Zeisho" }
    sequence(:email) { |n| "hoge#{n}@hoge.com" }
    password         { "hogehoge" }
  end
end

By doing the above, you can have a unique email address each time the factory creates a new user.

Deal with associations in the factory

FactoryBot can also generate data that is conscious of the association (association) of multiple models, so I will introduce it.

When associating, for example, when you want to create data of Note model belonging to User model and Project model, if you create an instance of Note model, the data of User and Project model associated with it will be automatically generated. Will come to you.

First, create data for the Note model that belongs to the User model and Project model.

$ rails g factory_bot:model note

spec/factories/notes.rb


FactoryBot.define do
  factory :note do
    message { "My important note." }
    association :project  #Association with test data project
    user { project.owner }  #Association with test data user
  end
end

Next is the Project model, which belongs to the User model and owns the Note model.

$ rails g factory_bot:model project

spec/factories/projects.rb


FactoryBot.define do
  factory :project do
    sequence(:name) { |n| "Project #{n}" }
    description     { "A test project." }
    due_on          {1.week.from_now}
    association     :owner  #Association of the owner
  end
end

Finally, add the association to User and you're done.

spec/factories/users.rb


FactoryBot.define do
  factory :user, aliases: [:owner] do
    name             { "Zeisho" }
    sequence(:email) { |n| "hoge#{n}@hoge.com" }
    password         { "hogehoge" }
  end
end

Factory inheritance

With FactoryBot, you can scoop multiple data in one file, and duplicate attributes can be omitted.

Let's take the case of giving a user multiple projects as an example.

spec/factories/projects.rb


FactoryBot.define do
  factory :project do
    sequence(:name) { |n| "Project #{n}" }
    description     { "A test project." }
    due_on          {1.week.from_now}
    association     :owner

    #Project deadline yesterday
    factory :project_due_yesterday do
      due_on { 1.day.ago }
    end

    #Today is the deadline project
    factory :project_due_today do
      due_on { Date.current.in_time_zone }
    end

    #Project deadline tomorrow
    factory :project_due_tomorrow do
      due_on { 1.day.from_now }
    end
  end
end

By writing in the block of: project, you can generate data that inherits attributes other than due_on from: project. When calling the factory data in the test, you can call it by specifying the factory name as it is.

Also, since FactoryBot is nested, it can be determined that: project_due_yesterday,: project_due_today,: project_due_tomorrow is a child factory of: project, so ** trait ** can be used to eliminate the designation of class: Project. I can do it.

spec/factories/projects.rb


FactoryBot.define do
  factory :project do
    sequence(:name) { |n| "Project #{n}" }
    description     { "A test project." }
    due_on          {1.week.from_now}
    association     :owner

    #Project deadline yesterday
    trait :due_yesterday do
      due_on { 1.day.ago }
    end

    #Today is the deadline project
    trait :due_today do
      due_on { Date.current.in_time_zone }
    end

    #Project deadline tomorrow
    trait :due_tomorrow do
      due_on { 1.day.from_now }
    end
  end
end

To call a child factory with a trait,

FactoryBot.create(:project, :due_yesterday)

It can be called by setting ** parent factory, child factory ** like.

Callback

Callbacks allow you to do additional work before and after the factory creates, builds, etc. an object.

When creating a project object, let's define a callback that also generates a note associated with it.

spec/factories/projects.rb


FactoryBot.define do
  factory :project do
    sequence(:name) { |n| "Project #{n}" }
    description     { "A test project." }
    due_on          {1.week.from_now}
    association     :owner

    #Project with notes
    trait :with_notes do
      after(:create) { |project| create_list(:note, 5, project: project) }
    end
  end
end

In: with_notes, after creating the project object, 5 note objects are created using the create_list method.

The defined callback can be called as follows.

FactoryBot.create(:project, :with_notes)

Continued

Recommended Posts

Introduction to RSpec 4. Create test data with Factory Bot
Introduction to RSpec 1. Test, RSpec
Rails Tutorial Chapter 14 Creating Relationship Test Data with Factory Bot
I want to test Action Cable with RSpec test
[Rails] Test with RSpec
Test Nokogiri with Rspec.
Introduction to RSpec 2. RSpec setup
[Rails] I want to test with RSpec. We support your step [Introduction procedure]
Introduction to RSpec 5. Controller specs
Introduction to RSpec 6. System specifications
Introduction to RSpec 3. Model specs
[RSpec] Let's master Factory Bot
Test Active Strage with RSpec
Introduction to Micronaut 2 ~ Unit test ~
Test GraphQL resolver with rspec
How to test a private method with RSpec for yourself
How to erase test image after running Rspec test with CarrierWave
Test with RSpec + Capybara + selenium + chromedriver
Introduction to algorithms with java-Shakutori method
Introduction to Design Patterns (Factory Method)
Introduction to Design Patterns (Abstract Factory)
[RSpec] How to write test code
Let's unit test with [rails] Rspec!
Introduction to SpringBoot + In-Memory Data Grid (Data Persistence)
[Ruby on Rails] View test with RSpec
Create realistic dummy data with gem Faker
Introduction to Ruby basic grammar with yakiniku
[Note] How to get started with Rspec
[Ruby on Rails] Controller test with RSpec
How to delete data with foreign key
How to test private scope with JUnit
How to write an RSpec controller test
Introduction to algorithms with java --Search (breadth-first search)
[Introduction to Java Data Structures] Create your own fast ArrayDeque for primitive types
How to perform UT with Excel as test data with Spring Boot + JUnit5 + DBUnit
Sample to create PDF from Excel with Ruby
Introduction to Robot Battle with Robocode (Environment Construction)
[Java] How to test for null with JUnit
I rewrote the Rails tutorial test with RSpec
Introduction to SpringBoot + In-Memory Data Grid (Event Handling)
Introduction to algorithms with java --Search (bit full search)
Introduction to algorithms with java-Search (Full search, Binary search)
[For beginners] Test devise user registration with RSpec
How to test interrupts during Thread.sleep with JUnit
How to use "sign_in" in integration test (RSpec)
How to create multiple pull-down menus with ActiveHash
Create related data together with FactoryBot for yourself
How to write test code with Basic authentication
Rails beginners tried to get started with RSpec
How to create hierarchical category data using ancestry
How to create member variables with JPA Model
Easy to make LINE BOT with Java Servlet
Introduction to Robot Battle with Robocode (Beginner Development)
[Introduction to Spring Boot] Authentication function with Spring Security
Create dummy data of portfolio with Faker [Note]
Introduction to Machine Learning with Spark "Price Estimate" # 3 Make a [Price Estimate Engine] by learning with training data