Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Context helpers for callbacks: #1695

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@
- [Multiple callbacks](callbacks/multiple-callbacks.md)
- [Global callbacks](callbacks/global-callbacks.md)
- [Symbol#to_proc](callbacks/symbol-to_proc.md)
- [Callback parameters](callbacks/callback-parameters.md)
- [Modifying factories](modifying-factories/summary.md)
- [Linting Factories](linting-factories/summary.md)
- [Custom Construction](custom-construction/summary.md)
Expand Down
112 changes: 112 additions & 0 deletions docs/src/callbacks/callback-parameters.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
# Callback parameters

Callbacks can receive zero, one or two parameters.

## With zero parameters

Callbacks with zero parameters simply execute the provided block of code:
```ruby
factory :user do
after(:stub) { do_something() }
end
```

## With one parameter

Callbacks with a single parameter receive the factory instance being constructed:
```ruby
factory :user do
after(:build) { |user| do_something_to(user) }
end
```

## With two parameters

Callbacks with two parameters receive both the factory instance
and the context in which the instance is constructed:
```ruby
factory :user do
transient { article { <article> } }

after(:create) { |user, context| user.post_first_article(context.article) }
end
```

## Callback context

The `context` parameter provides access to the environment in which the
instance is constructed.

### Transient settings

Transient settings are accessed directly from the `context`:
```ruby
factory :car do
transient { doors { 4 } }

after(:create) do |car, context|
car.update(style: :sedan) if context.doors == 4
car.update(style: :coupe) if context.doors == 2
end
end

car = FactoryBot.create(:car, doors: 2)
car.style #=> :coupe
```

### Strategy used

Sometimes you have a factory that you both `build` and `create` in different tests,
but always want the same code to run at the end.

It's not as simple as adding both `after(:build)` and `after(:create)` callbacks because `after(:create)` also triggers the `after(:build)` callback, so the code would be run twice.

Checking for the strategy used can help skip the `after(:build)` code when the strategy used is `create`.
```ruby
factory :user do
after(:build) { |user, context| run_this_code() if context.strategy.build?}
after(:create) { |user, context| run_this_code() }
end
```

### Defined attributes
Sometimes you need to know if an attribute was provided by the user or defined by the factory. `context` provides a list of the attributes which have been defined: in total; by the user; or by the factory. The list may also be queried for a simgle entry:
```ruby
# based on FactoryBot.build(:car, doors: 2)
factory :car do
transient { transmission { :manual } }

doors { 4 }
seats { 5 }
wheels { 6 }

after(:build) do |car, context|
context.defined_attributes #=> [:doors, :seats, :wheels]
context.user_defined_attributes #=> [:doors]
context.factory_defined_attributes #=> [:seats, :wheels]

context.defined_attributes.wheels? #=> true
context.user_defined_attributes.wheels? #=> false
context.factory_defined_attributes.wheels? #=> true
end
end
```

**Note**: Transient attributes are not included in the list unless they have been provided
by the user.
```ruby
# based on FactoryBot.build(:car, doors: 2, transmission: :automatic)
factory :car do
transient { transmission { :manual } }

doors { 4 }
seats { 5 }
wheels { 6 }

after(:build) do |car, context|
context.defined_attributes #=> [:doors, :seats, :transmission :wheels]
context.user_defined_attributes #=> [:doors, :transmission]
context.factory_defined_attributes #=> [:seats, :wheels]
end
end
```
1 change: 1 addition & 0 deletions lib/factory_bot.rb
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
require "factory_bot/decorator/disallows_duplicates_registry"
require "factory_bot/decorator/invocation_tracker"
require "factory_bot/decorator/new_constructor"
require "factory_bot/inquiry"
require "factory_bot/linter"
require "factory_bot/version"

Expand Down
19 changes: 19 additions & 0 deletions lib/factory_bot/evaluator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ class Evaluator
def initialize(build_strategy, overrides = {})
@build_strategy = build_strategy
@overrides = overrides
@user_overrides = overrides.keys
@cached_attributes = overrides
@instance = nil

Expand All @@ -35,6 +36,24 @@ def association(factory_name, *traits_and_overrides)

attr_accessor :instance

def strategy
@build_strategy.to_sym.to_s.extend(FactoryBot::Inquiry)
rescue NoMethodError # for custom strategies without :to_sym
"unknown".extend(FactoryBot::Inquiry)
end

def defined_attributes
__override_names__.sort.extend(FactoryBot::Inquiry)
end

def user_defined_attributes
@user_overrides.sort.extend(FactoryBot::Inquiry)
end

def factory_defined_attributes
(__override_names__ - @user_overrides).sort.extend(FactoryBot::Inquiry)
end

def method_missing(method_name, ...)
if @instance.respond_to?(method_name)
@instance.send(method_name, ...)
Expand Down
26 changes: 26 additions & 0 deletions lib/factory_bot/inquiry.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
module FactoryBot
module Inquiry
def respond_to_missing?(name, include_private = false)
name.end_with?("?") || super
end

def method_missing(name, ...)
if name.end_with?("?")
fb_inquire(name[0..-2])
else
super
end
end

def fb_inquire(test_value)
case self
when String
self == test_value
when Array
include?(test_value) || include?(test_value.to_sym)
else
false
end
end
end
end
58 changes: 58 additions & 0 deletions spec/factory_bot/evaluator/provided_attributes_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
describe FactoryBot::Evaluator do
context :methods do
before(:all) {
unless defined?(ContextAttributeTest)
class ContextAttributeTest
attr_accessor :name, :age, :admin, :results

def initialize
self.results = {}
end
end
end
}

after(:all) {
if defined?(ContextAttributeTest)
Object.send(:remove_const, :ContextAttributeTest)
end
}

before(:each) {
FactoryBot.define do
factory :context_attribute_test do
transient do
trans_attr { false }
end

name { "John Doh" }
age { 23 }
admin { false }

after(:build) do |object, context|
object.results[:defined_attributes] = context.defined_attributes
object.results[:user_defined_attributes] = context.user_defined_attributes
object.results[:factory_defined_attributes] = context.factory_defined_attributes
end
end
end
}

context ":defined_attributes" do
it "lists all provided attributes" do
obj = FactoryBot.build :context_attribute_test, admin: true, trans_attr: true
expect(obj.results[:defined_attributes]).to eq [:admin, :age, :name, :trans_attr]
end

it "lists the user provided attributes" do
obj = FactoryBot.build :context_attribute_test, admin: true, trans_attr: true
expect(obj.results[:user_defined_attributes]).to eq [:admin, :trans_attr]
end

it "lists the factory provided attributes" do
obj = FactoryBot.build :context_attribute_test, admin: true, trans_attr: true
expect(obj.results[:factory_defined_attributes]).to eq [:age, :name]
end
end
end
end
111 changes: 111 additions & 0 deletions spec/factory_bot/evaluator/strategy_used_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
describe FactoryBot::Evaluator do
context :methods do
context ":stragtegy" do
context "on success" do
context "with FactoryBot::Strategy::Null" do
let(:evaluator) { define_evaluator(FactoryBot::Strategy::Null) }

it "returns the string 'null'" do
expect(evaluator.strategy).to eq "null"
end

it "returns true with the correct inquiry :null?" do
expect(evaluator.strategy).to be_null
end

it "returns false with the incorrect inquiry :build?" do
expect(evaluator.strategy).not_to be_build
end
end

context "with FactoryBot::Strategy::Build" do
let(:evaluator) { define_evaluator(FactoryBot::Strategy::Build) }

it "returns the string 'build'" do
expect(evaluator.strategy).to eq "build"
end

it "returns true with the correct inquiry :build?" do
expect(evaluator.strategy).to be_build
end

it "returns false with an incorrect inquiry :create" do
expect(evaluator.strategy).not_to be_create
end
end

context "with FactoryBot::Strategy::Stub" do
let(:evaluator) { define_evaluator(FactoryBot::Strategy::Stub) }

it "returns the string 'stub'" do
expect(evaluator.strategy).to eq "stub"
end

it "returns true with the correct inquiry :stub?" do
expect(evaluator.strategy).to be_stub
end

it "returns false with the incorrect inquiry :build?" do
expect(evaluator.strategy).not_to be_build
end
end

context "with FactoryBot::Strategy::Create" do
let(:evaluator) { define_evaluator(FactoryBot::Strategy::Create) }

it "returns the string 'create'" do
expect(evaluator.strategy).to eq "create"
end

it "returns true with the correct inquiry :create?" do
expect(evaluator.strategy).to be_create
end

it "returns false with the incorrect inquiry :build?" do
expect(evaluator.strategy).not_to be_build
end
end

context "with FactoryBot::Strategy::AttributesFor" do
let(:evaluator) { define_evaluator(FactoryBot::Strategy::AttributesFor) }

it "returns the string 'attributes_for'" do
expect(evaluator.strategy).to eq "attributes_for"
end

it "returns true with the correct inquiry :attributes_for?" do
expect(evaluator.strategy).to be_attributes_for
end

it "returns false with the incorrect inquiry :build?" do
expect(evaluator.strategy).not_to be_build
end
end
end # on success

context "on failure" do
let(:evaluator) do
strategy = FactoryBot::Strategy::Null.new
allow(strategy).to receive(:to_sym).and_raise(NoMethodError)
FactoryBot::Evaluator.new(strategy)
end

it "returns 'unknown' when strategy does not implement :to_sym" do
expect(evaluator.strategy).to eq "unknown"
end

it "returns true with the correct inquiry :unknown?" do
expect(evaluator.strategy).to be_unknown
end

it "returns false with the incorrect inquiry :build?" do
expect(evaluator.strategy).not_to be_build
end
end # on failure
end
end

def define_evaluator(build_strategy = FactoryBot::Strategy::Null)
FactoryBot::Evaluator.new(build_strategy.new)
end
end
Loading