From 1f916bb4b41244b55e7ae5a71703a14492c6a281 Mon Sep 17 00:00:00 2001 From: Pete Yandell Date: Thu, 19 Feb 2009 05:20:16 +1100 Subject: [PATCH] If, in a blueprint, you don't provide a block for an attribute, Machinist 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 --- README.markdown | 123 ++++++++++++++++++++++++----------------- lib/machinist.rb | 8 ++- lib/sham.rb | 4 ++ spec/machinist_spec.rb | 7 +++ spec/sham_spec.rb | 19 ++++--- 5 files changed, 100 insertions(+), 61 deletions(-) diff --git a/README.markdown b/README.markdown index 0a600c8..914cdd1 100644 --- a/README.markdown +++ b/README.markdown @@ -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", @@ -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: @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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? @@ -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 = {}) diff --git a/lib/machinist.rb b/lib/machinist.rb index 0055111..090221e 100644 --- a/lib/machinist.rb +++ b/lib/machinist.rb @@ -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 diff --git a/lib/sham.rb b/lib/sham.rb index 18a33fa..f8adbe8 100644 --- a/lib/sham.rb +++ b/lib/sham.rb @@ -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) diff --git a/spec/machinist_spec.rb b/spec/machinist_spec.rb index e0cbc97..90c32a3 100644 --- a/spec/machinist_spec.rb +++ b/spec/machinist_spec.rb @@ -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 } diff --git a/spec/sham_spec.rb b/spec/sham_spec.rb index 02fc301..e9f7c74 100644 --- a/spec/sham_spec.rb +++ b/spec/sham_spec.rb @@ -1,30 +1,31 @@ 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 } @@ -32,12 +33,16 @@ 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