Skip to content

Commit

Permalink
If, in a blueprint, you don't provide a block for an attribute, Machi…
Browse files Browse the repository at this point in the history
…nist will default to trying to find a Sham definition with the same name as an attribute.

Whereas before there'd be a lot of this sort of thing:

    Post.blueprint do
      title { Sham.title }
      body  { Sham.body }
    end

you can now shorten this to:

    Post.blueprint do
      title
      body
    end
  • Loading branch information
notahat committed Feb 18, 2009
1 parent 25fa064 commit 1f916bb
Show file tree
Hide file tree
Showing 5 changed files with 100 additions and 61 deletions.
123 changes: 71 additions & 52 deletions README.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ Machinist lets you construct test data on the fly, but instead of doing this:

describe Comment do
before do
@user = User.create!(:name => "Test User")
@user = User.create!(:name => "Test User", :email => "user@example.com")
@post = Post.create!(:title => "Test Post", :author => @user, :body => "Lorem ipsum...")
@comment = Comment.create!(
:post => @post, :author_name => "Test Commenter", :author_email => "commenter@example.com",
Expand All @@ -32,7 +32,7 @@ you can just do this:
end
end

Machinist generates data for the fields you don't care about, and constructs any necessary associated objects.
Machinist generates data for the fields you don't care about, and constructs any necessary associated objects, leaving you to only specify the fields you *do* care about in your tests.

You tell Machinist how to do this with blueprints:

Expand All @@ -44,20 +44,21 @@ You tell Machinist how to do this with blueprints:
Sham.body { Faker::Lorem.paragraph }

User.blueprint do
name { Sham.name }
name
email
end

Post.blueprint do
title { Sham.title }
author { User.make }
body { Sham.body }
title
author
body
end

Comment.blueprint do
post
author_name { Sham.name }
author_email { Sham.email }
body { Sham.body }
body
end


Expand Down Expand Up @@ -103,7 +104,7 @@ Then, to generate a name, call:

Sham.name

So why not just define a method? Sham ensures two things for you:
So why not just define a helper method to do this? Sham ensures two things for you:

1. You get the same sequence of values each time your test is run
2. You don't get any duplicate values
Expand All @@ -120,51 +121,69 @@ If you want to allow duplicate values for a sham, you can pass the `:unique` opt

Sham.coin_toss(:unique => false) { rand(2) == 0 ? 'heads' : 'tails' }

You can define a bunch of sham methods in one hit like this:
You can create a bunch of sham definitions in one hit like this:

Sham.define do
name { Faker::Name.name }
email_address { Faker::Internet.email }
title { Faker::Lorem.words(5).join(' ') }
name { Faker::Name.name }
body { Faker::Lorem.paragraphs(3).join("\n\n") }
end


Blueprints - Generating ActiveRecord Objects
--------------------------------------------

A blueprint describes how to build a generic object for an ActiveRecord model. The idea is that you let the blueprint take care of constructing all the objects and attributes that you don't care about in your test, leaving you to focus on the just the things that you're testing.
A blueprint describes how to generate an ActiveRecord object. The idea is that you let the blueprint take care of making up values for attributes that you don't care about in your test, leaving you to focus on the just the things that you're testing.

A simple blueprint might look like this:

Comment.blueprint do
body "A comment!"
Post.blueprint do
title { Sham.title }
author { Sham.name }
body { Sham.body }
end

Once that's defined, you can construct a comment from this blueprint with:
You can then construct a Post from this blueprint with:

Comment.make
Post.make

Machinist calls `save!` on your ActiveRecord model to create the comment, so it will throw an exception if the blueprint doesn't pass your validations. It also calls `reload` after the `save!`.
When you call `make`, Machinist calls Post.new, then runs through the attributes in your blueprint, calling the block for each attribute to generate a value. It then calls `save!` and `reload` on the Post.

You can override values defined in the blueprint by passing parameters to make:
You can override values defined in the blueprint by passing a hash to make:

Comment.make(:body => "A different comment!")
Post.make(:title => "A Specific Title")

`make` doesn't call the blueprint blocks of any attributes that are passed in.

Rather than providing a constant value for an attribute, you can use Sham to generate a value for each new object:
If you don't supply a block for an attribute in the blueprint, Machinist will look for a Sham definition with the same name as the attribute, so you can shorten the above blueprint to:

Sham.body { Faker::Lorem.paragraph }
Comment.blueprint do
body { Sham.body }
Post.blueprint do
title
author { Sham.name }
body
end

Notice the curly braces around `Sham.body`. If you call `Comment.make` with your own body attribute, this block will not be executed.
If you want to generate an object without saving it to the database, replace `make` with `make_unsaved`. (`make_unsaved` also ensures that any associated objects that need to be generated are not saved. See the section on associations below.)


### Belongs\_to Associations

You can use this same syntax to generate objects through belongs_to associations:
You can generate an associated object like this:

Comment.blueprint do
post { Post.make }
end

If you're assigning an associated object this way, Machinist is smart enough to look at the association and work out what sort of object it needs to create, so you can just write:
Calling `Comment.make` will construct a Comment and its associated Post, and save both.

If you want to override the value for post when constructing the comment, you can:

post = Post.make(:title => "A particular title)
comment = Comment.make(:post => post)

Machinist will not call the blueprint block for the post attribute, so this won't generate two posts.

Machinist is smart enough to look at the association and work out what sort of object it needs to create, so you can shorten the above blueprint to:

Comment.blueprint do
post
Expand All @@ -174,27 +193,36 @@ You can refer to already assigned attributes when constructing a new attribute:

Comment.blueprint do
post
body { "Comment on " + post.name }
body { "Comment on " + post.title }
end

You can also override associated objects when calling make:

### Other Associations

post = Post.make
3.times { Comment.make(:post => post) }
For has\_many and has\_and\_belongs\_to\_many associations, ActiveRecord insists that the object be saved before any associated objects can be saved. That means you can't generate the associated objects from within the blueprint.


Note that make can take a block, into which it will pass the newly constructed object.
The simplest solution is to write a test helper:

def make_post_with_comments(attributes = {})
post = Post.make(attributes)
3.times { post.comments.make }
post
end

If you want to generate an object graph without saving to the database, use make\_unsaved:
Note here that you can call `make` on a has\_many association, as well as on an ActiveRecord subclass.

Comment.make_unsaved

This will generate both the Comment and the associated Post without saving either.
Make can take a block, into which is passes the constructed object, so the above can be written as:

def make_post_with_comments
Post.make(attributes) do |post|
3.times { post.comments.make }
end
end


### Using Blueprints in Rails Controller Tests

The plan method behaves like make, except it returns a hash of attributes, rather than saving the object. This is useful for passing in to controller tests:
The `plan` method behaves like `make`, except it returns a hash of attributes, and doesn't save the object. This is useful for passing in to controller tests:

test "should create post" do
assert_difference('Post.count') do
Expand All @@ -203,7 +231,9 @@ The plan method behaves like make, except it returns a hash of attributes, rathe
assert_redirected_to post_path(assigns(:post))
end

You an also call plan on ActiveRecord associations, making it easy to test nested controllers:
`plan` will save any associated objects. In this example, it will create an Author, and it knows that the controller expects an `author\_id` attribute, rather than an `author` attribute, and makes this translation for you.

You can also call plan on has\_many associations, making it easy to test nested controllers:

test "should create comment" do
post = Post.make
Expand All @@ -214,20 +244,9 @@ You an also call plan on ActiveRecord associations, making it easy to test neste
end



FAQ
---

### How do I construct associated object through has\_many/has\_and\_belongs\_to\_many associations in a blueprint?

Machinist isn't smart about has_many associations, and ActiveRecord requirements around save order make this stuff tricky.

The best way to do it is to simply create a test helper that constructs your object graph. For example:

def make_post_with_comments(attributes = {})
Post.make(attributes) do |post|
3.times { Comment.make(:post => post) }
end
end

### My blueprint is giving me really weird errors. Any ideas?

Expand All @@ -254,8 +273,8 @@ Machinist blueprints are a little different to factory_girl's factories. Your bl
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 { Sham.login }
password { Sham.password }
login
password
end

def make_admin_user(attributes = {})
Expand Down
8 changes: 6 additions & 2 deletions lib/machinist.rb
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,12 @@ def generate_attribute(attribute, args)
value = if block_given?
yield
elsif args.empty?
klass = @object.class.reflect_on_association(attribute).class_name.constantize
klass.make(args.first || {})
association = @object.class.reflect_on_association(attribute)
if association
association.class_name.constantize.make(args.first || {})
else
Sham.send(attribute)
end
else
args.first
end
Expand Down
4 changes: 4 additions & 0 deletions lib/sham.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ def self.method_missing(symbol, *args, &block)
sham.fetch_value
end
end

def self.clear
@@shams = {}
end

def self.reset
@@shams.values.each(&:reset)
Expand Down
7 changes: 7 additions & 0 deletions spec/machinist_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,13 @@ class Comment < ActiveRecord::Base
Person.make.name.should == "Fred"
end

it "should default to calling Sham for an attribute in the blueprint" do
Sham.clear
Sham.name { "Fred" }
Person.blueprint { name }
Person.make.name.should == "Fred"
end

it "should let the blueprint override an attribute with a default value" do
Post.blueprint do
published { false }
Expand Down
19 changes: 12 additions & 7 deletions spec/sham_spec.rb
Original file line number Diff line number Diff line change
@@ -1,43 +1,48 @@
require File.dirname(__FILE__) + '/spec_helper'
require 'sham'

Sham.random { rand }
Sham.half_index {|index| index/2 }
Sham.coin_toss(:unique => false) {|index| index % 2 == 1 ? 'heads' : 'tails' }
Sham.limited {|index| index%10 }
Sham.index {|index| index }
Sham.name {|index| index }

describe Sham do
it "should ensure generated values are unique" do
Sham.clear
Sham.half_index {|index| index/2 }
values = (1..10).map { Sham.half_index }
values.should == (0..9).to_a
end

it "should generate non-unique values when asked" do
Sham.clear
Sham.coin_toss(:unique => false) {|index| index % 2 == 1 ? 'heads' : 'tails' }
values = (1..4).map { Sham.coin_toss }
values.should == ['heads', 'tails', 'heads', 'tails']
end

it "should generate more than a dozen values" do
Sham.clear
Sham.index {|index| index }
values = (1..25).map { Sham.index }
values.should == (1..25).to_a
end

it "should generate the same sequence of values after a reset" do
Sham.clear
Sham.random { rand }
values1 = (1..10).map { Sham.random }
Sham.reset
values2 = (1..10).map { Sham.random }
values2.should == values1
end

it "should die when it runs out of unique values" do
Sham.clear
Sham.limited {|index| index%10 }
lambda {
(1..100).map { Sham.limited }
}.should raise_error(RuntimeError)
end

it "should allow over-riding the name method" do
Sham.clear
Sham.name {|index| index }
Sham.name.should == 1
end

Expand Down

0 comments on commit 1f916bb

Please sign in to comment.