Permalink
Browse files

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

  • Loading branch information...
notahat committed Feb 18, 2009
1 parent 1f916bb commit d6492e6927a8aa1819926e48b22377171fd20496
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
@@ -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 = {})

This comment has been minimized.

Show comment
Hide comment
@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.

@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.

Show comment
Hide comment
@ktec

ktec Jan 29, 2010

Just RTFM. Please ignore...

@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
-------
View
@@ -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
@@ -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
@@ -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
@@ -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

This comment has been minimized.

Show comment
Hide comment
@adzap

adzap Feb 18, 2009

Contributor

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.

Contributor

adzap replied 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

This comment has been minimized.

Show comment
Hide comment
@mapmarkus

mapmarkus Feb 18, 2009

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.

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

This comment has been minimized.

Show comment
Hide comment
@notahat

notahat Feb 18, 2009

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.)

Owner

notahat replied Feb 18, 2009

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

This comment has been minimized.

Show comment
Hide comment
@jnicklas

jnicklas Feb 19, 2009

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
```

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

This comment has been minimized.

Show comment
Hide comment
@joho

joho Feb 19, 2009

changing

admin { block_called = true; false }

to

admin { @block_called = true; false }

should do it as well yeah?

joho replied Feb 19, 2009

changing

admin { block_called = true; false }

to

admin { @block_called = true; false }

should do it as well yeah?

@notahat

This comment has been minimized.

Show comment
Hide comment
@notahat

notahat Feb 19, 2009

Owner

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

Owner

notahat replied Feb 19, 2009

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

Please sign in to comment.