Skip to content
Browse files

Added support for named blueprints (after many, many requests.)

  • Loading branch information...
1 parent 1f916bb commit d6492e6927a8aa1819926e48b22377171fd20496 @notahat committed Feb 19, 2009
Showing with 69 additions and 43 deletions.
  1. +22 −27 README.markdown
  2. +5 −2 lib/machinist.rb
  3. +14 −13 lib/machinist/active_record.rb
  4. +1 −0 spec/db/schema.rb
  5. +27 −1 spec/machinist_spec.rb
View
49 README.markdown
@@ -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
---
@@ -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 = {})
@ktec
ktec added a note 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.

@ktec
ktec added a note Jan 29, 2010

Just RTFM. Please ignore...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
- 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
-------
View
7 lib/machinist.rb
@@ -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
View
27 lib/machinist/active_record.rb
@@ -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
View
1 spec/db/schema.rb
@@ -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|
View
28 spec/machinist_spec.rb
@@ -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" }
@@ -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

6 comments on commit d6492e6

@adzap
adzap commented on d6492e6 Feb 18, 2009

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

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
Owner

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

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
joho commented on d6492e6 Feb 19, 2009

changing

admin { block_called = true; false }

to
admin { @block_called = true; false }

should do it as well yeah?

@notahat
Owner

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

Please sign in to comment.
Something went wrong with that request. Please try again.