翻翻 shoulda 的源代码,其实它只是个空架子,本身并没有料,如下所示:
require 'shoulda/version'
require 'shoulda-matchers'
require 'shoulda-context'
if defined?(RSpec)
require 'shoulda/matchers/integrations/rspec'
else
require 'shoulda/matchers/integrations/test_unit'
end
可知,它主要是为了 RSpec 或者 Test::Unit 服务。
action_controller
action_mailer
active_model
active_record
它细分为这四者 ‘服务’, 胃口还挺大的。一个个来看看…
算了,我现在对 active_model 和 active_record 比较感兴趣,还是先看看它们吧。
allow_mass_assignment_of_matcher.rb
allow_value_matcher.rb
ensure_exclusion_of_matcher.rb
ensure_inclusion_of_matcher.rb
ensure_length_of_matcher.rb
validate_acceptance_of_matcher.rb
validate_format_of_matcher.rb
validate_numericality_of_matcher.rb
validate_presence_of_matcher.rb
validate_uniqueness_of_matcher.rb
可见,它为我们提供了 3 类,共 10 个方法,基本上 满足了 我们对 validates 的所有要求。确实很方便…(注意是 validate, 不是 validates)
association_matcher.rb
have_db_column_matcher.rb
have_db_index_matcher.rb
have_readonly_attribute_matcher.rb
比较 吸引 眼球的是 association 还有 readonly (一个常用,而一个则为…)
值得一说的是,在 association 中,它用的是 have_xxx, 和 belong_to 前面的单词都是 ‘单数’, 使用的时候不要搞错了。
assert_same_elements(a1, a2, msg = nil)
assert_contains(collection, x, extra_msg = "")
assert_does_not_contain(collection, x, extra_msg = "")
assert_accepts(matcher, target, options = {})
assert_rejects(matcher, target, options = {})
module Shoulda
module Context
class << self
attr_accessor :contexts
def contexts # :nodoc:
@contexts ||= []
end
def current_context # :nodoc:
self.contexts.last
end
def add_context(context) # :nodoc:
self.contexts.push(context)
end
def remove_context # :nodoc:
self.contexts.pop
end
end
module ClassMethods
# == Should statements
#
# Should statements are just syntactic sugar over normal Test::Unit test
# methods. A should block contains all the normal code and assertions
# you're used to seeing, with the added benefit that they can be wrapped
# inside context blocks (see below).
#
# === Example:
#
# class UserTest < Test::Unit::TestCase
#
# def setup
# @user = User.new("John", "Doe")
# end
#
# should "return its full name"
# assert_equal 'John Doe', @user.full_name
# end
#
# end
#
# ...will produce the following test:
# * <tt>"test: User should return its full name. "</tt>
#
# Note: The part before <tt>should</tt> in the test name is gleamed from the name of the Test::Unit class.
#
# Should statements can also take a Proc as a <tt>:before </tt>option. This proc runs after any
# parent context's setups but before the current context's setup.
#
# === Example:
#
# context "Some context" do
# setup { puts("I run after the :before proc") }
#
# should "run a :before proc", :before => lambda { puts("I run before the setup") } do
# assert true
# end
# end
#
# Should statements can also wrap matchers, making virtually any matcher
# usable in a macro style. The matcher's description is used to generate a
# test name and failure message, and the test will pass if the matcher
# matches the subject.
#
# === Example:
#
# should validate_presence_of(:first_name).with_message(/gotta be there/)
#
def should(name_or_matcher, options = {}, &blk)
if Shoulda::Context.current_context
Shoulda::Context.current_context.should(name_or_matcher, options, &blk)
else
context_name = self.name.gsub(/Test/, "") if self.name
context = Shoulda::Context::Context.new(context_name, self) do
should(name_or_matcher, options, &blk)
end
context.build
end
end
# Allows negative tests using matchers. The matcher's description is used
# to generate a test name and negative failure message, and the test will
# pass unless the matcher matches the subject.
#
# === Example:
#
# should_not set_the_flash
def should_not(matcher)
if Shoulda::Context.current_context
Shoulda::Context.current_context.should_not(matcher)
else
context_name = self.name.gsub(/Test/, "") if self.name
context = Shoulda::Context::Context.new(context_name, self) do
should_not(matcher)
end
context.build
end
end
# == Before statements
#
# Before statements are should statements that run before the current
# context's setup. These are especially useful when setting expectations.
#
# === Example:
#
# class UserControllerTest < Test::Unit::TestCase
# context "the index action" do
# setup do
# @users = [Factory(:user)]
# User.stubs(:find).returns(@users)
# end
#
# context "on GET" do
# setup { get :index }
#
# should respond_with(:success)
#
# # runs before "get :index"
# before_should "find all users" do
# User.expects(:find).with(:all).returns(@users)
# end
# end
# end
# end
def before_should(name, &blk)
should(name, :before => blk) { assert true }
end
# Just like should, but never runs, and instead prints an 'X' in the Test::Unit output.
def should_eventually(name, options = {}, &blk)
context_name = self.name.gsub(/Test/, "")
context = Shoulda::Context::Context.new(context_name, self) do
should_eventually(name, &blk)
end
context.build
end
# == Contexts
#
# A context block groups should statements under a common set of setup/teardown methods.
# Context blocks can be arbitrarily nested, and can do wonders for improving the maintainability
# and readability of your test code.
#
# A context block can contain setup, should, should_eventually, and teardown blocks.
#
# class UserTest < Test::Unit::TestCase
# context "A User instance" do
# setup do
# @user = User.find(:first)
# end
#
# should "return its full name"
# assert_equal 'John Doe', @user.full_name
# end
# end
# end
#
# This code will produce the method <tt>"test: A User instance should return its full name. "</tt>.
#
# Contexts may be nested. Nested contexts run their setup blocks from out to in before each
# should statement. They then run their teardown blocks from in to out after each should statement.
#
# class UserTest < Test::Unit::TestCase
# context "A User instance" do
# setup do
# @user = User.find(:first)
# end
#
# should "return its full name"
# assert_equal 'John Doe', @user.full_name
# end
#
# context "with a profile" do
# setup do
# @user.profile = Profile.find(:first)
# end
#
# should "return true when sent :has_profile?"
# assert @user.has_profile?
# end
# end
# end
# end
#
# This code will produce the following methods
# * <tt>"test: A User instance should return its full name. "</tt>
# * <tt>"test: A User instance with a profile should return true when sent :has_profile?. "</tt>
#
# <b>Just like should statements, a context block can exist next to normal <tt>def test_the_old_way; end</tt>
# tests</b>. This means you do not have to fully commit to the context/should syntax in a test file.
def context(name, &blk)
if Shoulda::Context.current_context
Shoulda::Context.current_context.context(name, &blk)
else
context = Shoulda::Context::Context.new(name, self, &blk)
context.build
end
end
# Returns the class being tested, as determined by the test class name.
#
# class UserTest; described_type; end
# # => User
def described_type
@described_type ||= self.name.
gsub(/Test$/, '').
split('::').
inject(Object) { |parent, local_name| parent.const_get(local_name) }
end
# Sets the return value of the subject instance method:
#
# class UserTest < Test::Unit::TestCase
# subject { User.first }
#
# # uses the existing user
# should validate_uniqueness_of(:email)
# end
def subject(&block)
@subject_block = block
end
def subject_block # :nodoc:
@subject_block
end
end
module InstanceMethods
# Returns an instance of the class under test.
#
# class UserTest
# should "be a user" do
# assert_kind_of User, subject # passes
# end
# end
#
# The subject can be explicitly set using the subject class method:
#
# class UserTest
# subject { User.first }
# should "be an existing user" do
# assert !subject.new_record? # uses the first user
# end
# end
#
# The subject is used by all macros that require an instance of the class
# being tested.
def subject
@shoulda_subject ||= construct_subject
end
def subject_block # :nodoc:
(@shoulda_context && @shoulda_context.subject_block) || self.class.subject_block
end
def get_instance_of(object_or_klass) # :nodoc:
if object_or_klass.is_a?(Class)
object_or_klass.new
else
object_or_klass
end
end
def instance_variable_name_for(klass) # :nodoc:
klass.to_s.split('::').last.underscore
end
private
def construct_subject
if subject_block
instance_eval(&subject_block)
else
get_instance_of(self.class.described_type)
end
end
end
class Context # :nodoc:
attr_accessor :name # my name
attr_accessor :parent # may be another context, or the original test::unit class.
attr_accessor :subcontexts # array of contexts nested under myself
attr_accessor :setup_blocks # blocks given via setup methods
attr_accessor :teardown_blocks # blocks given via teardown methods
attr_accessor :shoulds # array of hashes representing the should statements
attr_accessor :should_eventuallys # array of hashes representing the should eventually statements
attr_accessor :subject_block
def initialize(name, parent, &blk)
Shoulda::Context.add_context(self)
self.name = name
self.parent = parent
self.setup_blocks = []
self.teardown_blocks = []
self.shoulds = []
self.should_eventuallys = []
self.subcontexts = []
if block_given?
merge_block(&blk)
else
merge_block { warn " * WARNING: Block missing for context '#{full_name}'" }
end
Shoulda::Context.remove_context
end
def merge_block(&blk)
blk.bind(self).call
end
def context(name, &blk)
self.subcontexts << Context.new(name, self, &blk)
end
def setup(&blk)
self.setup_blocks << blk
end
def teardown(&blk)
self.teardown_blocks << blk
end
def should(name_or_matcher, options = {}, &blk)
if name_or_matcher.respond_to?(:description) && name_or_matcher.respond_to?(:matches?)
name = name_or_matcher.description
blk = lambda { assert_accepts name_or_matcher, subject }
else
name = name_or_matcher
end
if blk
self.shoulds << { :name => name, :before => options[:before], :block => blk }
else
self.should_eventuallys << { :name => name }
end
end
def should_not(matcher)
name = matcher.description
blk = lambda { assert_rejects matcher, subject }
self.shoulds << { :name => "not #{name}", :block => blk }
end
def should_eventually(name, &blk)
self.should_eventuallys << { :name => name, :block => blk }
end
def subject(&block)
self.subject_block = block
end
def subject_block
return @subject_block if @subject_block
parent.subject_block
end
def full_name
parent_name = parent.full_name if am_subcontext?
return [parent_name, name].join(" ").strip
end
def am_subcontext?
parent.is_a?(self.class) # my parent is the same class as myself.
end
def test_unit_class
am_subcontext? ? parent.test_unit_class : parent
end
def test_methods
@test_methods ||= Hash.new { |h,k|
h[k] = Hash[k.instance_methods.map { |n| [n, true] }]
}
end
def create_test_from_should_hash(should)
test_name = ["test:", full_name, "should", "#{should[:name]}. "].flatten.join(' ').to_sym
if test_methods[test_unit_class][test_name.to_s] then
warn " * WARNING: '#{test_name}' is already defined"
end
test_methods[test_unit_class][test_name.to_s] = true
context = self
test_unit_class.send(:define_method, test_name) do
@shoulda_context = context
begin
context.run_parent_setup_blocks(self)
should[:before].bind(self).call if should[:before]
context.run_current_setup_blocks(self)
should[:block].bind(self).call
ensure
context.run_all_teardown_blocks(self)
end
end
end
def run_all_setup_blocks(binding)
run_parent_setup_blocks(binding)
run_current_setup_blocks(binding)
end
def run_parent_setup_blocks(binding)
self.parent.run_all_setup_blocks(binding) if am_subcontext?
end
def run_current_setup_blocks(binding)
setup_blocks.each do |setup_block|
setup_block.bind(binding).call
end
end
def run_all_teardown_blocks(binding)
teardown_blocks.reverse.each do |teardown_block|
teardown_block.bind(binding).call
end
self.parent.run_all_teardown_blocks(binding) if am_subcontext?
end
def print_should_eventuallys
should_eventuallys.each do |should|
test_name = [full_name, "should", "#{should[:name]}. "].flatten.join(' ')
puts " * DEFERRED: " + test_name
end
end
def build
shoulds.each do |should|
create_test_from_should_hash(should)
end
subcontexts.each { |context| context.build }
print_should_eventuallys
end
def method_missing(method, *args, &blk)
test_unit_class.send(method, *args, &blk)
end
end
end
end