Skip to content

Commit

Permalink
Added support for named blueprints (after many, many requests.)
Browse files Browse the repository at this point in the history
  • Loading branch information
notahat committed Feb 18, 2009
1 parent 1f916bb commit d6492e6
Show file tree
Hide file tree
Showing 5 changed files with 69 additions and 43 deletions.
49 changes: 22 additions & 27 deletions README.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,28 @@ You can also call plan on has\_many associations, making it easy to test nested
end


### Named Blueprints

Named blueprints let you define variations on an object. For example, suppose some of your Users are administrators:

User.blueprint do
name
email
end

User.blueprint(:admin) do
name { Sham.name + " (admin)" }
admin { true }
end

Calling:

User.make(:admin)

will use the `:admin` blueprint.

Named blueprints call the default blueprint to set any attributes not specifically provided, so in this example the `email` attribute will still be generated even for an admin user.


FAQ
---
Expand All @@ -264,33 +286,6 @@ This will result in Machinist attempting to run ruby's open command. To work aro
self.open { Time.now }
end

### I'm a factory_girl user, and I like having multiple factories for a single model. Can Machinist do the same thing?

Short answer: no.

Machinist blueprints are a little different to factory_girl's factories. Your blueprint should only specify how to generate values for attributes that you don't care about. If you care about an attribute's value, then it doesn't belong in the blueprint.

If you have want to construct objects with similar attributes in a number of tests, just make a test helper. For example:

User.blueprint do
login
password
end

def make_admin_user(attributes = {})

This comment has been minimized.

Copy link
@ktec

ktec Jan 29, 2010

Is it possible to create inheritance in blueprints? This way, rather than a "helper" method I can continue to use the Blueprint DSL and simply create an Admin blueprint which inherrits from User.

This comment has been minimized.

Copy link
@ktec

ktec Jan 29, 2010

Just RTFM. Please ignore...

User.make(attributes.merge(:role => :admin))
end

This keeps the blueprint very clean and generic, and also makes it clear what differentiates an admin user from a generic user.

If you want to make this look a bit cleaner in your tests, you can try the following in your blueprint:

class User
def self.make_admin(attributes = {})
make(attributes.merge(:role => :admin)
end
end


Credits
-------
Expand Down
7 changes: 5 additions & 2 deletions lib/machinist.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,13 @@ module Machinist
#
# The blueprint is instance_eval'd against the Lathe.
class Lathe
def self.run(object, attributes = {})
blueprint = object.class.blueprint
def self.run(object, *args)
blueprint = object.class.blueprint
named_blueprint = object.class.blueprint(args.shift) if args.first.is_a?(Symbol)
attributes = args.pop || {}
raise "No blueprint for class #{object.class}" if blueprint.nil?
returning self.new(object, attributes) do |lathe|
lathe.instance_eval(&named_blueprint) if named_blueprint
lathe.instance_eval(&blueprint)
end
end
Expand Down
27 changes: 14 additions & 13 deletions lib/machinist/active_record.rb
Original file line number Diff line number Diff line change
Expand Up @@ -51,45 +51,46 @@ def self.included(base)
end

module ClassMethods
def blueprint(&blueprint)
@blueprint = blueprint if block_given?
@blueprint
def blueprint(name = :master, &blueprint)
@blueprints ||= {}
@blueprints[name] = blueprint if block_given?
@blueprints[name]
end

def make(attributes = {}, &block)
lathe = Lathe.run(self.new, attributes)
def make(*args, &block)
lathe = Lathe.run(self.new, *args)
unless Machinist::ActiveRecord.nerfed?
lathe.object.save!
lathe.object.reload
end
lathe.object(&block)
end

def make_unsaved(attributes = {})
returning(Machinist::ActiveRecord.with_save_nerfed { make(attributes) }) do |object|
def make_unsaved(*args)
returning(Machinist::ActiveRecord.with_save_nerfed { make(*args) }) do |object|
yield object if block_given?
end
end

def plan(attributes = {})
lathe = Lathe.run(self.new, attributes)
def plan(*args)
lathe = Lathe.run(self.new, *args)
Machinist::ActiveRecord.assigned_attributes_without_associations(lathe)
end
end
end

module BelongsToExtensions
def make(attributes = {}, &block)
lathe = Lathe.run(self.build, attributes)
def make(*args, &block)
lathe = Lathe.run(self.build, *args)
unless Machinist::ActiveRecord.nerfed?
lathe.object.save!
lathe.object.reload
end
lathe.object(&block)
end

def plan(attributes = {})
lathe = Lathe.run(self.build, attributes)
def plan(*args)
lathe = Lathe.run(self.build, *args)
Machinist::ActiveRecord.assigned_attributes_without_associations(lathe)
end
end
Expand Down
1 change: 1 addition & 0 deletions spec/db/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
t.column :name, :string
t.column :type, :string
t.column :password, :string
t.column :admin, :boolean, :default => false
end

create_table :posts, :force => true do |t|
Expand Down
28 changes: 27 additions & 1 deletion spec/machinist_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ class Comment < ActiveRecord::Base
end
Person.make.name.should == "Fred"
end

it "should set an attribute on the constructed object from a block in the blueprint" do
Person.blueprint do
name { "Fred" }
Expand Down Expand Up @@ -119,6 +119,32 @@ class Comment < ActiveRecord::Base
Person.blueprint { type "Person" }
Person.make.type.should == "Person"
end

describe "for named blueprints" do
before do
@block_called = false
Person.blueprint do
name { "Fred" }
admin { block_called = true; false }
end
Person.blueprint(:admin) do
admin { true }
end
@person = Person.make(:admin)
end

it "should override an attribute from the parent blueprint in the child blueprint" do
@person.admin.should == true
end

it "should not call the block for an attribute from the parent blueprint if that attribute is overridden in the child" do
@block_called.should be_false
end

it "should set an attribute defined in the parent blueprint" do
@person.name.should == "Fred"
end
end

end # make method

Expand Down

6 comments on commit d6492e6

@adzap
Copy link
Contributor

@adzap adzap commented on d6492e6 Feb 18, 2009

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I resisted the temptation to implement this myself to force myself to think about the blueprints differently from fixtures. It has the potential to lead you back to fixture mindset I think.

Using the master as the base for a named blueprint’s undefined attributes is great idea. This makes it more macro-like rather than fixture.

@mapmarkus
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You have implemented a “default” feature too, great!

About the discussion of Machinist being more fixtures like, I disagree. The basic functionality remains untouched. The fact that you can create named blueprints doesn’t involve that you must use named blueprint. For most models, the basic idea of 1 model : 1 blueprint works awesomely, it’s fast, it’s easy to change, to read, to manage, it’s clean and it does everything for you, without those dumb yml files everywhere.

But this change, as I see it, it’s necessary. Necessary for testing large models, in which ‘fake’ data doesn’t work so well (user roles, for example). They were hard to test without a way to name blueprints, since you must repeat the attribute values for a specific tests too many times.

Now it will be the same as before, but with an option for complex models to use named blueprints.

@notahat
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was resistant to adding this feature precisely because it encourages fixture-like coding, and almost all the use cases people gave me were bad practice.

The “variations on a theme” use case (e.g. users with different roles) is a good one however. I think encouraging a “master” blueprint and then using the named blueprints to specify just the differences for each variation keeps things tidy.

I can still user plain old User.make if I don’t care about the role, and working out what differentiates a user in a particular role from a generic user is easy (as opposed to the same sort of thing done with fixtures.)

@jnicklas
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There’s an error in the spec, @block_called will never be set to true, even if the block is run, since a local variable is set in the block. You could change it to:

```
before do
block_called = false
Person.blueprint do
name { “Fred” }
admin { block_called = true; false }
end
Person.blueprint(:admin) do
admin { true }
end
@person = Person.make(:admin)
@block_called = block_called
end
```

@joho
Copy link

@joho joho commented on d6492e6 Feb 19, 2009

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

changing

admin { block_called = true; false }

to

admin { @block_called = true; false }

should do it as well yeah?

@notahat
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You’re right guys. Well spotted. I’ll fix it.

Please sign in to comment.