merge upstream

This commit is contained in:
Reinier Balt 2008-06-24 20:35:30 +02:00
parent c58186451f
commit ce1c092173
72 changed files with 4469 additions and 0 deletions

196
spec/models/todo_spec.rb Normal file
View file

@ -0,0 +1,196 @@
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
describe Todo do
def valid_attributes(attributes={})
{
:description => "don't forget the milk",
:context => mock_model(Context, :name => 'errands')
}.merge(attributes)
end
def create_todo(attributes={})
todo = Todo.new(valid_attributes(attributes))
todo.stub!(:user).and_return(mock_model(User, :date => Time.now))
todo.save!
todo
end
before(:each) do
@todo = Todo.new
end
it_should_belong_to :context
it_should_belong_to :project
it_should_belong_to :user
it_should_validate_presence_of :description
it_should_validate_presence_of :context
it_should_validate_length_of :description, :maximum => 100
it_should_validate_length_of :notes, :maximum => 60_000
it 'validates presence of show_from when deferred'
it 'ensures that show_from is a date in the future' do
todo = Todo.new(valid_attributes)
todo.stub!(:user).and_return(mock_model(User, :date => Time.now))
todo.show_from = 3.days.ago
todo.should have(1).error_on(:show_from)
end
it 'allows show_from to be blank' do
todo = Todo.new(valid_attributes(:show_from => ''))
todo.should_not have(:any).error_on(:show_from)
end
describe 'states' do
it 'is active on initializing' do
create_todo.should be_active
end
it 'is deferred when show from is in the future' do
create_todo(:show_from => 1.week.from_now).should be_deferred
end
describe 'active' do
%w(project_hidden completed deferred).each do |from_state|
it "is activable from `#{from_state}'" do
todo = create_todo
todo.state = from_state
todo.send("#{from_state}?").should be_true
todo.activate!
todo.should be_active
end
end
it 'clears show_from when entering active state' do
todo = create_todo
todo.show_from = 3.days.from_now
todo.should be_deferred
todo.activate!
todo.should be_active
todo.show_from.should be_nil
end
it 'clears completed_at when entering active state' do
todo = create_todo
todo.complete!
todo.should be_completed
todo.activate!
todo.should be_active
todo.completed_at.should be_nil
end
end
describe 'completed' do
%w(active project_hidden deferred).each do |from_state|
it "is completable from `#{from_state}'" do
todo = create_todo
todo.state = from_state
todo.send("#{from_state}?").should be_true
todo.complete!
todo.should be_completed
end
end
it 'sets complated_at' do
todo = create_todo
todo.complete!
todo.completed_at.should_not be_nil
end
end
describe 'project_hidden' do
%w(active deferred).each do |from_state|
it "is hiddable from `#{from_state}'" do
todo = create_todo
todo.state = from_state
todo.send("#{from_state}?").should be_true
todo.hide!
todo.should be_project_hidden
end
end
it 'unhides to deferred when if show_from' do
todo = create_todo(:show_from => 4.days.from_now)
todo.hide!
todo.should be_project_hidden
todo.unhide!
todo.should be_deferred
end
it 'unhides to active when not show_from' do
todo = create_todo(:show_from => '')
todo.hide!
todo.should be_project_hidden
todo.unhide!
todo.should be_active
end
end
it "is deferrable from `active'" do
todo = create_todo
todo.activate!
todo.should be_active
todo.defer!
todo.should be_deferred
end
end
describe 'when toggling completion' do
it 'toggles to active when completed' do
todo = create_todo
todo.complete!
todo.should be_completed
todo.toggle_completion!
todo.should be_active
end
it 'toggles to completed when not completed' do
todo = create_todo
todo.should_not be_completed
todo.toggle_completion!
todo.should be_completed
end
end
describe 'when retrieving project' do
it 'returns project if set' do
project = mock_model(Project)
todo = Todo.new(:project => project)
todo.project.should == project
end
it 'returns a NullProject if not set' do
Todo.new.project.should be_an_instance_of(NullProject)
end
end
describe('when setting show_from') { it 'is speced' }
it 'is starred if tag is "starred"' do
todo = create_todo
todo.should_not be_starred
todo.add_tag('starred')
todo.reload
todo.should be_starred
end
describe 'when toggling star flag' do
it 'toggles to not starred when starred' do
todo = create_todo
todo.add_tag('starred')
todo.should be_starred
todo.toggle_star!
todo.reload
todo.should_not be_starred
end
it 'toggles to starred when not starred' do
todo = create_todo
todo.should_not be_starred
todo.toggle_star!
todo.reload
todo.should be_starred
end
end
end

181
spec/models/user_spec.rb Normal file
View file

@ -0,0 +1,181 @@
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
describe User do
def valid_attributes(attributes={})
{
:login => 'simon',
:password => 'foobarspam',
:password_confirmation => 'foobarspam'
}.merge(attributes)
end
before(:each) do
@user = User.new
end
describe 'associations' do
it 'has many contexts' do
User.should have_many(:contexts).
with_order('position ASC').
with_dependent(:delete_all)
end
it 'has many projects' do
User.should have_many(:projects).
with_order('projects.position ASC').
with_dependent(:delete_all)
end
# TODO: uses fixtures to test those
it 'has many active_projects' do
User.should have_many(:active_projects).
with_order('projects.position ASC').
with_conditions('state = ?', 'active').
with_class_name('Project')
end
it 'has many active contexts' do
User.should have_many(:active_contexts).
with_order('position ASC').
with_conditions('hide = ?', 'true').
with_class_name('Context')
end
it 'has many todos' do
User.should have_many(:todos).
with_order('todos.completed_at DESC, todos.created_at DESC').
with_dependent(:delete_all)
end
it 'has many deferred todos' do
User.should have_many(:deferred_todos).
with_order('show_from ASC, todos.created_at DESC').
with_conditions('state = ?', 'deferred').
with_class_name('Todo')
end
it 'has many completed todos' do
User.should have_many(:completed_todos).
with_order('todos.completed_at DESC').
with_conditions('todos.state = ? and todos.completed_at is not null', 'completed').
with_include(:project, :context).
with_class_name('Todo')
end
it 'has many notes' do
User.should have_many(:notes).
with_order('created_at DESC').
with_dependent(:delete_all)
end
it 'has many taggings' do
User.should have_many(:taggings)
end
it 'has many tags through taggings' do
User.should have_many(:tags).through(:taggings).with_select('DISTINCT tags.*')
end
it 'has one preference' do
User.should have_one(:preference)
end
end
it_should_validate_presence_of :login
it_should_validate_presence_of :password
it_should_validate_presence_of :password_confirmation
it_should_validate_length_of :password, :within => 5..40
it_should_validate_length_of :login, :within => 3..80
it_should_validate_uniqueness_of :login
it_should_validate_confirmation_of :password
it 'validates presence of password only when password is required'
it 'validates presence of password_confirmation only when password is required'
it 'validates confirmation of password only when password is required'
it 'validates presence of open_id_url only when using openid'
it 'accepts only allow auth_type authorized by the admin' do
Tracks::Config.should_receive(:auth_schemes).exactly(3).times.and_return(%w(database open_id))
User.new(valid_attributes(:auth_type => 'database')).should_not have(:any).error_on(:auth_type)
User.new(valid_attributes(:auth_type => 'open_id')).should_not have(:any).error_on(:auth_type)
User.new(valid_attributes(:auth_type => 'ldap')).should have(1).error_on(:auth_type)
end
it 'returns login for #to_param' do
@user.login = 'john'
@user.to_param.should == 'john'
end
it 'has a custom finder to find admin' do
User.should_receive(:find).with(:first, :conditions => ['is_admin = ?', true])
User.find_admin
end
it 'has a custom finder to find by openid url'
it 'knows if there is any user through #no_users_yet? (TODO: better description)'
describe 'when choosing what do display as a name' do
it 'displays login when no first name and last name' do
User.new(valid_attributes).display_name.should == 'simon'
end
it 'displays last name when no first name' do
User.new(valid_attributes(:last_name => 'foo')).display_name.should == 'foo'
end
it 'displays first name when no last name' do
User.new(valid_attributes(:first_name => 'bar')).display_name.should == 'bar'
end
it 'displays first name and last name when both specified' do
User.new(valid_attributes(:first_name => 'foo', :last_name => 'bar')).display_name.should == 'foo bar'
end
end
describe 'authentication' do
before(:each) do
@user = User.create!(valid_attributes)
end
it 'authenticates user' do
User.authenticate('simon', 'foobarspam').should == @user
end
it 'resets password' do
@user.update_attributes(
:password => 'new password',
:password_confirmation => 'new password'
)
User.authenticate('simon', 'new password').should == @user
end
it 'does not rehash password after update of login' do
@user.update_attribute(:login, 'foobar')
User.authenticate('foobar', 'foobarspam').should == @user
end
it 'sets remember token' do
@user.remember_me
@user.remember_token.should_not be_nil
@user.remember_token_expires_at.should_not be_nil
end
it 'unsets remember token' do
@user.remember_me
@user.remember_token.should_not be_nil
@user.forget_me
@user.remember_token.should be_nil
end
it 'remembers me default two weeks' do
before = 2.weeks.from_now.utc
@user.remember_me
after = 2.weeks.from_now.utc
@user.remember_token.should_not be_nil
@user.remember_token_expires_at.should_not be_nil
@user.remember_token_expires_at.should be_between(before, after)
end
end
end

View file

@ -0,0 +1,19 @@
class ContextsScenario < Scenario::Base
uses :users
def load
%w(Call Email Errand Someday).each_with_index do |context, index|
create_context context, index+1
end
end
def create_context(name, position)
create_model :context, name.downcase.to_sym,
:name => name,
:position => position,
:hide => name == 'Someday' ? true : false,
:created_at => Time.now,
:updated_at => Time.now,
:user_id => user_id(:sean)
end
end

View file

@ -0,0 +1,20 @@
class ProjectsScenario < Scenario::Base
def load
create_project :build_time_machine, 'Build a working time machine'
create_project :make_more_money, 'Make more money than Billy Gates'
create_project :evict_dinosaurs, 'Evict dinosaurs from the garden'
create_project :attend_railsconf, 'Attend RailsConf'
end
def create_project(identifier, name)
attributes = {
:name => name,
:state => 'active',
:created_at => 4.day.ago,
:updated_at => 1.minute.ago
}
create_model :project,
identifier || attributes[:name].split.first.downcase.to_sym,
attributes
end
end

View file

@ -0,0 +1,30 @@
class TodosScenario < Scenario::Base
uses :contexts, :projects, :users
def load
create_todo :bill,
:description => 'Call Bill Gates to find out how much he makes per day',
:user => :sean,
:context => :call,
:project => :make_more_money
create_todo :bank,
:description => 'Call my bank',
:user => :sean,
:context => :call,
:project => :make_more_money
end
def create_todo(identifier, options={})
context = options.delete(:context)
project = options.delete(:project)
user = options.delete(:user)
attributes = {
:state => 'active',
:created_at => 1.week.ago,
:context_id => context_id(context),
:project_id => project_id(project),
:user_id => user_id(user)
}.merge(options)
create_model :todo, identifier, attributes
end
end

View file

@ -0,0 +1,19 @@
class UsersScenario < Scenario::Base
def load
create_user :login => 'johnny', :first_name => 'Johnny', :last_name => 'Smith'
create_user :login => 'jane', :first_name => 'Jane', :last_name => 'Pilbeam'
create_user :login => 'sean', :first_name => 'Sean', :last_name => 'Pallmer'
end
def create_user(attributes={})
password = attributes[:login] + Time.now.to_s
attributes = {
:password => password,
:password_confirmation => password,
:is_admin => attributes[:is_admin] || false,
}.merge(attributes)
identifier = attributes[:login].downcase.to_sym
user = create_model :user, identifier, attributes
Preference.create(:show_number_completed => 5, :user => user)
end
end

4
vendor/plugins/scenarios/.gitignore vendored Normal file
View file

@ -0,0 +1,4 @@
environments
*.log
tmp
vendor

19
vendor/plugins/scenarios/LICENSE vendored Normal file
View file

@ -0,0 +1,19 @@
Copyright (c) 2007, Adam Williams and John W. Long.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

262
vendor/plugins/scenarios/README vendored Normal file
View file

@ -0,0 +1,262 @@
== Rails Scenarios Plugin
Who hasn't experienced the pain of dozens of YAML files filled with hundreds
of inter-related data structures? When do you look at People of an
Organization and not have to look at the organization_id, open the
organizations.yml file, and search for 'id: X'?
In a nutshell, scenarios are a drop in replacement for YAML fixtures. Instead
of encouraging you to create a mindless amount of raw data in the form of
YAML, scenarios encourage you to create code that populates your tables with
the appropriate records.
How is it different from other solutions? A few things:
* It continues to provide a fundamental, fast insertion method using attributes
written directly to a table. This is the
Scenarios::TableMethods#create_record method.
* It allows you to create records using validations if you prefer, or if it's
important to have all your callbacks be invoked. See
Scenarios::TableMethods#create_model. Both create_record and create_model
allow you to name your instances for retrieval by the instance and id reader
methods (more below).
* Nothing stops you from simply invoking YouModel.create!, etc. We'll still
keep track of the tables the scenario modifies and clean things up afterward.
* It allows you to create re-usable scenarios as classes. These classes are
like any other class - they may include modules, subclass, and be composed of
other scenarios. See Scenarios::Base.uses. This also means that you can load
any scenario into any Rails environment. That's what the 'rake
db:scenario:load' task is good for (more below). Very handy for re-using all
that test support code to create populated demos!
=== Quick Start
Since Scenarios is a Rails plugin at this time, you should get it installed,
using the appropriate method (script/plugin, svn, piston) into your
vendor/plugins directory. Once you have this, in your spec_helper.rb or
test_helper.rb, add the following line after the spec requires:
require 'scenarios'
The Scenarios you write should be placed in the spec/scenarios directory of your
Rails project if you're using RSpec, or the test/scenarios directory of your
Rails project if you're using Test::Unit. Scenario file names always end in
"_scenario.rb" and classes end in "Scenario".
A simple scenario looks like this:
# in spec/scenarios/users_scenario.rb or test/scenarios/users_scenario.rb
class UsersScenario < Scenario::Base
def load
create_record :user, :john, :name => 'John', :password => 'doodaht'
create_record :user, :cindy, :name => 'Cindy', :password => 'whoot!'
end
end
In the example above, I'm using the <tt>create_record</tt> instance method to
create two users: John and Cindy. Notice the calls to <tt>create_record</tt>.
There are three parameters. The first is the singular name of the table to
insert the record into, the second is the symbolic name of the record (more on
that later), and the third is a hash of the attributes of the record.
To use the UsersScenario in a description, you should declare it using
the <tt>scenario</tt> method. Here it is within a spec file (RSpec):
# in spec/models/user_spec.rb
describe User do
scenario :users
it "should allow me to do something with John" do
user = users(:john)
user.password.should == "doodaht"
end
end
and here it is within a standard Test::Unit test:
# in test/unit/user_test.rb
class UserTest < Test::Unit::TestCase
scenario :users
def test_do_something
user = users(:john)
assert_equal "doodaht", user.password
end
end
Notice that it is easy to load an instance of a model object using its
symbolic name with a reader method, similar to that of Rails' fixtures. In the
example above, I loaded John with the reader method <tt>users</tt> and the
symbolic name <tt>:john</tt>. (Remember that in the Users scenario I declared
that John should be accessible through the symbolic name <tt>:john</tt>.)
I could also have retrieved an array of user fixtures by passing in
multiple symbolic names to the reader method:
# in spec/models/user_spec.rb
describe User do
scenario :users
it "should allow me to get all admins" do
admins = users(:john, :ryan)
User.admins.should eql(admins)
end
end
=== Composition
In real life your scenarios will probably grow quite complicated. The
scenarios plugin allows you to deal with this complexity through composition.
Here's a simple example:
# in spec/scenarios/posts_scenario.rb or test/scenarios/posts_scenario.rb
class PostsScenario < Scenario::Base
def load
create_record :post, :first, :title => "First Post"
create_record :post, :second, :title => "Second Post"
end
end
# in spec/scenarios/comments_scenario.rb or test/scenarios/comments_scenario.rb
class CommentsScenario < Scenario::Base
uses :posts
def load
create_record :comment, :first, :body => "Nice post!", :post_id => post_id(:first)
create_record :comment, :second, :body => "I like it.", :post_id => post_id(:first)
create_record :comment, :third, :body => "I thoroughly disagree.", :post_id => post_id(:second)
end
end
In the example above, the CommentsScenario declares that it depends on the
Posts scenario with the <tt>uses</tt> class method. This means that if you
load the CommentsScenario, the PostsScenario will be loaded first and the
CommentsScenario will have access to all the data loaded by the PostsScenario
in its own <tt>load</tt> method. Note that inside the load method I'm using
another form of reader methed which simply gives you the id for a symbolic
name (in this case: <tt>post_id</tt>). This is most useful for making
associations, as done here with comments and posts.
=== Helper Methods
Another way of simplifying your scenarios and specs/tests is through helper
methods. The Scenarios plugin provides a handy way to declare helper methods
that are accessible from inside the scenario and also from inside related
RSpec/Test::Unit examples:
# in spec/scenarios/users_scenario.rb or test/scenarios/users_scenario.rb
class UsersScenario < Scenario::Base
def load
create_user :name => "John"
end
helpers do
def create_user(attributes={})
create_record :user, attributes[:name].downcase.intern, attributes
end
def login_as(user)
@request.session[:user_id] = user.id
end
end
end
Helper methods declared inside the helpers block are mixed into the scenario
when it is instantiated and mixed into examples that declare that they are using
the scenario. Also, in the case where one scenario <tt>uses</tt> another, the
using scenario will have the helper methods of the used scenario.
# in spec/controllers/projects_controller_spec.rb
describe "Projects screen" do
scenario :users
it "should show active projects" do
login_as(users(:john))
get :projects
@response.should have_tag('#active_projects')
end
end
# in test/functional/projects_controller_test.rb
class PeopleControllerTest < Test::Unit::TestCase
scenario :users
def test_index
login_as(users(:john))
get :projects
assert_tag('#active_projects')
end
end
Notice that within my specs/tests I have access to the login_as helper method
declared inside the <tt>helpers</tt> block of the UsersScenario. Scenario
helpers are a great way to share helper methods between specs/tests that use a
specific scenario.
=== Built-in Scenario
There is a scenario named 'blank' that comes with the plugin. This scenario is
useful when you want to express, and guarantee, that the database is empty. It
works by using your db/schema.rb, so if the table isn't created in there, it
won't be cleaned up.
Scenario.load_paths is an array of the locations to look for scenario
definitions. The built-in scenarios directory is consulted last, so if you'd
like to re-define, for instance, the 'blank' scenario, simply create
'blank_scenario.rb' in your spec/scenarios or test/scenarios directory.
=== Load Rake Task
The Scenarios plugin provides a single Rake task, <tt>db:scenario:load</tt>,
which you may use in a fashion similar to Rails fixtures'
<tt>db:fixtures:load</tt>.
rake db:scenario:load SCENARIO=comments
When invoked, this task will populate the development database with the named
scenario.
If you do not specify SCENARIO, the task will expect to find a default scenario
(a file 'default_scenario.rb' having DefaultScenario defined in it). It is our
practice to have it such that this scenario <tt>uses</tt> a number of our other
scenarios, thereby:
* encouraging us to use test data that looks good in the running development
application
* allowing us to troubleshoot failing tests in the running development
application
=== More Information
For more information, be sure to look through the documentation over at RubyForge:
* http://faithfulcode.rubyforge.org/docs/scenarios
You might also enjoy taking a look at the specs for the plugin and the example
scenarios:
* http://faithfulcode.rubyforge.org/svn/plugins/trunk/scenarios/spec/scenarios_spec.rb
* http://faithfulcode.rubyforge.org/svn/plugins/trunk/scenarios/spec/scenarios
Browse the complete source code:
* http://faithfulcode.rubyforge.org/svn/plugins/trunk/scenarios
=== Running Tests
You should be able to simply run rake. Notice in testing/environment.rb the
revisions under which this project will work. If you intend to test against
HEAD, you will need to delete the directory testing/tmp/trunk/HEAD. At some
point, it would be nice to have the script track the revision of HEAD that we
have, and update the directory automatically.
=== License
The Scenarios plugin is released under the MIT-License and is Copyright (c)
2007, Adam Williams and John W. Long. Special thanks to Chris Redinger for his
part in helping us get this plugin ready for the public.

10
vendor/plugins/scenarios/Rakefile vendored Normal file
View file

@ -0,0 +1,10 @@
require File.expand_path(File.dirname(__FILE__) + '/testing/plugit_descriptor')
require 'rake/rdoctask'
Rake::RDocTask.new(:doc) do |r|
r.title = "Rails Scenarios Plugin"
r.main = "README"
r.options << "--line-numbers"
r.rdoc_files.include("README", "LICENSE", "lib/**/*.rb")
r.rdoc_dir = "doc"
end

1
vendor/plugins/scenarios/TODO vendored Normal file
View file

@ -0,0 +1 @@
Make sure before :all's that use scenario methods work. They don't right now.

127
vendor/plugins/scenarios/helpers.diff vendored Normal file
View file

@ -0,0 +1,127 @@
Only in /Users/aiwilliams/Workspaces/faithfulcode/scenarios/: .git
Only in scenarios/: .svn
Only in scenarios/: .tm_last_run_ruby
diff -r scenarios/Rakefile /Users/aiwilliams/Workspaces/faithfulcode/scenarios/Rakefile
3c3
< TESTING_ENVIRONMENTS["rspec_3317_rails_8956"].load
---
> TESTING_ENVIRONMENTS["rspec_3119_rails_8375"].load
Only in /Users/aiwilliams/Workspaces/faithfulcode/scenarios/: helpers.diff
Only in scenarios/lib: .svn
Only in scenarios/lib/scenarios: .svn
diff -r scenarios/lib/scenarios/base.rb /Users/aiwilliams/Workspaces/faithfulcode/scenarios/lib/scenarios/base.rb
11,14c11,12
< # be included into the scenario and all specs that include the scenario.
< # You may also provide names of helpers from your scenarios/helpers
< # directory, or any other module you'd like included in your Scenario.
< def helpers(helper_names_or_modules = [], &block)
---
> # be included into the scenario and all specs that include the scenario
> def helpers(&block)
17,19d14
< mod.module_eval do
< [helper_names_or_modules].flatten.each {|h| include h.is_a?(Module) ? h : h.to_scenario_helper}
< end
Only in scenarios/lib/scenarios/builtin: .svn
Only in scenarios/lib/scenarios/extensions: .svn
diff -r scenarios/lib/scenarios/extensions/string.rb /Users/aiwilliams/Workspaces/faithfulcode/scenarios/lib/scenarios/extensions/string.rb
22,39d21
< # Convert a string into the associated scenario helper module:
< #
< # "basic".to_scenario_helper #=> BasicScenarioHelper
< # "basic_scenario".to_scenario_helper #=> BasicScenarioHelper
< #
< # Raises Scenario::NameError if the the helper cannot be loacated as
< # 'helpers/<name>_helper' in Scenario.load_paths.
< def to_scenario_helper
< class_name = "#{self.strip.camelize.sub(/ScenarioHelper$/, '')}ScenarioHelper"
< Scenario.load_paths.each do |path|
< filename = "#{path}/#{class_name.underscore}.rb"
< if File.file?(filename)
< require filename
< break
< end
< end
< class_name.constantize rescue raise Scenario::NameError, "Expected to find #{class_name} in #{Scenario.load_paths.inspect}"
< end
diff -r scenarios/lib/scenarios/extensions/symbol.rb /Users/aiwilliams/Workspaces/faithfulcode/scenarios/lib/scenarios/extensions/symbol.rb
14,23d13
< # Convert a symbol into the associated scenario helper module:
< #
< # :basic.to_scenario_helper #=> BasicScenarioHelper
< # :basic_scenario.to_scenario_helper #=> BasicScenarioHelper
< #
< # Raises Scenario::NameError if the the helper cannot be loacated as
< # 'helpers/<name>_helper' in Scenario.load_paths.
< def to_scenario_helper
< to_s.to_scenario_helper
< end
Only in scenarios/spec: .svn
Only in scenarios/spec: environments.rb
Only in scenarios/spec/scenarios: .svn
Only in scenarios/spec/scenarios: helpers
diff -r scenarios/spec/scenarios_spec.rb /Users/aiwilliams/Workspaces/faithfulcode/scenarios/spec/scenarios_spec.rb
23,27d22
< it 'should allow us to have helpers in scenarios/helpers directory which we can get through the helpers class method' do
< klass = :empty.to_scenario
< klass.helpers :myown
< end
<
diff -r scenarios/spec/spec_helper.rb /Users/aiwilliams/Workspaces/faithfulcode/scenarios/spec/spec_helper.rb
1,20c1,6
< $LOAD_PATH << File.dirname(__FILE__) + '/../testing'
<
< require File.dirname(__FILE__) + "/environments"
<
< require 'active_support'
< require 'active_record'
< require 'action_controller'
< require 'action_view'
<
< module Spec
< module Rails
< module Example
< end
< end
< end
<
< require 'spec/rails'
< require 'scenarios'
<
< require 'models'
\ No newline at end of file
---
> require File.expand_path(File.dirname(__FILE__) + "/../testing/environment")
> TESTING_ENVIRONMENTS[TESTING_ENVIRONMENT].load(DATABASE_ADAPTER)
> require "models"
> require "spec"
> require "spec/rails"
> require "scenarios"
Only in scenarios/tasks: .svn
Only in scenarios/test: .svn
Only in scenarios/testing: .svn
diff -r scenarios/testing/environment.rb /Users/aiwilliams/Workspaces/faithfulcode/scenarios/testing/environment.rb
15c15
< TESTING_ENVIRONMENT = "rspec_3317_rails_8956" unless defined?(TESTING_ENVIRONMENT)
---
> TESTING_ENVIRONMENT = "rspec_3119_rails_8375" unless defined?(TESTING_ENVIRONMENT)
31c31
< # system "cd #{lib.support_directory} && patch -p0 < #{File.join(TESTING_ROOT, "rspec_on_rails_3119.patch")}"
---
> system "cd #{lib.support_directory} && patch -p0 < #{File.join(TESTING_ROOT, "rspec_on_rails_3119.patch")}"
36,38c36,38
< TESTING_ENVIRONMENTS << TestingLibrary::Environment.new("rspec_3317_rails_8956", SUPPORT_TEMP, DB_CONFIG_FILE, DB_SCHEMA_FILE) do |env|
< env.package "rails", "http://svn.rubyonrails.org/rails", "trunk", "8956", &rails_package
< env.package "rspec", "http://rspec.rubyforge.org/svn", "trunk", "3317", &rspec_package
---
> TESTING_ENVIRONMENTS << TestingLibrary::Environment.new("rspec_3119_rails_8375", SUPPORT_TEMP, DB_CONFIG_FILE, DB_SCHEMA_FILE) do |env|
> env.package "rails", "http://svn.rubyonrails.org/rails", "trunk", "8375", &rails_package
> env.package "rspec", "http://rspec.rubyforge.org/svn", "trunk", "3119", &rspec_package
40c40
< TESTING_ENVIRONMENTS << TestingLibrary::Environment.new("rspec_3317_rails_1_2_6", SUPPORT_TEMP, DB_CONFIG_FILE, DB_SCHEMA_FILE) do |env|
---
> TESTING_ENVIRONMENTS << TestingLibrary::Environment.new("rspec_3119_rails_1_2_6", SUPPORT_TEMP, DB_CONFIG_FILE, DB_SCHEMA_FILE) do |env|
42c42
< env.package "rspec", "http://rspec.rubyforge.org/svn", "trunk", "3317", &rspec_package
---
> env.package "rspec", "http://rspec.rubyforge.org/svn", "trunk", "3119", &rspec_package

View file

@ -0,0 +1,34 @@
module Scenarios
# Thrown by Scenario.load when it cannot find a specific senario.
class NameError < ::NameError; end
class << self
# The locations from which scenarios will be loaded.
mattr_accessor :load_paths
self.load_paths = ["#{RAILS_ROOT}/spec/scenarios", "#{RAILS_ROOT}/test/scenarios", "#{File.dirname(__FILE__)}/scenarios/builtin"]
# Load a scenario by name. <tt>scenario_name</tt> can be a string, symbol,
# or the scenario class.
def load(scenario_name)
klass = scenario_name.to_scenario
klass.load
klass
end
end
end
# The Scenario namespace makes for Scenario::Base
Scenario = Scenarios
# For Rails 1.2 compatibility
unless Class.instance_methods.include?(:superclass_delegating_reader)
require File.dirname(__FILE__) + "/scenarios/extensions/delegating_attributes"
end
require 'active_record/fixtures'
require 'scenarios/configuration'
require 'scenarios/table_blasting'
require 'scenarios/table_methods'
require 'scenarios/loading'
require 'scenarios/base'
require 'scenarios/extensions'

View file

@ -0,0 +1,73 @@
module Scenarios
class Base
class << self
# Class method to load the scenario. Used internally by the Scenarios
# plugin.
def load
new.load_scenarios(used_scenarios + [self])
end
# Class method for your own scenario to define helper methods that will
# be included into the scenario and all specs that include the scenario
def helpers(&block)
mod = (const_get(:Helpers) rescue const_set(:Helpers, Module.new))
mod.module_eval(&block) if block_given?
mod
end
# Class method for your own scenario to define the scenarios that it
# depends on. If your scenario depends on other scenarios those
# scenarios will be loaded before the load method on your scenario is
# executed.
def uses(*scenarios)
names = scenarios.map(&:to_scenario).reject { |n| used_scenarios.include?(n) }
used_scenarios.concat(names)
end
# Class method that returns the scenarios used by your scenario.
def used_scenarios # :nodoc:
@used_scenarios ||= []
@used_scenarios = (@used_scenarios.collect(&:used_scenarios) + @used_scenarios).flatten.uniq
end
# Returns the scenario class.
def to_scenario
self
end
end
include TableMethods
include Loading
attr_reader :table_config
# Initialize a scenario with a Configuration. Used internally by the
# Scenarios plugin.
def initialize(config = Configuration.new)
@table_config = config
table_config.update_scenario_helpers self.class
self.extend table_config.table_readers
self.extend table_config.scenario_helpers
end
# This method should be implemented in your scenarios. You may also have
# scenarios that simply use other scenarios, so it is not required that
# this be overridden.
def load
end
# Unload a scenario, sort of. This really only deletes the records, all of
# them, of every table this scenario modified. The goal is to maintain a
# clean database for successive runs. Used internally by the Scenarios
# plugin.
def unload
return if unloaded?
record_metas.each_value { |meta| blast_table(meta.table_name) }
@unloaded = true
end
def unloaded?
@unloaded == true
end
end
end

View file

@ -0,0 +1,18 @@
class BlankScenario < Scenarios::Base
def load
table_names.each do |table|
blast_table(table)
end
end
def table_names
self.class.table_names
end
def self.table_names
@table_names ||= begin
schema = (open(RAILS_ROOT + '/db/schema.rb') { |f| f.read } rescue '')
schema.grep(/create_table\s+(['"])(.+?)\1/m) { $2 }
end
end
end

View file

@ -0,0 +1,55 @@
module Scenarios
class Configuration # :nodoc:
attr_reader :blasted_tables, :loaded_scenarios, :record_metas, :table_readers, :scenario_helpers, :symbolic_names_to_id
def initialize
@blasted_tables = Set.new
@record_metas = Hash.new
@table_readers = Module.new
@scenario_helpers = Module.new
@symbolic_names_to_id = Hash.new {|h,k| h[k] = Hash.new}
@loaded_scenarios = Array.new
end
# Given a created record (currently ScenarioModel or ScenarioRecord),
# update the table readers module appropriately such that this record and
# it's id are findable via methods like 'people(symbolic_name)' and
# 'person_id(symbolic_name)'.
def update_table_readers(record)
ids, record_meta = symbolic_names_to_id, record.record_meta # scoping assignments
ids[record_meta.table_name][record.symbolic_name] = record.id
table_readers.send :define_method, record_meta.id_reader do |*symbolic_names|
record_ids = symbolic_names.flatten.collect do |symbolic_name|
if symbolic_name.kind_of?(ActiveRecord::Base)
symbolic_name.id
else
record_id = ids[record_meta.table_name][symbolic_name.to_sym]
raise ActiveRecord::RecordNotFound, "No object is associated with #{record_meta.table_name}(:#{symbolic_name})" unless record_id
record_id
end
end
record_ids.size > 1 ? record_ids : record_ids.first
end
table_readers.send :define_method, record_meta.record_reader do |*symbolic_names|
results = symbolic_names.flatten.collect do |symbolic_name|
symbolic_name.kind_of?(ActiveRecord::Base) ?
symbolic_name :
record_meta.record_class.find(send(record_meta.id_reader, symbolic_name))
end
results.size > 1 ? results : results.first
end
end
def update_scenario_helpers(scenario_class)
scenario_helpers.module_eval do
include scenario_class.helpers
end
end
def scenarios_loaded?
!loaded_scenarios.blank?
end
end
end

View file

@ -0,0 +1,5 @@
require File.dirname(__FILE__) + "/extensions/object"
require File.dirname(__FILE__) + "/extensions/string"
require File.dirname(__FILE__) + "/extensions/symbol"
require File.dirname(__FILE__) + "/extensions/active_record"
require File.dirname(__FILE__) + "/extensions/test_case" rescue nil

View file

@ -0,0 +1,14 @@
module ActiveRecord
class Base
cattr_accessor :table_config
include Scenarios::TableBlasting
# In order to guarantee that tables are tracked when _create_model_ is
# used, and those models cause other models to be created...
def create_with_table_blasting
prepare_table(self.class.table_name)
create_without_table_blasting
end
alias_method_chain :create, :table_blasting
end
end

View file

@ -0,0 +1,40 @@
# These class attributes behave something like the class
# inheritable accessors. But instead of copying the hash over at
# the time the subclass is first defined, the accessors simply
# delegate to their superclass unless they have been given a
# specific value. This stops the strange situation where values
# set after class definition don't get applied to subclasses.
class Class
def superclass_delegating_reader(*names)
class_name_to_stop_searching_on = self.superclass.name.blank? ? "Object" : self.superclass.name
names.each do |name|
class_eval <<-EOS
def self.#{name}
if defined?(@#{name})
@#{name}
elsif superclass < #{class_name_to_stop_searching_on} && superclass.respond_to?(:#{name})
superclass.#{name}
end
end
def #{name}
self.class.#{name}
end
EOS
end
end
def superclass_delegating_writer(*names)
names.each do |name|
class_eval <<-EOS
def self.#{name}=(value)
@#{name} = value
end
EOS
end
end
def superclass_delegating_accessor(*names)
superclass_delegating_reader(*names)
superclass_delegating_writer(*names)
end
end

View file

@ -0,0 +1,5 @@
class Object
def metaclass
(class << self; self; end)
end unless method_defined?(:metaclass)
end

View file

@ -0,0 +1,22 @@
class String
# Convert a string into the associated scenario class:
#
# "basic".to_scenario #=> BasicScenario
# "basic_scenario".to_scenario #=> BasicScenario
#
# Raises Scenario::NameError if the the scenario cannot be loacated in
# Scenario.load_paths.
def to_scenario
class_name = "#{self.strip.camelize.sub(/Scenario$/, '')}Scenario"
Scenario.load_paths.each do |path|
filename = "#{path}/#{class_name.underscore}.rb"
if File.file?(filename)
require filename
break
end
end
class_name.constantize rescue raise Scenario::NameError, "Expected to find #{class_name} in #{Scenario.load_paths.inspect}"
end
end

View file

@ -0,0 +1,14 @@
class Symbol
# Convert a symbol into the associated scenario class:
#
# :basic.to_scenario #=> BasicScenario
# :basic_scenario.to_scenario #=> BasicScenario
#
# Raises Scenario::NameError if the the scenario cannot be located in
# Scenario.load_paths.
def to_scenario
to_s.to_scenario
end
end

View file

@ -0,0 +1,77 @@
module Test #:nodoc:
module Unit #:nodoc:
class TestCase #:nodoc:
superclass_delegating_accessor :scenario_classes
superclass_delegating_accessor :table_config
# Changing either of these is not supported at this time.
self.use_transactional_fixtures = true
self.use_instantiated_fixtures = false
include Scenarios::TableMethods
include Scenarios::Loading
class << self
# This class method is mixed into RSpec and allows you to declare that
# you are using a given scenario or set of scenarios within a spec:
#
# scenario :basic # loads BasicScenario and any dependencies
# scenario :posts, :comments # loads PostsScenario and CommentsScenario
#
# It accepts an array of scenarios (strings, symbols, or classes) and
# will load them roughly in the order that they are specified.
def scenario(*names)
self.scenario_classes = []
names.each do |name|
scenario_class = name.to_scenario
scenario_classes.concat(scenario_class.used_scenarios + [scenario_class])
end
scenario_classes.uniq!
end
# Overridden to provide before all and after all code which sets up and
# tears down scenarios
def suite_with_scenarios
suite = suite_without_scenarios
class << suite
attr_accessor :test_class
def run_with_scenarios(*args, &block)
debugger
run_without_scenarios(*args, &block)
test_class.table_config.loaded_scenarios.each { |s| s.unload } if test_class.table_config
end
alias_method_chain :run, :scenarios
end
suite.test_class = self
suite
end
alias_method_chain :suite, :scenarios
end
# Hook into fixtures loading lifecycle to instead load scenarios. This
# is expected to be called in a fashion respective of
# use_transactional_fixtures. I feel like a leech.
def load_fixtures
if !scenarios_loaded? || !use_transactional_fixtures?
self.class.table_config = Scenarios::Configuration.new if !use_transactional_fixtures? || table_config.nil?
load_scenarios(scenario_classes)
end
self.extend scenario_helpers
self.extend table_readers
end
# Here we are counting on existing logic to allow teardown method
# overriding as done in fixtures.rb. Only if transaction fixtures are
# not being used do we unload scenarios after a test. Otherwise, we wait
# until the end of the run of all tests on this test_case (collection of
# tests, right!). See the TestSuite extension done in _suite_ for
# behaviour when using transaction fixtures.
def teardown_with_scenarios
teardown_without_scenarios
loaded_scenarios.each { |s| s.unload } unless use_transactional_fixtures?
end
alias_method_chain :teardown, :scenarios
end
end
end

View file

@ -0,0 +1,51 @@
module Scenarios
# Provides scenario loading and convenience methods around the Configuration
# that must be made available through a method _table_config_.
module Loading # :nodoc:
def load_scenarios(scenario_classes)
install_active_record_tracking_hook
scenario_classes.each do |scenario_class|
scenario = scenario_class.new(table_config)
scenario.load
table_config.loaded_scenarios << scenario
end if scenario_classes
end
def loaded_scenarios
table_config.loaded_scenarios
end
def scenarios_loaded?
table_config && table_config.scenarios_loaded?
end
# The sum of all the loaded scenario's helper methods. These can be mixed
# into anything you like to gain access to them.
def scenario_helpers
table_config.scenario_helpers
end
# The sum of all the available table reading methods. These will only
# include readers for which data has been placed into the table. These can
# be mixed into anything you like to gain access to them.
def table_readers
table_config.table_readers
end
# # This understand nesting descriptions one deep
# def table_config
# on_my_class = self.class.instance_variable_get("@table_config")
# return on_my_class if on_my_class
#
# if self.class.superclass
# on_super_class = self.class.superclass.instance_variable_get("@table_config")
# return on_super_class if on_super_class
# end
# end
private
def install_active_record_tracking_hook
ActiveRecord::Base.table_config = table_config
end
end
end

View file

@ -0,0 +1,20 @@
module Scenarios
module TableBlasting
def self.included(base)
base.module_eval do
delegate :blasted_tables, :to => :table_config
end
end
def blast_table(name) # :nodoc:
ActiveRecord::Base.silence do
ActiveRecord::Base.connection.delete "DELETE FROM #{name}", "Scenario Delete"
end
blasted_tables << name
end
def prepare_table(name)
blast_table(name) unless blasted_tables.include?(name)
end
end
end

View file

@ -0,0 +1,205 @@
module Scenarios
# This helper module contains the #create_record method. It is made
# available to all Scenario instances, test and example classes, and test
# and example instances.
module TableMethods
include TableBlasting
delegate :record_metas, :to => :table_config
# Insert a record into the database, add the appropriate helper methods
# into the scenario and spec, and return the ID of the inserted record:
#
# create_record :event, :name => "Ruby Hoedown"
# create_record Event, :hoedown, :name => "Ruby Hoedown"
#
# The first form will create a new record in the given class identifier
# and no symbolic name (essentially).
#
# The second form is exactly like the first, except for that it provides a
# symbolic name as the second parameter. The symbolic name will allow you
# to access the record through a couple of helper methods:
#
# events(:hoedown) # The hoedown event
# event_id(:hoedown) # The ID of the hoedown event
#
# These helper methods are only accessible for a particular table after
# you have inserted a record into that table using <tt>create_record</tt>.
def create_record(class_identifier, *args)
insert(ScenarioRecord, class_identifier, *args) do |record|
meta = record.record_meta
fixture = record.to_fixture
begin
meta.connection.insert_fixture(fixture, record.record_meta.table_name)
rescue # Rails 1.2 compatible!
meta.connection.execute "INSERT INTO #{meta.table_name} (#{fixture.key_list}) VALUES (#{fixture.value_list})"
end
record.id
end
end
# Instantiate and save! a model, add the appropriate helper methods into
# the scenario and spec, and return the new model instance:
#
# create_model :event, :name => "Ruby Hoedown"
# create_model Event, :hoedown, :name => "Ruby Hoedown"
#
# The first form will create a new model with no symbolic name
# (essentially).
#
# The second form is exactly like the first, except for that it provides a
# symbolic name as the second parameter. The symbolic name will allow you
# to access the record through a couple of helper methods:
#
# events(:hoedown) # The hoedown event
# event_id(:hoedown) # The ID of the hoedown event
#
# These helper methods are only accessible for a particular table after
# you have inserted a record into that table using <tt>create_model</tt>.
def create_model(class_identifier, *args)
insert(ScenarioModel, class_identifier, *args) do |record|
model = record.to_model
model.save!
model
end
end
private
def insert(record_or_model, class_identifier, *args, &insertion)
symbolic_name, attributes = extract_creation_arguments(args)
record_meta = (record_metas[class_identifier] ||= RecordMeta.new(class_identifier))
record = record_or_model.new(record_meta, attributes, symbolic_name)
return_value = nil
ActiveRecord::Base.silence do
prepare_table(record_meta.table_name)
return_value = insertion.call record
table_config.update_table_readers(record)
self.extend table_config.table_readers
end
return_value
end
def extract_creation_arguments(arguments)
if arguments.size == 2 && arguments.last.kind_of?(Hash)
arguments
elsif arguments.size == 1 && arguments.last.kind_of?(Hash)
[nil, arguments[0]]
else
[nil, Hash.new]
end
end
class RecordMeta # :nodoc:
attr_reader :class_name, :record_class, :table_name
def initialize(class_identifier)
@class_identifier = class_identifier
@class_name = resolve_class_name(class_identifier)
@record_class = class_name.constantize
@table_name = record_class.table_name
end
def timestamp_columns
@timestamp_columns ||= begin
timestamps = %w(created_at created_on updated_at updated_on)
columns.select do |column|
timestamps.include?(column.name)
end
end
end
def columns
@columns ||= connection.columns(table_name)
end
def connection
record_class.connection
end
def id_reader
@id_reader ||= begin
reader = ActiveRecord::Base.pluralize_table_names ? table_name.singularize : table_name
"#{reader}_id".to_sym
end
end
def record_reader
table_name.to_sym
end
def resolve_class_name(class_identifier)
case class_identifier
when Symbol
class_identifier.to_s.singularize.camelize
when Class
class_identifier.name
when String
class_identifier
end
end
def to_s
"#<RecordMeta: #{table_name}>"
end
end
class ScenarioModel # :nodoc:
attr_reader :attributes, :model, :record_meta, :symbolic_name
delegate :id, :to => :model
def initialize(record_meta, attributes, symbolic_name = nil)
@record_meta = record_meta
@attributes = attributes.stringify_keys
@symbolic_name = symbolic_name || object_id
end
def to_hash
to_model.attributes
end
def to_model
@model ||= record_meta.record_class.new(attributes)
end
end
class ScenarioRecord # :nodoc:
attr_reader :record_meta, :symbolic_name
def initialize(record_meta, attributes, symbolic_name = nil)
@record_meta = record_meta
@attributes = attributes.stringify_keys
@symbolic_name = symbolic_name || object_id
install_default_attributes!
end
def id
@attributes['id']
end
def to_hash
@attributes
end
def to_fixture
Fixture.new(to_hash, record_meta.class_name)
end
def install_default_attributes!
@attributes['id'] ||= symbolic_name.to_s.hash.abs
install_timestamps!
end
def install_timestamps!
record_meta.timestamp_columns.each do |column|
@attributes[column.name] = now(column) unless @attributes.key?(column.name)
end
end
def now(column)
now = ActiveRecord::Base.default_timezone == :utc ? column.klass.now.utc : column.klass.now
now.to_s(:db)
end
end
end
end

View file

@ -0,0 +1,9 @@
class ComplexCompositeScenario < Scenario::Base
uses :composite, :places
helpers do
def method_from_complex_composite_scenario
:method_from_complex_composite_scenario
end
end
end

View file

@ -0,0 +1,9 @@
class CompositeScenario < Scenario::Base
uses :people, :things
helpers do
def method_from_composite_scenario
:method_from_composite_scenario
end
end
end

View file

@ -0,0 +1,4 @@
class EmptyScenario < Scenario::Base
def load
end
end

View file

@ -0,0 +1,26 @@
class PeopleScenario < Scenario::Base
def load
create_person "John Long"
create_person "Adam Williams"
end
helpers do
def create_person(attributes = {})
if attributes.kind_of?(String)
first, last = attributes.split(/\s+/)
attributes = { :first_name => first, :last_name => last }
end
attributes = person_params(attributes)
create_record(:person, attributes[:first_name].strip.gsub(' ', '_').underscore.to_sym, attributes)
end
def person_params(attributes = {})
attributes = {
:first_name => "John",
:last_name => "Q."
}.update(attributes)
end
end
end

View file

@ -0,0 +1,22 @@
class PlacesScenario < Scenario::Base
def load
create_place "Taj Mahal", "India"
create_place "Whitehouse", "Washington DC"
end
helpers do
def create_place(name, location)
attributes = place_params(:name => name, :location => location)
create_record(:place, name.strip.gsub(' ', '_').underscore.to_sym, attributes)
end
def place_params(attributes = {})
attributes = {
:name => "Noplace",
:location => "Nowhere"
}.update(attributes)
end
end
end

View file

@ -0,0 +1,22 @@
class ThingsScenario < Scenario::Base
def load
create_thing "one"
create_thing "two"
end
helpers do
def create_thing(attributes = {})
attributes = { :name => attributes } if attributes.kind_of?(String)
attributes = thing_params(attributes)
create_record(:thing, attributes[:name].strip.gsub(' ', '_').underscore.to_sym, attributes)
end
def thing_params(attributes = {})
attributes = {
:name => "Unnamed Thing",
:description => "I'm not sure what this is."
}.update(attributes)
end
end
end

View file

@ -0,0 +1,185 @@
require File.expand_path(File.dirname(__FILE__) + "/spec_helper")
class ExplodesOnSecondInstantiationScenario < Scenario::Base
cattr_accessor :instance
def initialize(*args)
raise "Should only be created once" if self.class.instance
self.class.instance = super(*args)
end
end
describe "Scenario loading" do
scenario :explodes_on_second_instantiation
it "should work" do
end
it 'should work again' do
end
end
describe "Scenario loading" do
it "should load from configured directories" do
Scenario.load(:empty)
EmptyScenario
end
it "should raise Scenario::NameError when the scenario does not exist" do
lambda { Scenario.load(:whatcha_talkin_bout) }.should raise_error(Scenario::NameError)
end
it "should allow us to add helper methods through the helpers class method" do
klass = :empty.to_scenario
klass.helpers do
def hello
"Hello World"
end
end
klass.new.methods.should include('hello')
end
it "should provide a built-in scenario named :blank which clears all tables found in schema.rb" do
Scenario.load(:blank)
BlankScenario
end
end
describe Scenarios::TableMethods do
scenario :things
it "should understand namespaced models" do
create_record "ModelModule::Model", :raking, :name => "Raking", :description => "Moving leaves around"
models(:raking).should_not be_nil
end
it "should include record creation methods" do
create_record(:thing, :three, :name => "Three")
things(:three).name.should == "Three"
end
it "should include other example helper methods" do
create_thing("The Thing")
things(:the_thing).name.should == "The Thing"
end
describe "for retrieving objects" do
it "should have a pluralized name" do
should respond_to("things")
should_not respond_to("thing")
end
it "should answer a single object given a single name" do
things(:one).should be_kind_of(Thing)
things("one").should be_kind_of(Thing)
things(:two).name.should == "two"
end
it "should answer an array of objects given multiple names" do
things(:one, :two).should be_kind_of(Array)
things(:one, :two).should eql([things(:one), things(:two)])
end
it "should just return the argument if an AR instance is given" do
thing = things(:one)
things(thing).should eql(thing)
end
end
describe "for retrieving ids" do
it "should have a singular name" do
should respond_to("thing_id")
should_not respond_to("thing_ids")
should_not respond_to("things_id")
end
it "should answer a single id given a single name" do
thing_id(:one).should be_kind_of(Fixnum)
thing_id("one").should be_kind_of(Fixnum)
end
it "should answer an array of ids given multiple names" do
thing_id(:one, :two).should be_kind_of(Array)
thing_id(:one, :two).should eql([thing_id(:one), thing_id(:two)])
thing_id("one", "two").should eql([thing_id(:one), thing_id(:two)])
end
it "should answer the id of the argument if an AR instance id given" do
thing = things(:one)
thing_id(thing).should == thing.id
end
end
end
describe "it uses people and things scenarios", :shared => true do
it "should have reader helper methods for each used scenario" do
should respond_to(:things)
should respond_to(:people)
end
it "should allow us to use helper methods from each scenario inside an example" do
should respond_to(:create_thing)
should respond_to(:create_person)
end
end
describe "A composite scenario" do
scenario :composite
it_should_behave_like "it uses people and things scenarios"
it "should allow us to use helper methods scenario" do
should respond_to(:method_from_composite_scenario)
end
end
describe "Multiple scenarios" do
scenario :things, :people
it_should_behave_like "it uses people and things scenarios"
end
describe "A complex composite scenario" do
scenario :complex_composite
it_should_behave_like "it uses people and things scenarios"
it "should have correct reader helper methods" do
should respond_to(:places)
end
it "should allow us to use correct helper methods" do
should respond_to(:create_place)
end
end
describe "Overlapping scenarios" do
scenario :composite, :things, :people
it "should not cause scenarios to be loaded twice" do
Person.find_all_by_first_name("John").size.should == 1
end
end
describe "create_record table method" do
scenario :empty
it "should automatically set timestamps" do
create_record :note, :first, :content => "first note"
note = notes(:first)
note.created_at.should be_instance_of(Time)
end
end
describe "create_model table method" do
scenario :empty
it "should support symbolic names" do
thing = create_model Thing, :mything, :name => "My Thing", :description => "For testing"
things(:mything).should == thing
end
it "should blast any table touched as a side effect of creating a model (callbacks, observers, etc.)" do
create_model SideEffectyThing
blasted_tables.should include(Thing.table_name)
end
end

View file

@ -0,0 +1,7 @@
--colour
--format
progress
--loadby
mtime
--reverse
--backtrace

View file

@ -0,0 +1,24 @@
require File.expand_path(File.dirname(__FILE__) + '/../testing/plugit_descriptor')
TESTING_ROOT = File.expand_path("#{File.dirname(__FILE__)}/../testing")
TESTING_TMP = "#{TESTING_ROOT}/tmp"
require 'fileutils'
FileUtils.mkdir_p(TESTING_TMP)
FileUtils.touch("#{TESTING_TMP}/test.log")
require 'logger'
RAILS_DEFAULT_LOGGER = Logger.new("#{TESTING_TMP}/test.log")
RAILS_DEFAULT_LOGGER.level = Logger::DEBUG
ActiveRecord::Base.silence do
ActiveRecord::Base.configurations = {'sqlite3' => {
'adapter' => 'sqlite3',
'database' => "#{TESTING_TMP}/sqlite3.db"
}}
ActiveRecord::Base.establish_connection 'sqlite3'
load "#{TESTING_ROOT}/schema.rb"
end
require "models"
require "scenarios"

View file

@ -0,0 +1,19 @@
namespace :db do
namespace :scenario do
desc "Load a scenario into the current environment's database using SCENARIO=scenario_name"
task :load => 'db:reset' do
scenario_name = ENV['SCENARIO'] || 'default'
begin
klass = Scenarios.load(scenario_name)
puts "Loaded #{klass.name.underscore.gsub('_', ' ')}."
rescue Scenarios::NameError => e
if scenario_name == 'default'
puts "Error! Set the SCENARIO environment variable or define a DefaultScenario class."
else
puts "Error! Invalid scenario name [#{scenario_name}]."
end
exit(1)
end
end
end
end

View file

@ -0,0 +1,2 @@
class ApplicationController < ActionController::Base
end

View file

@ -0,0 +1,14 @@
class Person < ActiveRecord::Base; end
class Place < ActiveRecord::Base; end
class Thing < ActiveRecord::Base; end
class Note < ActiveRecord::Base; end
class SideEffectyThing < ActiveRecord::Base
after_create do
Thing.create!
end
end
module ModelModule
class Model < ActiveRecord::Base; end
end

View file

@ -0,0 +1,44 @@
require 'rubygems'
gem 'plugit'
require 'plugit'
$LOAD_PATH << File.expand_path("#{File.dirname(__FILE__)}/../lib")
$LOAD_PATH << File.expand_path(File.dirname(__FILE__))
RAILS_ROOT = File.expand_path("#{File.dirname(__FILE__)}/..")
Plugit.describe do |scenarios|
scenarios.environments_root_path = File.dirname(__FILE__) + '/environments'
vendor_directory = File.expand_path(File.dirname(__FILE__) + '/../vendor/plugins')
scenarios.environment :default, 'Released versions of Rails and RSpec' do |env|
env.library :rails, :export => "git clone git://github.com/rails/rails.git" do |rails|
rails.after_update { `git co v2.1.0_RC1` }
rails.load_paths = %w{/activesupport/lib /activerecord/lib /actionpack/lib}
rails.requires = %w{active_support active_record action_controller action_view}
end
env.library :rspec, :export => "git clone git://github.com/dchelimsky/rspec.git" do |rspec|
rspec.after_update { `git co 1.1.4 && mkdir -p #{vendor_directory} && ln -sF #{File.expand_path('.')} #{vendor_directory + '/rspec'}` }
rspec.requires = %w{spec}
end
env.library :rspec_rails, :export => "git clone git://github.com/dchelimsky/rspec-rails.git" do |rspec_rails|
rspec_rails.after_update { `git co 1.1.4` }
rspec_rails.requires = %w{spec/rails}
end
end
scenarios.environment :edge, 'Edge versions of Rails and RSpec' do |env|
env.library :rails, :export => "git clone git://github.com/rails/rails.git --depth 1" do |rails|
rails.before_install { `git pull` }
rails.load_paths = %w{/activesupport/lib /activerecord/lib /actionpack/lib}
rails.requires = %w{active_support active_record action_controller action_view}
end
env.library :rspec, :export => "git clone git://github.com/dchelimsky/rspec.git --depth 1" do |rspec|
rspec.after_update { `git pull && mkdir -p #{vendor_directory} && ln -sF #{File.expand_path('.')} #{vendor_directory + '/rspec'}` }
rspec.requires = %w{spec}
end
env.library :rspec_rails, :export => "git clone git://github.com/dchelimsky/rspec-rails.git --depth 1" do |rspec_rails|
rspec_rails.after_update { `git pull` }
rspec_rails.requires = %w{spec/rails}
end
end
end

View file

@ -0,0 +1,31 @@
ActiveRecord::Schema.define do
create_table :people, :force => true do |t|
t.column :first_name, :string
t.column :last_name, :string
end
create_table :places, :force => true do |t|
t.column :name, :string
t.column :location, :string
end
create_table :things, :force => true do |t|
t.column :name, :string
t.column :description, :string
end
create_table :side_effecty_things, :force => true do |t|
end
create_table :models, :force => true do |t|
t.column :name, :string
t.column :description, :string
end
create_table :notes, :force => true do |t|
t.column :content, :string
t.column :created_at, :datetime
t.column :updated_at, :datetime
end
end

2
vendor/plugins/skinny_spec/.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
.DS_Store
doc

267
vendor/plugins/skinny_spec/README.rdoc vendored Normal file
View file

@ -0,0 +1,267 @@
= Skinny Spec
Skinny Spec is a collection of spec helper methods designed to help trim the fat and DRY up
some of the bloat that sometimes results from properly specing your classes and templates.
== Requirements and Recommendations
Obviously you'll need to be using RSpec[http://github.com/dchelimsky/rspec/tree/master] and
Rspec-Rails[http://github.com/dchelimsky/rspec-rails/tree/master] as your testing framework.
Skinny Spec was originally designed [and best enjoyed] if you're using
Haml[http://github.com/nex3/haml/tree/master] and
make_resourceful[http://github.com/rsl/make_resourceful/tree/master] but will default to
ERb and a facsimile of Rails' default scaffolding [for the views and controllers, respectively]
if Haml and/or make_resourceful are not available. I recommend using them though. :)
In addition, Skinny Spec uses Ruby2Ruby to make nicer expectation messages and you'll want to
have that installed as well. It's not a dependency or anything but it <i>is</i> highly
recommended.
== Setup
Once you've installed the plugin in your app's vendor/plugins folder, you're ready to rock!
Skinny Spec includes itself into the proper RSpec classes so there's no configuration on your
part. Sweet!
== Usage
The simplest way to use Skinny Specs is to generate a resource scaffold:
script/generate skinny_scaffold User
This command takes the usual complement of attribute definitions like
<tt>script/generate scaffold</tt>. Then have a look at the generated files (particularly the
specs) to see what's new and different with Skinny Spec.
=== Controller Specs
Let's look at the controller specs.
describe UsersController do
def valid_attributes(args = {})
{
# Add valid attributes for the your params[:user] here!
}.merge(args)
end
describe "GET :index" do
before(:each) do
@users = stub_index(User)
end
it_should_find_and_assign :users
it_should_render :template, "index"
end
# ...
describe "POST :create" do
describe "when successful" do
before(:each) do
@user = stub_create(User)
end
it_should_initialize_and_save :user
it_should_redirect_to { user_url(@user) }
end
# ...
First thing you should see is a method definition for
<tt>valid_attributes</tt>. This will be used later by the <tt>create</tt> and <tt>update</tt>
specs to more accurately represent how the controller works in actual practice by supplying
somewhat real data for the <tt>params</tt> coming from the HTML forms.
Next we find an example group for <tt>GET :index</tt>. That <tt>stub_index</tt> method there
does a lot of work behind the curtain. I'll leave it up to you to check the documentation for it
(and its brothers and sister methods like <tt>stub_new</tt>) but I will point out that the
methods named <tt>stub_<i>controller_method</i></tt> should only be used for stubbing and
mocking the main object of the method. To create mocks for other ancillary objects, please
use <tt>stub_find_all</tt>, <tt>stub_find_one</tt>, and <tt>stub_initialize</tt>. The reason
for this is because the former methods actually save us a step by defining an implicit
controller method request. If you add a new method to your resource routing, you'll want to
use the helper method <tt>define_request</tt> in those example groups to define an explicit
request. You can also define a method called <tt>shared_request</tt> to "share a
<tt>define_request</tt>" across shared describe blocks, like so:
describe "POST :create" do
def shared_request
post :create
end
describe "when successful" do
# ...
end
describe "when unsuccessful" do
# ...
end
end
Note: When you're adding longer, more complicated controller specs you can still leverage
implicit and shared requests by calling <tt>do_request</tt> in your spec as in the following
example:
# Let's assume this controller is _not_ CategoriesController
# and that loading the categories isn't part of the default actions
describe "GET :index" do
before(:each) do
@categories = stub_find_all(Category)
end
it "should preload categories" do
Category.should_receive(:find).with(:all)
do_request
end
it "should assign @categories" do
do_request
assigns[:categories].should == @categories
end
end
Finally we get to the meat of the spec and of Skinny Specs itself: the actual expectations.
The first thing you'll notice is the use of example group (read: "describe" block) level methods
instead of the usual example (read: "it") blocks. Using this helper at the example group level
saves us three lines over using an example block. (If this isn't significant to you, this is
probably the wrong plugin for you as well. Sorry.) Note that none of these methods use the
instance variables defined in the "before" block because they are all nil at the example block
level. Let's look at a sample method to see how it works:
it_should_find_and_assign :users
This actually wraps two different expectations: one that <tt>User.should_receive(:find).with(:all)</tt>
and another that the instance variable <tt>@users</tt> is assigned with the return value from that finder call.
If you need to add more detailed arguments to the find, you can easily break this into two different
expectations like:
it_should_find :users, :limit => 2
it_should_assign :users
See the documentation for the <tt>it_should_find</tt> for more information. You might have guessed that
<tt>it_should_initialize_assign</tt> and <tt>it_should_render_template</tt> work in a similar
fashion and you'd be right. Again, see the documentation for these individual methods for more
information. Lots of information in those docs.
A useful helper method that doesn't appear in any of the scaffolding is <tt>with_default_restful_actions</tt>
which takes a block and evaluates it for each of the RESTful controller actions. Very useful for
spec'ing that these methods redirect to the login page when the user isn't logged in, for example. This
method is designed to be used inside an example like so:
describe "when not logged in" do
it "should redirect all requests to the login page" do
with_default_restful_actions do
response.should redirect_to(login_url)
end
end
end
Before we're through with the controller specs, let me point out one more important detail. In
order to use <tt>it_should_redirect_to</tt> we have to send the routing inside a block argument
there so it can be evaluated in the example context instead of the example group, where it
completely blows up. This methodology is used anywhere routing is referred to in a "skinny",
example group level spec.
=== View Specs
Now let's move to the view specs!
describe "/users/form.html.haml" do
before(:each) do
@user = mock_and_assign(User, :stub => {
:name => "foo",
:birthday => 1.week.ago,
:adult => false
})
end
it_should_have_form_for :user
it_should_allow_editing :user, :name
it_should_allow_editing :user, :birthday
it_should_allow_editing :user, :adult
it_should_link_to_show :user
it_should_link_to { users_path }
end
Like the special <tt>stub_index</tt> methods in the controller
specs, the view specs have a shorthand mock and stub helpers: <tt>mock_and_assign</tt> and
<tt>mock_and_assign_collection</tt>. These are well documented so please check them out.
There are also some really nice helper methods that I'd like point out. First is
<tt>it_should_have_form_for</tt>. This is a really good convenience wrapper that basically wraps
the much longer:
it "should use form_for to generate the proper form action and options" do
template.should_receive(:form_for).with(@user)
do_render
end
Next up is the <tt>it_should_allow_editing</tt> helper. I love this method the most because it
really helps DRY up that view spec while at the same time being amazingly unbrittle. Instead of
creating an expectation for a specific form element, this method creates a generalized expectation
that there's a form element with the <tt>name</tt> attribute set in such away that it will
generate the proper <tt>params</tt> to use in the controller to edit or create the instance.
Check out the docs and the source for more information on this. Also check out
<tt>it_should_have_form_element_for</tt> which is roughly equivalent for those times when you use
<tt>form_tag</tt> instead.
Finally let's look at those <tt>it_should_link_to_<i>controller_method</i></tt> helpers.
These methods (and there's one each for the controller methods
<tt>new</tt>, <tt>edit</tt>, <tt>show</tt>, and <tt>delete</tt>) point to instance variables
which you should be created in the "before" blocks with <tt>mock_and_assign</tt>. The other is
<tt>it_should_allow_editing</tt> which is likewise covered extensively in the documentation and
I will just point out here that, like <tt>it_should_link_to_edit</tt> and such, it takes a
symbol for the name of the instance variable it refers to and <i>additionally</i> takes
a symbol for the name of the attribute to be edited.
Also note that, when constructing a long form example, instead of defining an instance variable
for the name of the template and calling <tt>render @that_template</tt> you can simply call
<tt>do_render</tt> which takes the name of the template from the outermost example group where
it is customarily stated.
=== Model Specs
Skinny Spec adds a matcher for the various ActiveRecord associations. On the example group level
you call them like:
it_should_belong_to :manager
it_should_have_many :clients
Within an example you can call them on either the class or the instance setup in the
"before" block. These are equivalent:
@user.should belong_to(:group)
User.should belong_to(:group)
I've also added some very basic validation helpers like <tt>it_should_validate_presence_of</tt>,
<tt>it_should_validate_uniqueness_of</tt>, <tt>it_should_not_mass_assign</tt>. Please consult
the documentation for more information.
== Miscellaneous Notes
In the scaffolding, I have used my own idiomatic Rails usage:
* All controller actions which use HTML forms [<tt>new</tt>, <tt>edit</tt>, etc] use a shared
form and leverage <tt>form_for</tt> to its fullest by letting it create the appropriate
action and options.
* Some instances where you might expect link_to are button_to. This is to provide a common
interface element which can be styled the same instead of a mishmash of links and buttons and
inputs everywhere. To take full advantage of this, I usually override many of Rails' default
helpers with custom ones that all use actual HTML <tt>BUTTON</tt> elements which are much
easier to style than "button" typed <tt>INPUT</tt>. I've provided a text file in the
"additional" folder of this plugin which you can use in your ApplicationHelper. (I also
provide an optional override helper for the <tt>label</tt> method which uses
<tt>#titleize</tt> instead of <tt>humanize</tt> for stylistic reasons).
* Probably more that I can't think of.
== Credits and Thanks
Sections of this code were taken from or inspired by Rick Olsen's
rspec_on_rails_on_crack[http://github.com/technoweenie/rspec_on_rails_on_crack/tree/master].
Also thanks and props to Hampton Catlin and Nathan Weizenbaum for the lovely and imminently useable
Haml and make_resourceful. Also also praises and glory to David Chelimsky and the Rspec crew.
Also thanks to Don Petersen for his suggestions and fixes.

11
vendor/plugins/skinny_spec/Rakefile vendored Normal file
View file

@ -0,0 +1,11 @@
require 'rake'
require 'rake/rdoctask'
desc 'Generate documentation for the Skinny Spec plugin'
Rake::RDocTask.new(:rdoc) do |rdoc|
rdoc.rdoc_dir = 'doc'
rdoc.title = 'Skinny Spec'
rdoc.options << '--line-numbers' << '--inline-source'
rdoc.rdoc_files.include('README.rdoc')
rdoc.rdoc_files.include('lib/**/*.rb')
end

View file

@ -0,0 +1,58 @@
# Please insert these into your ApplicationHelper
# Replacement for Rails' default submit_tag helper
# using HTML button element rather than HTML input element
def submit_tag(text, options = {})
content_tag :button, text, options.merge(:type => :submit)
end
# Replacement for Rails' default button_to helper
# using HTML button element rather than HTML input element
def button_to(name, options = {}, html_options = {})
html_options = html_options.stringify_keys
convert_boolean_attributes!(html_options, %w( disabled ))
method_tag = ''
if (method = html_options.delete('method')) && %w{put delete}.include?(method.to_s)
method_tag = tag('input', :type => 'hidden', :name => '_method', :value => method.to_s)
end
form_method = method.to_s == 'get' ? 'get' : 'post'
request_token_tag = ''
if form_method == 'post' && protect_against_forgery?
request_token_tag = tag(:input, :type => "hidden", :name => request_forgery_protection_token.to_s, :value => form_authenticity_token)
end
if confirm = html_options.delete("confirm")
html_options["onclick"] = "return #{confirm_javascript_function(confirm)};"
end
url = options.is_a?(String) ? options : self.url_for(options)
name ||= url
html_options.merge!("type" => "submit", "value" => name)
"<form method=\"#{form_method}\" action=\"#{escape_once url}\" class=\"button_to\"><div>" +
method_tag + content_tag("button", name, html_options) + request_token_tag + "</div></form>"
end
# Replacement for Rails' default button_to_function helper
# using HTML button element rather than HTML input element
def button_to_function(name, *args, &block)
html_options = args.extract_options!
function = args[0] || ''
html_options.symbolize_keys!
function = update_page(&block) if block_given?
content_tag(:button, name, html_options.merge({
:onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};"
}))
end
# Replacement for Rails' default label helper
# using String#titleize rather than String#humanize
def label(object_name, method, text = nil, options = {})
text ||= method.to_s[].titleize
super
end

View file

@ -0,0 +1,93 @@
class SkinnyScaffoldGenerator < Rails::Generator::NamedBase
attr_reader :controller_class_path, :controller_file_path, :controller_class_nesting,
:controller_class_nesting_depth, :controller_class_name, :controller_underscore_name,
:controller_plural_name, :template_language
alias_method :controller_file_name, :controller_underscore_name
alias_method :controller_singular_name, :controller_file_name
alias_method :controller_table_name, :controller_plural_name
def initialize(runtime_args, runtime_options = {})
super
base_name, @controller_class_path, @controller_file_path, @controller_class_nesting, @controller_class_nesting_depth = extract_modules(@name.pluralize)
@controller_class_name_without_nesting, @controller_underscore_name, @controller_plural_name = inflect_names(base_name)
if @controller_class_nesting.empty?
@controller_class_name = @controller_class_name_without_nesting
else
@controller_class_name = "#{@controller_class_nesting}::#{@controller_class_name_without_nesting}"
end
end
def manifest
record do |m|
# Check for class naming collisions
m.class_collisions controller_class_path, "#{controller_class_name}Controller", "#{controller_class_name}Helper"
m.class_collisions class_path, "#{class_name}"
# # Controller, helper, and views directories
m.directory File.join('app', 'views', controller_class_path, controller_file_name)
m.directory File.join('spec', 'views', controller_class_path, controller_file_name)
m.directory File.join('app', 'helpers', controller_class_path)
m.directory File.join('spec', 'helpers', controller_class_path)
m.directory File.join('app', 'controllers', controller_class_path)
m.directory File.join('spec', 'controllers', controller_class_path)
m.directory File.join('app', 'models', class_path)
m.directory File.join('spec', 'models', class_path)
# Views
@template_language = defined?(Haml) ? "haml" : "erb"
%w{index show form}.each do |action|
m.template "#{action}.html.#{template_language}",
File.join('app/views', controller_class_path, controller_file_name, "#{action}.html.#{template_language}")
m.template "#{action}.html_spec.rb",
File.join('spec/views', controller_class_path, controller_file_name, "#{action}.html.#{template_language}_spec.rb")
end
m.template "index_partial.html.#{template_language}",
File.join('app/views', controller_class_path, controller_file_name, "_#{file_name}.html.#{template_language}")
m.template 'index_partial.html_spec.rb',
File.join('spec/views', controller_class_path, controller_file_name, "_#{file_name}.html.#{template_language}_spec.rb")
# Helper
m.template 'helper.rb',
File.join('app/helpers', controller_class_path, "#{controller_file_name}_helper.rb")
m.template 'helper_spec.rb',
File.join('spec/helpers', controller_class_path, "#{controller_file_name}_helper_spec.rb")
# Controller
m.template 'controller.rb',
File.join('app/controllers', controller_class_path, "#{controller_file_name}_controller.rb")
m.template 'controller_spec.rb',
File.join('spec/controllers', controller_class_path, "#{controller_file_name}_controller_spec.rb")
# Model
m.template 'model.rb',
File.join('app/models', class_path, "#{file_name}.rb")
m.template 'model_spec.rb',
File.join('spec/models', class_path, "#{file_name}_spec.rb")
# Routing
m.route_resources controller_file_name
unless options[:skip_migration]
m.migration_template(
'migration.rb', 'db/migrate',
:assigns => {
:migration_name => "Create#{class_name.pluralize.gsub(/::/, '')}",
:attributes => attributes
},
:migration_file_name => "create_#{file_path.gsub(/\//, '_').pluralize}"
)
end
end
end
protected
def banner
"Usage: #{$0} skinny_scaffold ModelName [field:type, field:type]"
end
def model_name
class_name.demodulize
end
end

View file

@ -0,0 +1,105 @@
class <%= controller_class_name %>Controller < ApplicationController
<%- if defined?(Resourceful::Maker) -%>
make_resourceful do
actions :all
# Let's get the most use from form_for and share a single form here!
response_for :new, :edit do
render :template => "<%= plural_name %>/form"
end
response_for :create_fails, :update_fails do
flash[:error] = "There was a problem!"
render :template => "<%= plural_name %>/form"
end
end
<%- else -%>
# GET /<%= table_name %>
# GET /<%= table_name %>.xml
def index
@<%= table_name %> = <%= class_name %>.find(:all)
respond_to do |format|
format.html # index.html.erb
format.xml { render :xml => @<%= table_name %> }
end
end
# GET /<%= table_name %>/1
# GET /<%= table_name %>/1.xml
def show
@<%= file_name %> = <%= class_name %>.find(params[:id])
respond_to do |format|
format.html # show.html.erb
format.xml { render :xml => @<%= file_name %> }
end
end
# GET /<%= table_name %>/new
# GET /<%= table_name %>/new.xml
def new
@<%= file_name %> = <%= class_name %>.new
respond_to do |format|
format.html { render :template => "<%= plural_name %>/form" }
format.xml { render :xml => @<%= file_name %> }
end
end
# GET /<%= table_name %>/1/edit
def edit
@<%= file_name %> = <%= class_name %>.find(params[:id])
render :template => "<%= plural_name %>/form"
end
# POST /<%= table_name %>
# POST /<%= table_name %>.xml
def create
@<%= file_name %> = <%= class_name %>.new(params[:<%= file_name %>])
respond_to do |format|
if @<%= file_name %>.save
flash[:notice] = '<%= class_name %> was successfully created.'
format.html { redirect_to(@<%= file_name %>) }
format.xml { render :xml => @<%= file_name %>, :status => :created, :location => @<%= file_name %> }
else
flash.now[:error] = '<%= class_name %> could not be created.'
format.html { render :template => "<%= plural_name %>/form" }
format.xml { render :xml => @<%= file_name %>.errors, :status => :unprocessable_entity }
end
end
end
# PUT /<%= table_name %>/1
# PUT /<%= table_name %>/1.xml
def update
@<%= file_name %> = <%= class_name %>.find(params[:id])
respond_to do |format|
if @<%= file_name %>.update_attributes(params[:<%= file_name %>])
flash[:notice] = '<%= class_name %> was successfully updated.'
format.html { redirect_to(@<%= file_name %>) }
format.xml { head :ok }
else
flash.now[:error] = '<%= class_name %> could not be created.'
format.html { render :template => "<%= plural_name %>/form" }
format.xml { render :xml => @<%= file_name %>.errors, :status => :unprocessable_entity }
end
end
end
# DELETE /<%= table_name %>/1
# DELETE /<%= table_name %>/1.xml
def destroy
@<%= file_name %> = <%= class_name %>.find(params[:id])
@<%= file_name %>.destroy
respond_to do |format|
flash[:notice] = '<%= class_name %> was successfully deleted.'
format.html { redirect_to(<%= table_name %>_url) }
format.xml { head :ok }
end
end
<%- end -%>
end

View file

@ -0,0 +1,116 @@
require File.dirname(__FILE__) + '/../spec_helper'
describe <%= controller_class_name %>Controller do
def valid_attributes(args = {})
{
<% if attributes.empty? -%>
# Add valid attributes for the your params[:<%= singular_name %>] here!
<% else -%>
<%- attributes.each_with_index do |attribute, index| -%>
<%- case attribute.type -%>
<%- when :string, :text -%>
"<%= attribute.name %>" => "foo"<%= index < attributes.size - 1 ? "," : "" %>
<%- when :integer, :float, :decimal -%>
"<%= attribute.name %>" => 815<%= index < attributes.size - 1 ? "," : "" %>
<%- when :boolean -%>
"<%= attribute.name %>" => false<%= index < attributes.size - 1 ? "," : "" %>
<%- when :date, :datetime, :time, :timestamp -%>
"<%= attribute.name %>" => 1.week.ago<%= index < attributes.size - 1 ? "," : "" %>
<%- else -%>
"<%= attribute.name %>" => nil<%= index < attributes.size - 1 ? "," : "" %> # Could not determine valid attribute
<%- end -%>
<%- end -%>
<% end -%>
}.merge(args)
end
describe "GET :index" do
before(:each) do
@<%= plural_name %> = stub_index(<%= class_name %>)
end
it_should_find_and_assign :<%= plural_name %>
it_should_render_template "index"
end
describe "GET :new" do
before(:each) do
@<%= singular_name %> = stub_new(<%= class_name %>)
end
it_should_initialize_and_assign :<%= singular_name %>
it_should_render_template "form"
end
describe "POST :create" do
describe "when successful" do
before(:each) do
@<%= singular_name %> = stub_create(<%= class_name %>)
end
it_should_initialize_and_save :<%= singular_name %>
it_should_set_flash :notice
it_should_redirect_to { <%= singular_name %>_url(@<%= singular_name %>) }
end
describe "when unsuccessful" do
before(:each) do
@<%= singular_name %> = stub_create(<%= class_name %>, :return => :false)
end
it_should_initialize_and_assign :<%= singular_name %>
it_should_set_flash :error
it_should_render_template "form"
end
end
describe "GET :show" do
before(:each) do
@<%= singular_name %> = stub_show(<%= class_name %>)
end
it_should_find_and_assign :<%= singular_name %>
it_should_render_template "show"
end
describe "GET :edit" do
before(:each) do
@<%= singular_name %> = stub_edit(<%= class_name %>)
end
it_should_find_and_assign :<%= singular_name %>
it_should_render_template "form"
end
describe "PUT :update" do
describe "when successful" do
before(:each) do
@<%= singular_name %> = stub_update(<%= class_name %>)
end
it_should_find_and_update :<%= singular_name %>
it_should_set_flash :notice
it_should_redirect_to { <%= singular_name %>_url(@<%= singular_name %>) }
end
describe "when unsuccessful" do
before(:each) do
@<%= singular_name %> = stub_update(<%= class_name %>, :return => :false)
end
it_should_find_and_assign :<%= singular_name %>
it_should_set_flash :error
it_should_render_template "form"
end
end
describe "DELETE :destroy" do
before(:each) do
@<%= singular_name %> = stub_destroy(<%= class_name %>)
end
it_should_find_and_destroy :<%= singular_name %>
it_should_set_flash :notice
it_should_redirect_to { <%= plural_name %>_url }
end
end

View file

@ -0,0 +1,25 @@
<h1><%= singular_name %>.new_record? ? "New" : "Edit" %> <%= model_name %></h1>
<%% form_for(@<%= singular_name %>) do |f| %>
<div id="form_errors">
<%%= f.error_messages %>
</div>
<%- if attributes.blank? -%>
<p>Add your form elements here, please!</p>
<%- else -%>
<%- attributes.each do |attribute| -%>
<p>
<%%= f.label :<%= attribute.name %> %><br />
<%%= f.<%= attribute.field_type %> :<%= attribute.name %> %>
</p>
<%- end -%>
<%- end -%>
<div id="commands">
<%%= submit_tag "Save" %>
<div id="navigation_commands">
<%% unless @<%= singular_name %>.new_record? -%>
<%%= button_to "Show", <%= singular_name %>_path(@<%= singular_name %>), :method => "get", :title => "Show <%= singular_name %>. Unsaved changes will be lost." %>
<%% end -%>
<%%= button_to "Back to List", <%= plural_name %>_path, :class => "cancel", :method => "get", :title => "Return to <%= singular_name %> list without saving changes" %>
</div>
</div>
<%% end -%>

View file

@ -0,0 +1,18 @@
%h1== #{@<%= singular_name %>.new_record? ? "New" : "Edit"} #{<%= model_name %>}
- form_for @<%= singular_name %> do |f|
#form_errors= f.error_messages
<% if attributes.blank? -%>
%p Add your form elements here, please!
<% else -%>
<%- attributes.each do |attribute| -%>
%p
= f.label :<%= attribute.name %>
= f.<%= attribute.field_type %> :<%= attribute.name %>
<%- end -%>
<% end -%>
#commands
= submit_tag "Save"
#navigation_commands
- unless @<%= singular_name %>.new_record?
= button_to "Show", <%= singular_name %>_path(@<%= singular_name %>), :method => "get", :title => "Show <%= singular_name %>. Unsaved changes will be lost."
= button_to "Back to List", <%= plural_name %>_path, :class => "cancel", :method => "get", :title => "Return to <%= singular_name %> list without saving changes"

View file

@ -0,0 +1,41 @@
require File.dirname(__FILE__) + '<%= '/..' * controller_class_nesting_depth %>/../../spec_helper'
describe "<%= File.join(controller_class_path, controller_singular_name) %>/form.html.<%= template_language %>" do
before(:each) do
@<%= singular_name %> = mock_and_assign(<%= model_name %>, :stub => {
<% if attributes.blank? -%>
# Add your stub attributes and return values here like:
# :name => "Foo", :created_at => 1.week.ago, :updated_at => nil
<% else -%>
<%- attributes.each_with_index do |attribute, index| -%>
<%- case attribute.type -%>
<%- when :string, :text -%>
:<%= attribute.name %> => "foo"<%= index < attributes.size - 1 ? "," : "" %>
<%- when :integer, :float, :decimal -%>
:<%= attribute.name %> => 815<%= index < attributes.size - 1 ? "," : "" %>
<%- when :boolean -%>
:<%= attribute.name %> => false<%= index < attributes.size - 1 ? "," : "" %>
<%- when :date, :datetime, :time, :timestamp -%>
:<%= attribute.name %> => 1.week.ago<%= index < attributes.size - 1 ? "," : "" %>
<%- else -%>
:<%= attribute.name %> => nil<%= index < attributes.size - 1 ? "," : "" %> # Could not determine valid attribute
<%- end -%>
<%- end -%>
<% end -%>
})
end
it_should_have_form_for :<%= singular_name %>
<% if attributes.blank? -%>
# Add specs for editing attributes here, please! Like this:
#
# it_should_allow_editing :<%= singular_name %>, :foo
<% else -%>
<%- attributes.each do |attribute| -%>
it_should_allow_editing :<%= singular_name %>, :<%= attribute.name %>
<%- end -%>
<% end -%>
it_should_link_to_show :<%= singular_name %>
it_should_link_to { <%= plural_name %>_path }
end

View file

@ -0,0 +1,2 @@
module <%= controller_class_name %>Helper
end

View file

@ -0,0 +1,5 @@
require File.dirname(__FILE__) + '<%= '/..' * controller_class_nesting_depth %>/../spec_helper'
describe <%= controller_class_name %>Helper do
# Add your specs here or remove this file completely, please!
end

View file

@ -0,0 +1,31 @@
<h1><%= model_name %> List</h1>
<table>
<%%- if @<%= plural_name %>.empty? -%>
<tbody>
<tr class="empty">
<td>There are no <%= plural_name.humanize.downcase %></td>
</tr>
</tbody>
<%%- else -%>
<thead>
<tr>
<%- if attributes.blank? -%>
<th><!-- Generic display column --></th>
<%- else -%>
<%- attributes.each do |attribute| -%>
<th><%= attribute.name.titleize %></th>
<%- end -%>
<%- end -%>
<th class="show"><!-- "Show" link column --></th>
<th class="edit"><!-- "Edit" link column --></th>
<th class="delete"><!-- "Delete" link column --></th>
</tr>
</thead>
<tbody>
<%%= render :partial => @<%= plural_name %> %>
</tbody>
<%%- end -%>
</table>
<div id="commands">
<%%= button_to "New <%= singular_name.titleize %>", new_<%= singular_name %>_path, :method => "get" %>
</div>

View file

@ -0,0 +1,23 @@
%h1 <%= model_name %> List
%table
- if @<%= plural_name %>.empty?
%tbody
%tr.empty
%td== There are no <%= plural_name.humanize.downcase %>
- else
%thead
%tr
<% if attributes.blank? -%>
%th= # Generic display column
<% else -%>
<%- attributes.each do |attribute| -%>
%th <%= attribute.name.titleize %>
<%- end -%>
<% end -%>
%th.show= # 'Show' link column
%th.edit= # 'Edit' link column
%th.delete= # 'Delete' link column
%tbody
= render :partial => @<%= plural_name %>
#commands
= button_to "New <%= singular_name.titleize %>", new_<%= singular_name %>_path, :method => "get"

View file

@ -0,0 +1,15 @@
require File.dirname(__FILE__) + '<%= '/..' * controller_class_nesting_depth %>/../../spec_helper'
describe "<%= File.join(controller_class_path, controller_singular_name) %>/index.html.<%= template_language %>" do
before(:each) do
@<%= plural_name %> = mock_and_assign_collection(<%= model_name %>)
template.stub_render :partial => @<%= plural_name %>
end
it "should render :partial => @<%= plural_name %>" do
template.expect_render :partial => @<%= plural_name %>
do_render
end
it_should_link_to_new :<%= singular_name %>
end

View file

@ -0,0 +1,12 @@
<tr class="<%%= cycle("odd", "even") %>">
<% if attributes.blank? -%>
<td><%= model_name %> #<%%= <%= singular_name %>.id %></td>
<% else -%>
<%- attributes.each do |attribute| -%>
<td><%%=h <%= singular_name %>.<%= attribute.name %> %></td>
<%- end %>
<% end -%>
<td class="show"><%%= button_to "Show", <%= singular_name %>, :method => "get" %></td>
<td class="edit"><%%= button_to "Edit", edit_<%= singular_name %>_path(<%= singular_name %>), :method => "get" %></td>
<td class="delete"><%%= button_to "Delete", <%= singular_name %>, :method => "delete" %></td>
</tr>

View file

@ -0,0 +1,11 @@
%tr{:class => cycle("odd", "even")}
<% if attributes.blank? -%>
%td== <%= model_name %> #{<%= singular_name %>.id}
<% else -%>
<%- attributes.each do |attribute| -%>
%td=h <%= singular_name %>.<%= attribute.name %>
<%- end -%>
<% end -%>
%td.show= button_to "Show", <%= singular_name %>_path(<%= singular_name %>), :method => "get"
%td.edit= button_to "Edit", edit_<%= singular_name %>_path(<%= singular_name %>), :method => "get"
%td.delete= button_to "Delete", <%= singular_name %>, :method => "delete"

View file

@ -0,0 +1,32 @@
require File.dirname(__FILE__) + '<%= '/..' * controller_class_nesting_depth %>/../../spec_helper'
describe "<%= File.join(controller_class_path, controller_singular_name) %>/_<%= singular_name %>.html.<%= template_language %>" do
before(:each) do
@<%= singular_name %> = mock_and_assign(<%= model_name %>, :stub => {
<% if attributes.blank? -%>
# Add your stub attributes and return values here like:
# :name => "Foo", :created_at => 1.week.ago, :updated_at => nil
<% else -%>
<%- attributes.each_with_index do |attribute, index| -%>
<%- case attribute.type -%>
<%- when :string, :text -%>
:<%= attribute.name %> => "foo"<%= index < attributes.size - 1 ? "," : "" %>
<%- when :integer, :float, :decimal -%>
:<%= attribute.name %> => 815<%= index < attributes.size - 1 ? "," : "" %>
<%- when :boolean -%>
:<%= attribute.name %> => false<%= index < attributes.size - 1 ? "," : "" %>
<%- when :date, :datetime, :time, :timestamp -%>
:<%= attribute.name %> => 1.week.ago<%= index < attributes.size - 1 ? "," : "" %>
<%- else -%>
:<%= attribute.name %> => nil<%= index < attributes.size - 1 ? "," : "" %>
<%- end -%>
<%- end -%>
<% end -%>
})
template.stub!(:<%= singular_name %>).and_return(@<%= singular_name %>)
end
it_should_link_to_show :<%= singular_name %>
it_should_link_to_edit :<%= singular_name %>
it_should_link_to_delete :<%= singular_name %>
end

View file

@ -0,0 +1,14 @@
class <%= migration_name %> < ActiveRecord::Migration
def self.up
create_table :<%= table_name %>, :force => true do |t|
<% attributes.each do |attribute| -%>
t.column :<%= attribute.name %>, :<%= attribute.type %>
<% end -%>
t.timestamps
end
end
def self.down
drop_table :<%= table_name %>
end
end

View file

@ -0,0 +1,2 @@
class <%= class_name %> < ActiveRecord::Base
end

View file

@ -0,0 +1,25 @@
require File.dirname(__FILE__) + '<%= '/..' * class_nesting_depth %>/../spec_helper'
describe <%= class_name %> do
def valid_attributes(args = {})
{
# Add valid attributes for building your model instances here!
}.merge(args)
end
before(:each) do
@<%= singular_name %> = <%= class_name %>.new
end
after(:each) do
@<%= singular_name %>.destroy unless @<%= singular_name %>.new_record?
end
# Add your model specs here, please!
# And don't forget about the association matchers built-in to skinny_spec like:
#
# it_should_have_many :foos
# it_should_validate_presence_of :bar
#
# Check out the docs for more information.
end

View file

@ -0,0 +1,15 @@
<h1>Show <%= model_name %></h1>
<% if attributes.blank? -%>
<p>Add your customized markup here, please!</p>
<% else -%>
<%- attributes.each do |attribute| -%>
<p>
<label><%= attribute.name.titleize %>:</label>
<%%=h @<%= singular_name %>.<%= attribute.name %> %>
</p>
<%- end -%>
<% end -%>
<div id="commands">
<%%= button_to "Edit", edit_<%= singular_name %>_path(@<%= singular_name %>), :method => "get" %>
<%%= button_to "Back to List", <%= plural_name %>_path, :method => "get" %>
</div>

View file

@ -0,0 +1,13 @@
%h1== Show #{<%= model_name %>}
<% if attributes.blank? -%>
%p Add your customized markup here, please!
<% else -%>
<%- attributes.each do |attribute| -%>
%p
%label <%= attribute.name.titleize %>:
=h @<%= singular_name %>.<%= attribute.name %>
<%- end -%>
<% end -%>
#commands
= button_to "Edit", edit_<%= singular_name %>_path(@<%= singular_name %>), :method => "get"
= button_to "Back to List", <%= plural_name %>_path, :method => "get"

View file

@ -0,0 +1,32 @@
require File.dirname(__FILE__) + '<%= '/..' * controller_class_nesting_depth %>/../../spec_helper'
describe "<%= File.join(controller_class_path, controller_singular_name) %>/show.html.<%= template_language %>" do
before(:each) do
<% if attributes.blank? -%>
@<%= singular_name %> = mock_and_assign(<%= model_name %>)
<% else -%>
@<%= singular_name %> = mock_and_assign(<%= model_name %>, :stub => {
<%- attributes.each_with_index do |attribute, index| -%>
<%- case attribute.type -%>
<%- when :string, :text -%>
:<%= attribute.name %> => "foo"<%= index < attributes.size - 1 ? "," : "" %>
<%- when :integer, :float, :decimal -%>
:<%= attribute.name %> => 815<%= index < attributes.size - 1 ? "," : "" %>
<%- when :boolean -%>
:<%= attribute.name %> => false<%= index < attributes.size - 1 ? "," : "" %>
<%- when :date, :datetime, :time, :timestamp -%>
:<%= attribute.name %> => 1.week.ago<%= index < attributes.size - 1 ? "," : "" %>
<%- else -%>
:<%= attribute.name %> => nil<%= index < attributes.size - 1 ? "," : "" %>
<%- end -%>
<%- end -%>
})
<% end -%>
end
# Add your specs here, please! But remember not to make them brittle
# by specing specing specific HTML elements and classes.
it_should_link_to_edit :<%= singular_name %>
it_should_link_to { <%= plural_name %>_path }
end

3
vendor/plugins/skinny_spec/init.rb vendored Normal file
View file

@ -0,0 +1,3 @@
if RAILS_ENV == "test"
require "skinny_spec"
end

View file

@ -0,0 +1,46 @@
module LuckySneaks
# These methods are mostly just called internally by various other spec helper
# methods but you're welcome to use them as needed in your own specs.
module CommonSpecHelpers
# Returns class for the specified name. Example:
#
# class_for("foo") # => Foo
def class_for(name)
name.to_s.constantize
rescue NameError
name.to_s.classify.constantize
# Let any other error rise!
end
# Returns instance variable for the specified name. Example:
#
# instance_for("foo") # => @foo
def instance_for(name)
instance_variable_get("@#{name.to_s.underscore}")
end
# Wraps a matcher that checks if the receiver contains an <tt>A</tt> element (link)
# whose <tt>href</tt> attribute is set to the specified path.
def have_link_to(path)
have_tag("a[href='#{path}']")
end
# Returns dummy value for specified attribute based on the datatype expected for that
# attribute.
def dummy_value_for(instance, attribute)
if datatype = instance.column_for_attribute(attribute)
actual = instance.send(attribute)
case datatype.type
when :string, :text
actual == "foo" ? "bar" : "food"
when :integer, :float, :decimal
actual == 108 ? 815 : 108
when :boolean
actual ? false : true
when :date, :datetime, :time, :timestamp
actual == 1.week.ago ? 2.years.ago : 1.week.ago
end
end
end
end
end

View file

@ -0,0 +1,67 @@
module LuckySneaks
module ControllerRequestHelpers # :nodoc:
def self.included(base)
base.extend ExampleGroupMethods
end
private
def define_implicit_request(method)
@controller_method = method
@implicit_request = case method
when :index, :new, :show, :edit
proc { get method, params }
when :create
proc { post :create, params }
when :update
proc { put :update, params }
when :destroy
proc { put :destroy, params }
end
end
def eval_request
instance_eval &self.class.instance_variable_get("@the_request")
rescue ArgumentError # missing block
try_shared_request_definition
end
alias do_request eval_request
def try_shared_request_definition
shared_request
rescue NameError
if @implicit_request
try_implicit_request
else
error_message = "Could not determine request definition for 'describe' context. "
error_message << "Please use define_request or define a shared_request."
raise ArgumentError, error_message
end
end
def try_implicit_request
@implicit_request.call
end
def get_response(&block)
eval_request
block.call(response) if block_given?
response
end
module ExampleGroupMethods
# Defines a request at the example group ("describe") level to be evaluated in the examples. Example:
#
# define_request { get :index, params }
#
# <b>Note:</b> The following methods all define implicit requests: <tt>stub_index</tt>, <tt>stub_new</tt>,
# <tt>stub_create</tt>, <tt>stub_show</tt>, <tt>stub_edit</tt>, <tt>stub_update</tt>, and
# <tt>stub_destroy</tt>. Using them in your <tt>before</tt> blocks will allow you to forego
# defining explicit requests using <tt>define_request</tt>. See
# LuckySneaks::ControllerStubHelpers for information on these methods.
def define_request(&block)
raise ArgumentError, "Must provide a block to define a request!" unless block_given?
@the_request = block
end
end
end
end

View file

@ -0,0 +1,435 @@
$:.unshift File.join(File.dirname(__FILE__), "..")
require "skinny_spec"
module LuckySneaks
module ControllerSpecHelpers # :nodoc:
include LuckySneaks::CommonSpecHelpers
include LuckySneaks::ControllerRequestHelpers
include LuckySneaks::ControllerStubHelpers
def self.included(base)
base.extend ExampleGroupMethods
base.extend ControllerRequestHelpers::ExampleGroupMethods
end
# Evaluates the specified block for each of the RESTful controller methods.
# This is useful to spec that all controller methods redirect when no user is
# logged in.
def with_default_restful_actions(params = {}, &block)
{
:get => :index,
:get => :new,
:post => :create
}.each do |method_id, message|
self.send method_id, message, params
block.call
end
{
:get => :edit,
:put => :update,
:delete => :destroy
}.each do |method_id, message|
if params[:before]
params.delete(:before).call
end
# Presuming any id will do
self.send method_id, message, params.merge(:id => 1)
block.call
end
end
private
def create_ar_class_expectation(name, method, argument = nil, options = {})
args = []
if [:create, :update].include?(@controller_method)
args << (argument.nil? ? valid_attributes : argument)
else
args << argument unless argument.nil?
end
args << options unless options.empty?
if args.empty?
return_value = class_for(name).send(method)
class_for(name).should_receive(method).and_return(return_value)
else
return_value = class_for(name).send(method, *args)
class_for(name).should_receive(method).with(*args).and_return(return_value)
end
end
def create_positive_ar_instance_expectation(name, method, *args)
instance = instance_for(name)
if args.empty?
return_value = instance.send(method)
instance.should_receive(method).and_return(true)
else
return_value = instance.send(method, *args)
instance.should_receive(method).with(*args).and_return(true)
end
end
# These methods are designed to be used at the example group [read: "describe"] level
# to simplify and DRY up common expectations.
module ExampleGroupMethods
# Creates an expectation that the controller method calls <tt>ActiveRecord::Base.find</tt>.
# Examples:
#
# it_should_find :foos # => Foo.should_receive(:find).with(:all)
# it_should_find :foos, :all # An explicit version of the above
# it_should_find :foos, :conditions => {:foo => "bar"} # => Foo.should_receive(:find).with(:all, :conditions => {"foo" => "bar"}
# it_should_find :foo # => Foo.should_recieve(:find).with(@foo.id.to_s)
# it_should_find :foo, :params => "id" # => Foo.should_receive(:find).with(params[:id].to_s)
# it_should_find :foo, 2 # => Foo.should_receive(:find).with("2")
#
# <b>Note:</b> All params (key and value) will be strings if they come from a form element and are handled
# internally with this expectation.
def it_should_find(name, *args)
name_string = name.to_s
name_message = if name_string == name_string.singularize
"a #{name}"
else
name
end
it "should find #{name_message}" do
options = args.extract_options!
# Blech!
argument = if param = params[options.delete(:params)]
param.to_s
else
if args.first
args.first
elsif (instance = instance_variable_get("@#{name}")).is_a?(ActiveRecord::Base)
instance.id.to_s
else
:all
end
end
create_ar_class_expectation name, :find, argument, options
eval_request
end
end
# Creates an expectation that the controller method calls <tt>ActiveRecord::Base.new</tt>.
# Takes optional <tt>params</tt> for the initialization arguments. Example
#
# it_should_initialize :foo # => Foo.should_receive(:new)
# it_should_initialize :foo, :params => :bar # => Foo.should_receive(:new).with(params[:bar])
# it_should_initialize :foo, :bar => "baz" # => Foo.should_receive(:new).with(:bar => "baz")
def it_should_initialize(name, options = {})
it "should initialize a #{name}" do
create_ar_class_expectation name, :new, params[options.delete(:params)], options
eval_request
end
end
# Creates an expectation that the controller method calls <tt>ActiveRecord::Base#save</tt> on the
# named instance. Example:
#
# it_should_save :foo # => @foo.should_receive(:save).and_return(true)
#
# <b>Note:</b> This helper should not be used to spec a failed <tt>save</tt> call. Use <tt>it_should_assign</tt>
# instead, to verify that the instance is captured in an instance variable for the inevitable re-rendering
# of the form template.
def it_should_save(name)
it "should save the #{name}" do
create_positive_ar_instance_expectation name, :save
eval_request
end
end
# Creates an expectation that the controller method calls <tt>ActiveRecord::Base#update_attributes</tt>
# on the named instance. Takes optional argument for <tt>params</tt> to specify in the
# expectation. Examples:
#
# it_should_update :foo # => @foo.should_receive(:update_attributes).and_return(true)
# it_should_update :foo, :params => :bar # => @foo.should_receive(:update_attributes).with(params[:bar]).and_return(true)
#
# <b>Note:</b> This helper should not be used to spec a failed <tt>update_attributes</tt> call. Use
# <tt>it_should_assign</tt> instead, to verify that the instance is captured in an instance variable
# for the inevitable re-rendering of the form template.
def it_should_update(name, options = {})
it "should update the #{name}" do
create_positive_ar_instance_expectation name, :update_attributes, params[options[:params]]
eval_request
end
end
# Creates an expectation that the controller method calls <tt>ActiveRecord::Base#destroy</tt> on the named
# instance. Example:
#
# it_should_destroy :foo # => @foo.should_receive(:destroy).and_return(true)
#
# <b>Note:</b> This helper should not be used to spec a failed <tt>destroy</tt> call. Use
# <tt>it_should_assign</tt> instead, if you need to verify that the instance is captured in an instance
# variable if it is re-rendered somehow. This is probably a really edge use case.
def it_should_destroy(name, options = {})
it "should delete the #{name}" do
create_positive_ar_instance_expectation name, :destroy
eval_request
end
end
# Creates expectation[s] that the controller method should assign the specified
# instance variables along with any specified values. Examples:
#
# it_should_assign :foo # => assigns[:foo].should == @foo
# it_should_assign :foo => "bar" # => assigns[:foo].should == "bar"
# it_should_assign :foo => :nil # => assigns[:foo].should be_nil
# it_should_assign :foo => :not_nil # => assigns[:foo].should_not be_nil
# it_should_assign :foo => :undefined # => controller.send(:instance_variables).should_not include("@foo")
#
# Very special thanks to Rick Olsen for the basis of this code. The only reason I even
# redefine it at all is purely an aesthetic choice for specs like "it should foo"
# over ones like "it foos".
def it_should_assign(*names)
names.each do |name|
if name.is_a?(Symbol)
it_should_assign name => name
elsif name.is_a?(Hash)
name.each do |key, value|
it_should_assign_instance_variable key, value
end
end
end
end
# Wraps the separate expectations <tt>it_should_find</tt> and <tt>it_should_assign</tt>
# for simple cases. If you need more control over the parameters of the find, this
# isn't the right helper method and you should write out the two expectations separately.
def it_should_find_and_assign(*names)
names.each do |name|
it_should_find name
it_should_assign name
end
end
# Wraps the separate expectations <tt>it_should_initialize</tt> and <tt>it_should_assign</tt>
# for simple cases. If you need more control over the parameters of the initialization, this
# isn't the right helper method and you should write out the two expectations separately.
#
# <b>Note:</b> This method is used for controller methods like <tt>new</tt>, where the instance
# is initialized without being saved (this includes failed <tt>create</tt> requests).
# If you want to spec that the controller method successfully saves the instance,
# please use <tt>it_should_initialize_and_save</tt>.
def it_should_initialize_and_assign(*names)
names.each do |name|
it_should_initialize name
it_should_assign name
end
end
# Wraps the separate expectations <tt>it_should_initialize</tt> and <tt>it_should_save</tt>
# for simple cases. If you need more control over the parameters of the initialization, this
# isn't the right helper method and you should write out the two expectations separately.
#
# <b>Note:</b> This method is used for controller methods like <tt>create</tt>, where the instance
# is initialized and successfully saved. If you want to spec that the instance is created
# but not saved, just use <tt>it_should_initialize_and_assign</tt>.
def it_should_initialize_and_save(*names)
names.each do |name|
it_should_initialize name
it_should_save name
end
end
# Wraps the separate expectations <tt>it_should_find</tt> and <tt>it_should_update</tt>
# for simple cases. If you need more control over the parameters of the find, this
# isn't the right helper method and you should write out the two expectations separately.
#
# <b>Note:</b> This method is used for controller methods like <tt>update</tt>, where the
# instance is loaded from the database and successfully saved. If you want to spec that the
# instance is found but not saved, just use <tt>it_should_find_and_assign</tt>.
def it_should_find_and_update(*names)
names.each do |name|
it_should_find name
it_should_update name
end
end
# Wraps the separate expectations <tt>it_should_find</tt> and <tt>it_should_destroy</tt>
# for simple cases. If you need more control over the parameters of the find, this
# isn't the right helper method and you should write out the two expectations separately.
def it_should_find_and_destroy(*names)
names.each do |name|
it_should_find name
it_should_destroy name
end
end
# Creates an expectation that the specified collection (<tt>flash</tt> or <tt>session</tt>)
# contains the specified key and value. To specify that the collection should be set
# to <tt>nil</tt>, specify the value as :nil instead.
def it_should_set(collection, key, value = nil, &block)
it "should set #{collection}[:#{key}]" do
# Allow flash.now[:foo] to remain in the flash
flash.stub!(:sweep) if collection == :flash
eval_request
if value
if value == :nil
self.send(collection)[key].should be_nil
else
self.send(collection)[key].should == value
end
elsif block_given?
self.send(collection)[key].should == block.call
else
self.send(collection)[key].should_not be_nil
end
end
end
# Wraps <tt>it_should_set :flash</tt>. To specify that the collection should be set
# to <tt>nil</tt>, specify the value as :nil instead.
def it_should_set_flash(name, value = nil, &block)
it_should_set :flash, name, value, &block
end
# Wraps <tt>it_should_set :session</tt>. To specify that the collection should be set
# to <tt>nil</tt>, specify the value as :nil instead.
def it_should_set_session(name, value = nil, &block)
it_should_set :session, name, value, &block
end
# Wraps the various <tt>it_should_render_<i>foo</i></tt> methods:
# <tt>it_should_render_template</tt>, <tt>it_should_render_partial</tt>,
# <tt>it_should_render_xml</tt>, <tt>it_should_render_json</tt>,
# <tt>it_should_render_formatted</tt>, and <tt>it_should_render_nothing</tt>.
def it_should_render(render_method, *args)
send "it_should_render_#{render_method}", *args
end
# Creates an expectation that the controller method renders the specified template.
# Accepts the following options which create additional expectations.
#
# <tt>:content_type</tt>:: Creates an expectation that the Content-Type header for the response
# matches the one specified
# <tt>:status</tt>:: Creates an expectation that the HTTP status for the response
# matches the one specified
def it_should_render_template(name, options = {})
create_status_expectation options[:status] if options[:status]
it "should render '#{name}' template" do
eval_request
response.should render_template(name)
end
create_content_type_expectation(options[:content_type]) if options[:content_type]
end
# Creates an expectation that the controller method renders the specified partial.
# Accepts the following options which create additional expectations.
#
# <tt>:content_type</tt>:: Creates an expectation that the Content-Type header for the response
# matches the one specified
# <tt>:status</tt>:: Creates an expectation that the HTTP status for the response
# matches the one specified
def it_should_render_partial(name, options = {})
create_status_expectation options[:status] if options[:status]
it "should render '#{name}' partial" do
controller.expect_render(:partial => name)
eval_request
end
create_content_type_expectation(options[:content_type]) if options[:content_type]
end
# Creates an expectation that the controller method renders the specified record via <tt>to_xml</tt>.
# Accepts the following options which create additional expectations.
#
# <tt>:content_type</tt>:: Creates an expectation that the Content-Type header for the response
# matches the one specified
# <tt>:status</tt>:: Creates an expectation that the HTTP status for the response
# matches the one specified
def it_should_render_xml(record = nil, options = {}, &block)
it_should_render_formatted :xml, record, options, &block
end
# Creates an expectation that the controller method renders the specified record via <tt>to_json</tt>.
# Accepts the following options which create additional expectations.
#
# <tt>:content_type</tt>:: Creates an expectation that the Content-Type header for the response
# matches the one specified
# <tt>:status</tt>:: Creates an expectation that the HTTP status for the response
# matches the one specified
def it_should_render_json(record = nil, options = {}, &block)
it_should_render_formatted :json, record, options, &block
end
# Called internally by <tt>it_should_render_xml</tt> and <tt>it_should_render_json</tt>
# but should not really be called much externally unless you have defined your own
# formats with a matching <tt>to_foo</tt> method on the record.
#
# Which is probably never.
def it_should_render_formatted(format, record = nil, options = {}, &block)
create_status_expectation options[:status] if options[:status]
it "should render #{format.inspect}" do
if record.is_a?(Hash)
options = record
record = nil
end
if record.nil? && !block_given?
raise ArgumentError, "it_should_render must be called with either a record or a block and neither was given."
else
if record
pieces = record.to_s.split(".")
record = instance_variable_get("@#{pieces.shift}")
record = record.send(pieces.shift) until pieces.empty?
end
block ||= proc { record.send("to_#{format}") }
get_response do |response|
response.should have_text(block.call)
end
end
end
create_content_type_expectation(options[:content_type]) if options[:content_type]
end
# Creates an expectation that the controller method returns a blank page. You'd already
# know when and why to use this so I'm not typing it out.
def it_should_render_nothing(options = {})
create_status_expectation options[:status] if options[:status]
it "should render :nothing" do
get_response do |response|
response.body.strip.should be_blank
end
end
end
# Creates an expectation that the controller method redirects to the specified destination. Example:
#
# it_should_redirect_to { foos_url }
#
# <b>Note:</b> This method takes a block to evaluate the route in the example
# context rather than the example group context.
def it_should_redirect_to(hint = nil, &route)
if hint.nil? && route.respond_to?(:to_ruby)
hint = route.to_ruby.gsub(/(^proc \{)|(\}$)/, '').strip
end
it "should redirect to #{(hint || route)}" do
eval_request
response.should redirect_to(instance_eval(&route))
end
end
private
def it_should_assign_instance_variable(name, value)
expectation_proc = case value
when :nil
proc { assigns[name].should be_nil }
when :not_nil
proc { assigns[name].should_not be_nil }
when :undefined
proc { controller.send(:instance_variables).should_not include("@{name}") }
when Symbol
if (instance_variable = instance_variable_get("@#{name}")).nil?
proc { assigns[name].should_not be_nil }
else
proc { assigns[name].should == instance_variable }
end
else
proc { assigns[name].should == value }
end
it "should #{value == :nil ? 'not ' : ''}assign @#{name}" do
eval_request
instance_eval &expectation_proc
end
end
end
end
end

View file

@ -0,0 +1,199 @@
module LuckySneaks # :nodoc:
# These methods are designed to be used in your example <tt>before</tt> blocks to accomplish
# a whole lot of functionality with just a tiny bit of effort. The methods which correspond
# to the controller methods perform the most duties as they create the mock_model instances,
# stub out all the necessary methods, and also create implicit requests to DRY up your spec
# file even more. You are encouraged to use these methods to setup the basic calls for your
# resources and only resort to the other methods when mocking and stubbing secondary objects
# and calls.
#
# Both <tt>stub_create</tt> and <tt>stub_update</tt> benefit from having a <tt>valid_attributes</tt>
# method defined at the top level of your example groups, ie the top-most "describe" block
# of the spec file. If you did not generate your specs with <tt>skinny_scaffold</tt> or
# <tt>skinny_resourceful</tt> generators, you can simply write a method like the following
# for yourself:
#
# def valid_attributes
# {
# "foo" => "bar",
# "baz" => "quux"
# }
# end
#
# Note this method employs strings as both the key and values to best replicate the way
# they are used in actual controllers where the params will come from a form.
module ControllerStubHelpers
# Stubs out <tt>find :all</tt> and returns a collection of <tt>mock_model</tt>
# instances of that class. Accepts the following options:
#
# <b>:format</b>:: Format of the request. Used to only add <tt>to_xml</tt> and
# <tt>to_json</tt> when actually needed.
# <b>:size</b>:: Number of instances to return in the result. Default is 3.
# <b>:stub</b>:: Additional methods to stub on the instances
#
# Any additional options will be passed as arguments to the class find.
# You will want to make sure to pass those arguments to the <tt>it_should_find</tt> spec as well.
def stub_find_all(klass, options = {})
returning(Array.new(options[:size] || 3){mock_model(klass)}) do |collection|
stub_out klass, options.delete(:stub)
if format = options.delete(:format)
stub_formatted collection, format
params[:format] = format
end
if options.empty?
klass.stub!(:find).with(:all).and_return(collection)
else
klass.stub!(:find).with(:all, options).and_return(collection)
end
end
end
# Alias for <tt>stub_find_all</tt> but additionally defines an implicit request <tt>get :index</tt>.
def stub_index(klass, options = {})
define_implicit_request :index
stub_find_all klass, options
end
# Stubs out <tt>new</tt> method and returns a <tt>mock_model</tt> instance marked as a new record.
# Accepts the following options:
#
# <b>:format</b>:: Format of the request. Used to only add <tt>to_xml</tt> and
# <tt>to_json</tt> when actually needed.
# <b>:stub</b>:: Additional methods to stub on the instances
#
# It also accepts some options used to stub out <tt>save</tt> with a specified <tt>true</tt>
# or <tt>false</tt> but you should be using <tt>stub_create</tt> in that case.
def stub_initialize(klass, options = {})
returning mock_model(klass) do |member|
stub_out member, options.delete(:stub)
if format = options[:format]
stub_formatted member, format
params[:format] = format
end
klass.stub!(:new).and_return(member)
if options[:stub_save]
stub_ar_method member, :save, options[:return]
klass.stub!(:new).with(params[options[:params]]).and_return(member)
else
member.stub!(:new_record?).and_return(true)
member.stub!(:id).and_return(nil)
end
end
end
# Alias for <tt>stub_initialize</tt> which additionally defines an implicit request <tt>get :new</tt>.
def stub_new(klass, options = {})
define_implicit_request :new
stub_initialize klass, options
end
# Alias for <tt>stub_initialize</tt> which additionally defines an implicit request <tt>post :create</tt>.
#
# <b>Note:</b> If <tt>stub_create<tt> is provided an optional <tt>:params</tt> hash
# or the method <tt>valid_attributes</tt> is defined within its scope,
# those params will be added to the example's <tt>params</tt> object. If <i>neither</i>
# are provided an <tt>ArgumentError</tt> will be raised.
def stub_create(klass, options = {})
define_implicit_request :create
if options[:params].nil?
if self.respond_to?(:valid_attributes)
params[klass.name.underscore.to_sym] = valid_attributes
options[:params] = valid_attributes
else
error_message = "Params for creating #{klass} could not be determined. "
error_message << "Please define valid_attributes method in the base 'describe' block "
error_message << "or manually set params in the before block."
raise ArgumentError, error_message
end
end
stub_initialize klass, options.merge(:stub_save => true)
end
# Stubs out <tt>find</tt> and returns a single <tt>mock_model</tt>
# instances of that class. Accepts the following options:
#
# <b>:format</b>:: Format of the request. Used to only add <tt>to_xml</tt> and
# <tt>to_json</tt> when actually needed.
# <b>:stub</b>:: Additional methods to stub on the instances
#
# Any additional options will be passed as arguments to <tt>find</tt>.You will want
# to make sure to pass those arguments to the <tt>it_should_find</tt> spec as well.
#
# <b>Note:</b> The option <tt>:stub_ar</tt> is used internally by <tt>stub_update</tt>
# and <tt>stub_destroy</tt>. If you need to stub <tt>update_attributes</tt> or
# <tt>destroy</tt> you should be using the aforementioned methods instead.
def stub_find_one(klass, options = {})
returning mock_model(klass) do |member|
stub_out member, options.delete(:stub)
if options[:format]
stub_formatted member, options[:format]
params[:format] = options[:format]
end
if options[:current_object]
params[:id] = member.id
if options[:stub_ar]
stub_ar_method member, options[:stub_ar], options[:return]
end
end
klass.stub!(:find).with(member.id.to_s).and_return(member)
end
end
# Alias for <tt>stub_find_one</tt> which additionally defines an implicit request <tt>get :show</tt>.
def stub_show(klass, options = {})
define_implicit_request :show
stub_find_one klass, options.merge(:current_object => true)
end
# Alias for <tt>stub_find_one</tt> which additionally defines an implicit request <tt>get :edit</tt>.
def stub_edit(klass, options = {})
define_implicit_request :edit
stub_find_one klass, options.merge(:current_object => true)
end
# Alias for <tt>stub_find_one</tt> which additionally defines an implicit request <tt>put :update</tt>
# and stubs out the <tt>update_attribute</tt> method on the instance as well.
#
# <b>Note:</b> If <tt>stub_update<tt> is provided an optional <tt>:params</tt> hash
# or the method <tt>valid_attributes</tt> is defined within its scope,
# those params will be added to the example's <tt>params</tt> object. If <i>neither</i>
# are provided an <tt>ArgumentError</tt> will be raised.
def stub_update(klass, options = {})
define_implicit_request :update
stub_find_one klass, options.merge(:current_object => true, :stub_ar => :update_attributes)
end
# Alias for <tt>stub_find_one</tt> which additionally defines an implicit request <tt>delete :destroy</tt>
# and stubs out the <tt>destroy</tt> method on the instance as well.
def stub_destroy(klass, options = {})
define_implicit_request :destroy
stub_find_one klass, options.merge(:current_object => true, :stub_ar => :destroy)
end
# Stubs <tt>to_xml</tt> or <tt>to_json</tt> respectively based on <tt>format</tt> argument.
def stub_formatted(object, format)
return unless format
object.stub!("to_#{format}").and_return("#{object.class} formatted as #{format}")
end
private
# Stubs out multiple methods. You shouldn't be calling this yourself and if you do
# you should be able to understand the code yourself, right?
def stub_out(object, stubs = {})
return if stubs.nil?
stubs.each do |method, value|
if value
object.stub!(method).and_return(value)
else
object.stub!(method)
end
end
end
# Stubs out ActiveRecord::Base methods like #save, #update_attributes, etc
# that may be called on a found or instantiated mock_model instance.
def stub_ar_method(object, method, return_value)
object.stub!(method).and_return(return_value ? false : true)
end
end
end

View file

@ -0,0 +1,326 @@
$:.unshift File.join(File.dirname(__FILE__), "..")
require "skinny_spec"
module LuckySneaks
# These methods are designed to be used in your example [read: "it"] blocks
# to make your model specs a little more DRY. You might also be interested
# in checking out the example block [read: "describe"] level versions in of these
# methods which can DRY things up even more:
# LuckySneaks::ModelSpecHelpers::ExampleGroupLevelMethods
#
# <b>Note:</b> The validation matchers are only meant to be used for simple validation checking
# not as a one-size-fits-all solution.
module ModelSpecHelpers
include LuckySneaks::CommonSpecHelpers
def self.included(base) # :nodoc:
base.extend ExampleGroupLevelMethods
end
class AssociationMatcher # :nodoc:
def initialize(associated, macro)
@associated = associated
@macro = macro
@options = {}
end
def matches?(main_model)
unless main_model.respond_to?(:reflect_on_association)
if main_model.class.respond_to?(:reflect_on_association)
main_model = main_model.class
else
@not_model = main_model
return false
end
end
if @association = main_model.reflect_on_association(@associated)
@options.all?{|k, v| @association.options[k] == v ||
[@association.options[k]] == v} # Stupid to_a being obsoleted!
end
end
def failure_message
if @not_model
" expected: #{@not_model} to be a subclass of ActiveRecord::Base class, but was not"
elsif @association
" expected: #{association_with(@options)}\n got: #{association_with(@association.options)}"
else
" expected: #{association_with(@options)}, but the association does not exist"
end
end
def negative_failure_message
if @association
" expected: #{association_with(@options)}\n got: #{association_with(@association.options)}"
else
" expected: #{association_with(@options)} to not occur but it does"
end
end
# The following public methods are chainable extensions on the main matcher
# Examples:
#
# Foo.should have_many(:bars).through(:foobars).with_dependent(:destroy)
# Bar.should belong_to(:baz).with_class_name("Unbaz")
def through(through_model)
@options[:through] = through_model
self
end
def and_includes(included_models)
@options[:include] = included_models
self
end
def and_extends(*modules)
@options[:extends] = modules
self
end
def with_counter_cache(counter_cache = false)
if counter_cache
@options[:counter_cache] = counter_cache
end
self
end
def uniq(*irrelevant_args)
@options[:uniq] = true
self
end
alias and_is_unique uniq
alias with_unique uniq
def polymorphic(*irrelevant_args)
@options[:polymorphic] = true
self
end
alias and_is_polymorphic polymorphic
alias with_polymorphic polymorphic
def as(interface)
@options[:as] = interface
end
# Use this to just specify the options as a hash.
# Note: It will completely override any previously set options
def with_options(options = {})
options.each{|k, v| @options[k] = v}
self
end
private
# Takes care of methods like with_dependent(:destroy)
def method_missing(method_id, *args, &block)
method_name = method_id.to_s
if method_name =~ /^with_(.*)/
@options[$1.to_sym] = args
self
else
super method_id, *args, &block
end
end
def association_with(options)
option_string = (options.nil? || options.empty?) ? "" : options.inspect
unless option_string.blank?
option_string.sub! /^\{(.*)\}$/, ', \1'
option_string.gsub! /\=\>/, ' => '
end
"#{@macro} :#{@associated}#{option_string}"
end
end
# Creates matcher that checks if the receiver has a <tt>belongs_to</tt> association
# with the specified model.
#
# <b>Note:</b> The argument should be a symbol as in the model's association definition
# and not the model's class name.
def belong_to(model)
AssociationMatcher.new model, :belongs_to
end
# Creates matcher that checks if the receiver has a <tt>have_one</tt> association
# with the specified model.
#
# <b>Note:</b> The argument should be a symbol as in the model's association definition
# and not the model's class name.
def have_one(model)
AssociationMatcher.new model, :has_one
end
# Creates matcher that checks if the receiver has a <tt>have_many</tt> association
# with the specified model.
#
# <b>Note:</b> The argument should be a symbol as in the model's association definition
# and not the model's class name.
def have_many(models)
AssociationMatcher.new models, :has_many
end
# Creates matcher that checks if the receiver has a <tt>have_and_belong_to_many</tt> association
# with the specified model.
#
# <b>Note:</b> The argument should be a symbol as in the model's association definition
# and not the model's class name.
def have_and_belong_to_many(models)
AssociationMatcher.new models, :has_and_belongs_to_many
end
private
def class_or_instance
@model_spec_class_or_instance ||= class_for(self.class.description_text) || instance
end
def instance
@model_spec_instance ||= instance_for(self.class.description_text)
end
# These methods are designed to be used at the example group [read: "describe"] level
# to simplify and DRY up common expectations. Most of these methods are wrappers for
# matchers which can also be used on the example level [read: within an "it" block]. See
# LuckySneaks::ModelSpecHelpers for more information.
module ExampleGroupLevelMethods
# Creates an expectation that the current model being spec'd has a <tt>belongs_to</tt>
# association with the specified model.
#
# <b>Note:</b> The argument should be a symbol as in the model's association definition
# and not the model's class name.
def it_should_belong_to(model)
it "should belong to a #{model}" do
class_or_instance.should belong_to(model)
end
end
# Creates an expectation that the current model being spec'd has a <tt>have_one</tt>
# association with the specified model.
#
# <b>Note:</b> The argument should be a symbol as in the model's association definition
# and not the model's class name.
def it_should_have_one(model)
it "should have one #{model}" do
class_or_instance.should have_one(model)
end
end
# Creates an expectation that the current model being spec'd has a <tt>have_many</tt>
# association with the specified model.
#
# <b>Note:</b> The argument should be a symbol as in the model's association definition
# and not the model's class name.
def it_should_have_many(models)
it "should have many #{models}" do
class_or_instance.should have_many(models)
end
end
# Creates an expectation that the current model being spec'd has a <tt>have_and_belong_to_many</tt>
# association with the specified model.
#
# <b>Note:</b> The argument should be a symbol as in the model's association definition
# and not the model's class name.
def it_should_have_and_belong_to_many(models)
it "should have and belong to many #{models}" do
class_or_instance.should have_and_belong_to_many(models)
end
end
# Creates an expectation that the current model being spec'd <tt>validates_presence_of</tt>
# the specified attribute. Takes an optional custom message to match the one in the model's
# validation.
def it_should_validate_presence_of(attribute, message = ActiveRecord::Errors.default_error_messages[:blank])
it "should not be valid if #{attribute} is blank" do
instance.send "#{attribute}=", nil
instance.errors_on(attribute).should include(message)
end
end
# Creates an expectation that the current model being spec'd <tt>validates_numericality_of</tt>
# the specified attribute. Takes an optional custom message to match the one in the model's
# validation.
def it_should_validate_numericality_of(attribute, message = ActiveRecord::Errors.default_error_messages[:not_a_number])
it "should validate #{attribute} is a numeric" do
instance.send "#{attribute}=", "NaN"
instance.errors_on(attribute).should include(message)
end
end
# Creates an expectation that the current model being spec'd <tt>validates_confirmation_of</tt>
# the specified attribute. Takes an optional custom message to match the one in the model's
# validation.
def it_should_validate_confirmation_of(attribute, message = ActiveRecord::Errors.default_error_messages[:confirmation])
it "should validate confirmation of #{attribute}" do
dummy_value = dummy_value_for(instance, attribute) || "try a string"
instance.send "#{attribute}=", dummy_value
instance.send "#{attribute}_confirmation=", dummy_value.succ
instance.errors_on(attribute).should include(message)
end
end
# Creates an expectation that the current model being spec'd <tt>validates_uniqueness_of</tt>
# the specified attribute. Takes an optional custom message to match the one in the model's
# validation.
#
# <b>Note:</b> This method will fail completely if <tt>valid_attributes</tt>
# does not provide all the attributes needed to create a valid record.
def it_should_validate_uniqueness_of(attribute, message = ActiveRecord::Errors.default_error_messages[:taken])
it "should validate #{attribute} confirmation" do
previous_instance = class_for(self.class.description_text).create!(valid_attributes)
instance.attributes = valid_attributes
instance.errors_on(attribute).should include(message)
previous_instance.destroy
end
end
# Creates an expectation that the current model being spec'd accepts the specified values as
# valid for the specified attribute. This is most likely used with <tt>validates_format_of</tt>
# but there's nothing saying it couldn't be another validation.
def it_should_accept_as_valid(attribute, *values)
values.each do |value|
value_inspect = case value
when String : "'#{value}'"
when NilClass : "nil"
else value
end
it "should accept #{value_inspect} as a valid #{attribute}" do
instance.send "#{attribute}=", value
instance.errors_on(attribute).should == []
end
end
end
# Creates an expectation that the current model being spec'd does not accept the specified
# values as valid for the specified attribute. This is most likely used with
# <tt>validates_format_of</tt> but there's nothing saying it couldn't be another validation.
# Takes an optional argument <tt>:message => "some custom error messsage"</tt> for
# spec'ing the actual error message.
def it_should_not_accept_as_valid(attribute, *values)
options = values.extract_options!
values.each do |value|
value_inspect = case value
when String : "'#{value}'"
when NilClass : "nil"
else value
end
it "should not accept #{value_inspect} as a valid #{attribute}" do
instance.send "#{attribute}=", value
if options[:message]
instance.errors_on(attribute).should include(options[:message])
else
instance.should have_at_least(1).errors_on(attribute)
end
end
end
end
# Creates an expectation that the current model being spec'd doesn't allow mass-assignment
# of the specified attribute.
def it_should_not_mass_assign(attribute)
it "should not allow mass-assignment of #{attribute}" do
lambda {
instance.send :attributes=, {attribute => dummy_value_for(instance, attribute)}
}.should_not change(instance, attribute)
end
end
end
end
end

View file

@ -0,0 +1,460 @@
$:.unshift File.join(File.dirname(__FILE__), "..")
require "skinny_spec"
module LuckySneaks
# These methods are designed to be used in your example [read: "it"] blocks
# to make your view specs less brittle and more DRY. You might also be interested
# in checking out the example block [read: "describe"] level versions in of these
# methods which can DRY things up even more:
# LuckySneaks::ViewSpecHelpers::ExampleGroupLevelMethods
module ViewSpecHelpers
include LuckySneaks::CommonSpecHelpers
include ActionController::PolymorphicRoutes
def self.included(base) # :nodoc:
base.extend ExampleGroupLevelMethods
end
# Wraps a matcher that checks if the receiver contains a <tt>FORM</tt> element with
# its <tt>action</tt> attribute set to the specified path.
def submit_to(path)
have_tag("form[action=#{path}]")
end
# Wraps a matcher that checks is the receiver contains any of several form elements
# that would return sufficient named parameters to allow editing of the specified
# attribute on the specified instance. Example:
#
# response.should allow_editing(@foo, "bar")
#
# can be satisfied by any of the following HTML elements:
#
# <input name="foo[bar]" type="text" />
# <input name="foo[bar]" type="checkbox" />
# <input name="foo[bar_ids][]" type="checkbox" />
# <select name="foo[bar]"></select>
# <textarea name="foo[bar]"></textarea>
def allow_editing(instance, attribute)
instance_name = instance.class.name.underscore.downcase
if instance.send(attribute).is_a?(Time)
have_tag(
"input[name='#{instance_name}[#{attribute}]'],
select[name=?]", /#{instance_name}\[#{attribute}\(.*\)\]/
)
else
have_tag(
"input[type='text'][name='#{instance_name}[#{attribute}]'],
input[type='password'][name='#{instance_name}[#{attribute}]'],
select[name='#{instance_name}[#{attribute}]'],
textarea[name='#{instance_name}[#{attribute}]'],
input[type='checkbox'][name='#{instance_name}[#{attribute}]'],
input[type='checkbox'][name='#{instance_name}[#{attribute.to_s.tableize.singularize}_ids][]']"
)
end
end
# Wraps a matcher that checks if the receiver contains an <tt>A</tt> element (link)
# whose <tt>href</tt> attribute is set to the specified path or a <tt>FORM</tt>
# element whose <tt>action</tt> attribute is set to the specified path.
def have_link_or_button_to(path)
have_tag(
"a[href='#{path}'],
form[action='#{path}'] input,
form[action='#{path}'] button"
)
end
alias have_link_to have_link_or_button_to
alias have_button_to have_link_or_button_to
# Wraps <tt>have_link_or_button_to new_polymorphic_path<tt> for the specified class which
# corresponds with the <tt>new</tt> method of the controller.
#
# <b>Note:</b> This method may takes a string or symbol representing the model's name
# to send to <tt>have_link_or_button_to_show</tt> or the model's name itself.
def have_link_or_button_to_new(name)
have_link_or_button_to new_polymorphic_path(name.is_a?(ActiveRecord::Base) ? name : class_for(name))
end
# Wraps <tt>have_link_or_button_to polymorphic_path(instance)<tt> which
# corresponds with the <tt>show</tt> method of the controller.
def have_link_or_button_to_show(instance)
have_link_or_button_to polymorphic_path(instance)
end
alias have_link_to_show have_link_or_button_to_show
alias have_button_to_show have_link_or_button_to_show
# Wraps <tt>have_link_or_button_to edit_polymorphic_path(instance)<tt> which
# corresponds with the <tt>edit</tt> method of the controller.
def have_link_or_button_to_edit(instance)
have_link_or_button_to edit_polymorphic_path(instance)
end
alias have_link_to_edit have_link_or_button_to_edit
alias have_button_to_edit have_link_or_button_to_edit
# Wraps a matcher that checks if the receiver contains the HTML created by Rails'
# <tt>button_to</tt> helper: to wit, a <tt>FORM</tt> element whose <tt>action</tt>
# attribute is pointed at the <tt>polymorphic_path</tt> of the instance
# and contains an <tt>INPUT</tt> named "_method" with a value of "delete".
def have_button_to_delete(instance)
path = polymorphic_path(instance)
have_tag(
"form[action='#{path}'] input[name='_method'][value='delete'] + input,
form[action='#{path}'] input[name='_method'][value='delete'] + button"
)
end
# Creates a <tt>mock_model</tt> instance and adds it to the <tt>assigns</tt> collection
# using either the name passed as the first argument or the underscore version
# of its class name. Accepts optional arguments to stub out additional methods
# (and their return values) on the <tt>mock_model</tt> instance. Example:
#
# mock_and_assign(Foo, :stub => {:bar => "bar"})
#
# is the same as running <tt>assigns[:foo] = mock_model(Foo, :bar => "bar")</tt>.
#
# mock_and_assign(Foo, "special_foo", :stub => {:bar => "baz"})
#
# is the same as running <tt>assigns[:special_foo] = mock_model(Foo, :bar => "baz").
#
# <b>Note:</b> Adding to the assigns collection returns the object added, so this can
# be chained a la <tt>@foo = mock_and_assign(Foo)</tt>.
def mock_and_assign(klass, *args)
options = args.extract_options!
mocked = if options[:stub]
mock_model(klass, options[:stub])
else
mock_model(klass)
end
yield mocked if block_given?
self.assigns[args.first || "#{klass}".underscore] = mocked
end
# Creates an array of <tt>mock_model</tt> instances in the manner of
# <tt>mock_and_assign</tt>. Accepts <tt>option[:size]</tt> which sets the size
# of the array (default is 3).
def mock_and_assign_collection(klass, *args)
options = args.dup.extract_options!
return_me = Array.new(options[:size] || 3) do
mocked = if options[:stub]
mock_model(klass, options[:stub])
else
mock_model(klass)
end
yield mocked if block_given?
mocked
end
self.assigns[args.first || "#{klass}".tableize] = return_me
end
private
def do_render
if @the_template
render @the_template
elsif File.exists?(File.join(RAILS_ROOT, "app/views", self.class.description_text))
render self.class.description_text
else
error_message = "Cannot determine template for render. "
error_message << "Please define @the_template in the before block "
error_message << "or name your describe block so that it indicates the correct template."
raise NameError, error_message
end
end
# These methods are designed to be used at the example group [read: "describe"] level
# to simplify and DRY up common expectations. Most of these methods are wrappers for
# matchers which can also be used on the example level [read: within an "it" block]. See
# LuckySneaks::ViewSpecHelpers for more information.
module ExampleGroupLevelMethods
include LuckySneaks::CommonSpecHelpers
# Creates an expectation which calls <tt>submit_to</tt> on the response
# from rendering the template. See that method for more details.
#
# <b>Note:</b> This method takes a Proc to evaluate the route not simply a named route
# helper, which would be undefined in the scope of the example block.
def it_should_submit_to(hint = nil, &route)
if hint.nil? && route.respond_to?(:to_ruby)
hint = route.to_ruby.gsub(/(^proc \{)|(\}$)/, '').strip
end
it "should submit to #{(hint || route)}" do
do_render
response.should submit_to(instance_eval(&route))
end
end
# Creates an expectation that the template uses Rails' <tt>form_for</tt> to generate
# the proper form action and method to create or update the specified object.
#
# <b>Note:</b> This method takes a string or symbol representing the instance
# variable's name to create the expectation for <tt>form_for</tt>
# not an instance variable, which would be nil in the scope of the example block.
# If you use namespacing for your <tt>form_for</tt>, you'll have to manually write out
# a similar spec.
def it_should_have_form_for(name)
it "should have a form_for(@#{name})" do
template.should_receive(:form_for).with(instance_for(name))
do_render
end
end
# Negative version of <tt>it_should_have_form_for</tt>. See that method for more
# details.
def it_should_not_have_form_for(name)
it "should not have a form_for(@#{name})" do
template.should_not_receive(:form_for).with(instance_for(name))
do_render
end
end
# Creates an expectation which calls <tt>allow_editing</tt> on the response
# from rendering the template. See that method for more details.
#
# <b>Note:</b> This method takes a string or symbol representing the instance
# variable's name to send to <tt>allow_editing</tt>
# not an instance variable, which would be nil in the scope of the example block.
def it_should_allow_editing(name, method)
it "should allow editing of @#{name}##{method}" do
do_render
response.should allow_editing(instance_for(name), method)
end
end
# Negative version of <tt>it_should_allow_editing</tt>. See that method for more
# details.
def it_should_not_allow_editing(name, method)
it "should not allow editing of @#{name}##{method}" do
do_render
response.should_not allow_editing(instance_for(name), method)
end
end
# Creates an expectation that the rendered template contains a <tt>FORM</tt> element
# (<tt>INPUT</tt>, <tt>TEXTAREA</tt>, or <tt>SELECT</tt>) with the specified name.
def it_should_have_form_element_for(name)
it "should have a form element named '#{name}'" do
do_render
response.should have_tag(
"form input[name='#{name}'],
form textarea[name='#{name}'],
form select[name='#{name}']"
)
end
end
# Negative version of <tt>it_should_have_form_element_for</tt>. See that method
# for more details.
def it_should_not_have_form_element_for(name)
it "should not have a form element named '#{name}'" do
do_render
response.should_not have_tag(
"form input[name='#{name}'],
form textarea[name='#{name}'],
form select[name='#{name}']"
)
end
end
# Creates an expectation which calls <tt>have_link_or_button_to</tt> on the response
# from rendering the template. See that method for more details.
#
# <b>Note:</b> This method takes a block to evaluate the route in the example context
# instead of the example group context.
def it_should_link_to(hint = nil, &route)
if hint.nil? && route.respond_to?(:to_ruby)
hint = route.to_ruby.gsub(/(^proc \{)|(\}$)/, '').strip
end
it "should have a link/button to #{(hint || route)}" do
do_render
response.should have_link_or_button_to(instance_eval(&route))
end
end
alias it_should_have_link_to it_should_link_to
alias it_should_have_button_to it_should_link_to
alias it_should_have_button_or_link_to it_should_link_to
# Negative version of <tt>it_should_link_to</tt>. See that method
# for more details.
def it_should_not_link_to(hint = nil, &route)
if hint.nil? && route.respond_to?(:to_ruby)
hint = route.to_ruby.gsub(/(^proc \{)|(\}$)/, '').strip
end
it "should have a link/button to #{(hint || route)}" do
do_render
response.should_not have_link_or_button_to(instance_eval(&route))
end
end
alias it_should_not_have_link_to it_should_not_link_to
alias it_should_not_have_button_to it_should_not_link_to
alias it_should_not_have_button_or_link_to it_should_not_link_to
# Creates an expectation which calls <tt>have_link_or_button_to_new</tt> on the response
# from rendering the template. See that method for more details.
#
# <b>Note:</b> This method may takes a string or symbol representing the model's name
# to send to <tt>have_link_or_button_to_show</tt> or the model's name itself.
def it_should_link_to_new(name)
it "should have a link/button to create a new #{name}" do
do_render
response.should have_link_or_button_to_new(name)
end
end
alias it_should_have_link_to_new it_should_link_to_new
alias it_should_have_button_to_new it_should_link_to_new
alias it_should_have_button_or_link_to_new it_should_link_to_new
# Negative version of <tt>it_should_link_to_show</tt>. See that method
# for more details.
def it_should_not_link_to_new(name)
it "should have a link/button to create a new #{name}" do
do_render
response.should_not have_link_or_button_to_new(name)
end
end
alias it_should_not_have_link_to_new it_should_not_link_to_new
alias it_should_not_have_button_to_new it_should_not_link_to_new
alias it_should_not_have_button_or_link_to_new it_should_not_link_to_new
# Creates an expectation which calls <tt>have_link_or_button_to_show</tt> on the response
# from rendering the template. See that method for more details.
#
# <b>Note:</b> This method takes a string or symbol representing the instance
# variable's name to send to <tt>have_link_or_button_to_show</tt>
# not an instance variable, which would be nil in the scope of the example block.
def it_should_link_to_show(name)
it "should have a link/button to show @#{name}" do
do_render
response.should have_link_or_button_to_show(instance_for(name))
end
end
alias it_should_have_link_to_show it_should_link_to_show
alias it_should_have_button_to_show it_should_link_to_show
alias it_should_have_button_or_link_to_show it_should_link_to_show
# Negative version of <tt>it_should_link_to_show</tt>. See that method
# for more details.
def it_should_not_link_to_show(name)
it "should have a link/button to show @#{name}" do
do_render
response.should_not have_link_or_button_to_show(instance_for(name))
end
end
alias it_should_not_have_link_to_show it_should_not_link_to_show
alias it_should_not_have_button_to_show it_should_not_link_to_show
alias it_should_not_have_button_or_link_to_show it_should_not_link_to_show
# Creates an expectation which calls <tt>have_link_or_button_to_show</tt>
# for each member of the instance variable matching the specified name
# on the response from rendering the template. See that method for more details.
#
# <b>Note:</b> This method takes a string or symbol representing the instance
# variable's name and not an instance variable, which would be nil
# in the scope of the example block.
def it_should_link_to_show_each(name)
it "should have a link/button to show each member of @#{name}" do
do_render
instance_for(name).each do |member|
response.should have_link_or_button_to_show(member)
end
end
end
alias it_should_have_link_to_show_each it_should_link_to_show_each
alias it_should_have_button_to_show_each it_should_link_to_show_each
alias it_should_have_button_or_link_to_show_each it_should_link_to_show_each
# Creates an expectation which calls <tt>have_link_or_button_to_edit</tt> on the response
# from rendering the template. See that method for more details.
#
# <b>Note:</b> This method takes a string or symbol representing the instance
# variable's name to send to <tt>have_link_or_button_to_edit</tt>
# not an instance variable, which would be nil in the scope of the example block.
def it_should_link_to_edit(name)
it "should have a link/button to edit @#{name}" do
do_render
response.should have_link_or_button_to_edit(instance_for(name))
end
end
alias it_should_have_link_to_edit it_should_link_to_edit
alias it_should_have_button_to_edit it_should_link_to_edit
alias it_should_have_button_or_link_to_edit it_should_link_to_edit
# Negative version of <tt>it_should_link_to_edit</tt>. See that method
# for more details.
def it_should_not_link_to_edit(name)
it "should have a link/button to edit @#{name}" do
do_render
response.should_not have_link_or_button_to_edit(instance_for(name))
end
end
alias it_should_not_have_link_to_edit it_should_not_link_to_edit
alias it_should_not_have_button_to_edit it_should_not_link_to_edit
alias it_should_not_have_button_or_link_to_edit it_should_not_link_to_edit
# Creates an expectation which calls <tt>have_link_or_button_to_edit</tt>
# for each member of the instance variable matching the specified name
# on the response from rendering the template. See that method for more details.
#
# <b>Note:</b> This method takes a string or symbol representing the instance
# variable's name and not an instance variable, which would be nil
# in the scope of the example block.
def it_should_link_to_edit_each(name)
it "should have a link/button to edit each member of @#{name}" do
do_render
instance_for(name).each do |member|
response.should have_link_or_button_to_edit(member)
end
end
end
alias it_should_have_link_to_edit_each it_should_link_to_edit_each
alias it_should_have_button_to_edit_each it_should_link_to_edit_each
alias it_should_have_button_or_link_to_edit_each it_should_link_to_edit_each
# Creates an expectation which calls <tt>have_link_or_button_to_delete</tt> on the response
# from rendering the template. See that method for more details.
#
# <b>Note:</b> This method takes a string or symbol representing the instance
# variable's name to send to <tt>have_link_or_button_to_delete</tt>
# not an instance variable, which would be nil in the scope of the example block.
def it_should_link_to_delete(name)
it "should have a link/button to delete @#{name}" do
do_render
response.should have_button_to_delete(instance_for(name))
end
end
alias it_should_have_link_to_delete it_should_link_to_delete
alias it_should_have_button_to_delete it_should_link_to_delete
alias it_should_have_button_or_link_to_delete it_should_link_to_delete
# Negative version of <tt>it_should_link_to_delete</tt>. See that method
# for more details.
def it_should_not_link_to_delete(name)
it "should not have a link/button to delete @#{name}" do
do_render
response.should_not have_button_to_delete(instance_for(name))
end
end
alias it_should_not_have_link_to_delete it_should_not_link_to_delete
alias it_should_not_have_button_to_delete it_should_not_link_to_delete
alias it_should_not_have_button_or_link_to_delete it_should_not_link_to_delete
# Creates an expectation which calls <tt>have_link_or_button_to_delete</tt>
# for each member of the instance variable matching the specified name
# on the response from rendering the template. See that method for more details.
#
# <b>Note:</b> This method takes a string or symbol representing the instance
# variable's name and not an instance variable, which would be nil
# in the scope of the example block.
def it_should_link_to_delete_each(name)
it "should have a link/button to delete each member of @#{name}" do
do_render
instance_for(name).each do |member|
response.should have_button_to_delete(member)
end
end
end
alias it_should_have_link_to_delete_each it_should_link_to_delete_each
alias it_should_have_button_to_delete_each it_should_link_to_delete_each
alias it_should_have_button_or_link_to_delete_each it_should_link_to_delete_each
end
end
end

View file

@ -0,0 +1,26 @@
# Let's make sure everyone else is loaded
require File.expand_path(File.dirname(__FILE__) + "/../../../../config/environment")
require 'spec'
require 'spec/rails'
begin
require 'ruby2ruby'
rescue
puts "-----"
puts "Attention: skinny_spec requires ruby2ruby for nicer route descriptions"
puts "It is highly recommended that you install it: sudo gem install ruby2ruby"
puts "-----"
end
# Let's load our family now
require "lucky_sneaks/common_spec_helpers"
require "lucky_sneaks/controller_request_helpers"
require "lucky_sneaks/controller_spec_helpers"
require "lucky_sneaks/controller_stub_helpers"
require "lucky_sneaks/model_spec_helpers"
require "lucky_sneaks/view_spec_helpers"
# Let's all come together
Spec::Rails::Example::ViewExampleGroup.send :include, LuckySneaks::ViewSpecHelpers
Spec::Rails::Example::HelperExampleGroup.send :include, LuckySneaks::CommonSpecHelpers
Spec::Rails::Example::ControllerExampleGroup.send :include, LuckySneaks::ControllerSpecHelpers
Spec::Rails::Example::ModelExampleGroup.send :include, LuckySneaks::ModelSpecHelpers