lib/rspec/rails/mocks.rb
module RSpec module Rails class IllegalDataAccessException < StandardError; end module Mocks module InstanceMethods def valid? true end def as_new_record self.stub(:id) { nil } self end def new_record? !persisted? end def persisted? !!id end def destroy self.stub(:id) { nil } end end # Creates a mock object instance for a +string_or_model_class+ with # common methods stubbed out. Additional methods may be easily stubbed # (via add_stubs) if +stubs+ is passed. # # +model_class+ can be any of: # # * A String representing a Class that does not exist # * A String representing a Class that extends ActiveModel::Naming # * A Class that extends ActiveModel::Naming def mock_model(string_or_model_class, options_and_stubs = {}) if String === string_or_model_class if Object.const_defined?(string_or_model_class) model_class = Object.const_get(string_or_model_class) else model_class = Object.const_set(string_or_model_class, Class.new do extend ActiveModel::Naming end) end else model_class = string_or_model_class end unless model_class.kind_of? ActiveModel::Naming raise ArgumentError.new <<-EOM The mock_model method can only accept as its first argument: * A String representing a Class that does not exist * A String representing a Class that extends ActiveModel::Naming * A Class that extends ActiveModel::Naming It received #{model_class.inspect} EOM end id = options_and_stubs.has_key?(:id) ? options_and_stubs[:id] : next_id options_and_stubs = options_and_stubs.reverse_merge({ :id => id, :destroyed? => false, :marked_for_destruction? => false }) derived_name = "#{model_class.name}_#{id}" m = mock(derived_name, options_and_stubs) m.extend InstanceMethods m.extend ActiveModel::Conversion errors = ActiveModel::Errors.new(m) [:save, :update_attributes].each do |key| if options_and_stubs[key] == false errors.stub(:empty?) { false } end end m.stub(:errors) { errors } m.__send__(:__mock_proxy).instance_eval(<<-CODE, __FILE__, __LINE__) def @object.is_a?(other) #{model_class}.ancestors.include?(other) end def @object.kind_of?(other) #{model_class}.ancestors.include?(other) end def @object.instance_of?(other) other == #{model_class} end def @object.respond_to?(method_name) #{model_class}.respond_to?(:column_names) && #{model_class}.column_names.include?(method_name.to_s) || super end def @object.class #{model_class} end def @object.to_s "#{model_class.name}_#{id}" end CODE yield m if block_given? m end module ModelStubber def connection raise RSpec::Rails::IllegalDataAccessException.new("stubbed models are not allowed to access the database") end def new_record? __send__(self.class.primary_key).nil? end def as_new_record self.__send__("#{self.class.primary_key}=", nil) self end end # :call-seq: # stub_model(Model) # stub_model(Model).as_new_record # stub_model(Model, hash_of_stubs) # stub_model(Model, instance_variable_name, hash_of_stubs) # # Creates an instance of +Model+ that is prohibited from accessing the # database*. For each key in +hash_of_stubs+, if the model has a # matching attribute (determined by asking it) are simply assigned the # submitted values. If the model does not have a matching attribute, the # key/value pair is assigned as a stub return value using RSpec's # mocking/stubbing framework. # # <tt>new_record?</tt> is overridden to return the result of id.nil? # This means that by default new_record? will return false. If you want # the object to behave as a new record, sending it +as_new_record+ will # set the id to nil. You can also explicitly set :id => nil, in which # case new_record? will return true, but using +as_new_record+ makes the # example a bit more descriptive. # # While you can use stub_model in any example (model, view, controller, # helper), it is especially useful in view examples, which are # inherently more state-based than interaction-based. # # == Database Independence # # +stub_model+ does not make your examples entirely # database-independent. It does not stop the model class itself from # loading up its columns from the database. It just prevents data access # from the object itself. To completely decouple from the database, take # a look at libraries like unit_record or NullDB. # # == Examples # # stub_model(Person) # stub_model(Person).as_new_record # stub_model(Person, :id => 37) # stub_model(Person) do |person| # person.first_name = "David" # end def stub_model(model_class, stubs={}) primary_key = model_class.primary_key.to_sym stubs = {primary_key => next_id}.merge(stubs) model_class.new.tap do |m| m.__send__("#{primary_key}=", stubs.delete(primary_key)) m.extend ModelStubber m.stub(stubs) yield m if block_given? end end private @@model_id = 1000 def next_id @@model_id += 1 end end end end RSpec.configure do |c| c.include RSpec::Rails::Mocks end