Skip to content

Commit

Permalink
improve readme
Browse files Browse the repository at this point in the history
  • Loading branch information
ermolaev committed Sep 30, 2024
1 parent 1e6a82b commit 90a61bf
Showing 1 changed file with 74 additions and 141 deletions.
215 changes: 74 additions & 141 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,189 +1,105 @@
# fixture_bot
# `fixture_bot`

We all love Rails fixtures because they're fast, but we hate to deal with YAML/CSV/SQL files. Here enters [factory_bot](https://rubygems.org/gems/factory_bot) (FB).
Improve the performance of your tests, as factories generate and insert data into the database every time, it can be slow. See [benchmarks](#Benchmarks).

Now, you can easily create records by using predefined factories. The problem is that hitting the database everytime to create records is pretty slow. And believe me, you'll feel the pain when you have lots of tests/specs.
Problems using [Rails fixtures](https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html):

So here enters Factory Bot Preload (FBP). You can define which factories will be preloaded, so you don't have to recreate it every time (that will work for 99.37% of the time, according to statistics I just made up).
- No data validation
- Long loading of fixtures (for one or all tests - equally long)
- YML format (no reuse code)
- heavy support for fixtures and factories together

### Installation

Add both FB and FBP to your Gemfile:

```ruby
source "https://rubygems.org"

gem "rails"

group :test, :development do
group :test do
gem "factory_bot"
gem "fixture_bot", require: false
end
```

Notice that adding `require: false` is important; otherwise you won't be able to
run commands such as `rails db:test:prepare`.

### RSpec Setup

On your `spec/spec_helper.rb` file, make sure that transactional fixtures are
enabled. Here's is my file without all those RSpec comments:

```ruby
ENV["RAILS_ENV"] ||= "test"
require File.expand_path("../../config/environment", __FILE__)
require "rspec/rails"

# First, load fixture_bot/preload.
require "fixture_bot/preload"

# Then load your factories
Dir[Rails.root.join("spec/support/factories/**/*.rb")].each do |file|
require file
end

RSpec.configure do |config|
config.use_transactional_fixtures = true
config.mock_with :rspec
end
```

### Minitest Setup

On your `test/test_helper.rb` file, make sure that transaction fixtures are
enabled. Here's what your file may look like:

```ruby
ENV["RAILS_ENV"] ||= "test"
require_relative "../config/environment"
require "rails/test_help"

module ActiveSupport
class TestCase
self.use_instantiated_fixtures = true
end
end

# First, load fixture_bot/preload.
require "fixture_bot/preload"

# Then load your factories.
Dir["./test/support/factories/**/*.rb"].each do |file|
require file
end

# Finally, setup minitest.
# Your factories won't behave correctly unless you
# call `FixtureBot.minitest` after loading them.
FixtureBot.minitest
```

### Usage

Create your factories and load it from your setup file (either
`test/test_helper.rb` or `spec/spec_helper.rb`) You may have something like
this:
To define your fixture in factories, use the `preload` method

```ruby
FactoryBot.define do
factory :user do
name "John Doe"
sequence(:email) {|n| "john#{n}@example.org" }
sequence(:username) {|n| "john#{n}" }
password "test"
password_confirmation "test"
end

factory :projects do
name "My Project"
association :user

preload(:users) do
fixture_with_id(:first) { create(:user, id: 1) }
fixture(:john) { create(:user) }
fixture(:with_gmail) { create(:user, email: "[email protected]") }
end
end
```

To define your preloadable factories, just use the `preload` method:

```ruby
FactoryBot.define do
factory :user do
name "John Doe"
sequence(:email) {|n| "john#{n}@example.org" }
sequence(:username) {|n| "john#{n}" }
password "test"
password_confirmation "test"
end

factory :projects do
name "My Project"
association :user
user { users(:with_gmail) }
end

preload do
fixture(:john) { create(:user) }
preload(:users) do
fixture(:myapp) { create(:project, user: users(:john)) }
end
end
```

You can also use preloaded factories on factory definitions.

### RSpec usage

```ruby
FactoryBot.define do
factory :user do
# ...
require "spec_helper"

describe User do
let(:user) { users(:john) }

it "returns john's record" do
expect(users(:john)).to be_a User
end

factory :projects do
name "My Project"
user { users(:john) }
it "returns myapp's record" do
expect(projects(:myapp).user).to eq users(:john)
end

preload do
fixture(:john) { create(:user) }
fixture(:myapp) { create(:project, user: users(:john)) }
it "each call fixture return new object" do
expect(user.object_id).not_to eq users(:john).object_id
end
end
```

### RSpec Setup

If you need to create records with specifying id, use `fixture_stub_const`, it stub const in model to next value from generated primary key
On your `spec/support/factory_bot.rb` file

```ruby
class User
ADMIN_ID = 10
require "fixture_bot" # the order is important, it must be before loaded factories
require "factory_bot_rails"

def can_edit_article?
id == ADMIN_ID
end
FactoryBot::SyntaxRunner.class_eval do
include RSpec::Mocks::ExampleMethods
end
```

```ruby
FactoryBot.define do
factory :user do
# ...
end

preload do
fixture(:john) { create(:user) }
fixture_stub_const(:admin, :ADMIN_ID) { create(:user) }
end
RSpec.configure do |config|
config.include FactoryBot::Syntax::Methods
end
```

```ruby
describe User do
let(:admin) { users(:admin) }

it "admin can edit article" do
expect(admin).to be_can_edit_article
end
end
````
### Minitest Setup

On your `test/test_helper.rb` file, make sure that transaction fixtures are
enabled. Here's what your file may look like

Like Rails fixtures, FBP will define methods for each model. You can use it on
your examples and alike.
```ruby
# First, load fixture_bot
require "fixture_bot"
FixtureBot.minitest
```

```ruby
require "test_helper"
Expand All @@ -199,25 +115,42 @@ class UserTest < ActiveSupport::TestCase
end
```

Or if you're using RSpec:
## Callbacks
```ruby
require "spec_helper"
FixtureBot.after_load_fixtures do
# code uses fixtures
end
```

describe User do
let(:user) { users(:john) }
## Benchmarks
#### factories vs fixtures

it "returns john's record" do
users(:john).should be_an(User)
end
simple model with 10 fields
```ruby
Benchmark.ips do |x|
x.report("fixture") { brands(:lux) }. # fixture: 4666.2 i/s
x.report("factory") { create(:brand) } # factory: 1077.8 i/s - 4.33x slower
x.compare!
end
```

it "returns myapp's record" do
projects(:myapp).user.should == users(:john)
end
user model with 40+ fields, 1 association
```ruby
Benchmark.ips do |x|
x.report("fixture") { users(:with_post_index)} # fixture: 3395.4 i/s
x.report("factory") { create(:user) } # factory: 159.6 i/s - 21.27x slower
x.compare!
end
```

That's it!
product model with 40+ fields, 5+ associations
```ruby
Benchmark.ips do |x|
x.report("fixture") { products(:available) } # fixture: 3564.3 i/s
x.report("factory") { create(:product, :available) } # factory: 67.7 i/s - 52.68x slower
x.compare!
end
```

## License

Expand Down

0 comments on commit 90a61bf

Please sign in to comment.