BDD Model Validation using RSpec and extensions to Hash

I avoid writing fixtures for testing. They are cumbersome to write and management (particularly after the fact) is a hassle. When testing a model’s validations I want to work with a generally static model that only changes one or two attributes which illustrate the test condition I am working on. To this end I followed Luke Redpath’s blog post on BDD and models in particular that uses a more behavior driven approach to testing a model and it’s attributes.

As such in my rails projects now I like to use this Hash extension when defining the validation rules for my model.

module HashExtension
  def except(*keys)
    self.reject { |k,v|
      keys.include? k.to_sym
    }
  end
end

And then I can define a model’s allowed behavior using RSpec. For example:

describe "a test model" do
  def valid_model_attr
     { :name => "name", :age => 21}.extend(HashExtension)
  end 

   it "should be valid" do
      Model.new(valid_model_attr).should be_valid
   end

   it "should be invalid without name" do
       model  = Model.new(valid_model_attr.except(:name))
       mode.should_not be_valid
       model.errors.on(:name).should_not be_nil
   end

  it "should be invalid if age is under 21" do
       model  = Model.new(valid_model_attr.except(:age))
       model.age = 20
       mode.should_not be_valid
       model.errors.on(:age).should_not be_nil
  end
end

I can add as many tests as I want without defining several test specific fixtures which need to be updated when the validation rules for a model change. Using an attribute hash and except I can update one hash.

This technique gets a little more complicated when testing a model with a has_many / belongs_to relationship but I recently had success with that on this site I developed (www.clowndown.net). In that case I defined a test hash a such:

  before(:each) do
    @team  = Team.new
  end

  def valid_team
    {
      :name => "team name",
      :contact_number => "call me here!",
      :people => [valid_person, valid_person],
      :knowledge_of_chicago => "pretty good",
      :why_will_win => "might get lucky",
      :relationship => "great"
    }.extend(HashExtension)
  end

  def valid_person
    person = Person.new
    person.attributes = {
      :email => "email@email.com",
      :name => "My Great Name",
      :shirt_size => "L",
      :hometown => "Chicago",
      :team => @team,
      :occupation => "occupation"
    }
    person
  end

And I was able to write specs like this that used an object relation that focused on testing of the behavior of the has_many / belongs_to relation.

  it "is invalid with 1 person" do
    @team.attributes = valid_team.except(:people)
    @team.people = [valid_person]
    @team.should_not be_valid
    @team.errors.on(:people).should_not be_nil
  end

7 Responses to “BDD Model Validation using RSpec and extensions to Hash”

  1. Kerry Buckley Says:

    I started writing a helper to remove some of the repetition from testing validations this way. It’s described here if you want to borrow any of the code.

  2. Priit Tamboom Says:

    I like the hash way but where do you hold data for development then? Right now I’m holding everything in fixtures and both development and test will get populated from the same source.

    Does the hash way start duplicating fixtures too much? or I miss something?

  3. peter Says:

    Using this HashExtension I do not use fixtures at all. The purpose of this is to specify the behavior of the model and that is it. In terms of data for the development database that would have to come from a separate source. Generally I will only use data in development on an as needed basis and do not manage development data.

    Still even if you were to maintain fixtures for a common development data they need not interfer with using this technique to validate the model’s correct behavior. The fixtures written for model validation will be invalid and wouldn’t be used for a controller specification.

    That said when writing controller specs I hardly ever use the actual model to test and keep them as lean as possible. I avoid loading fixtures in the database (due to the performance drag plus after the fact adjustments) whenever possible.

  4. John Barton Says:

    Thanks for that snippet. I was just seeing a bunch of duplication in my specs and was about to write exactly that. lucky i googled first :)
    My preference with the hash extension is just to open up hash and add the methods there.
    I also include an only method:
    def only(*keys)
    self.dup.reject do |k,v|
    !keys.include? k.to_sym
    end
    end

  5. Chris Pratt Says:

    I implemented this in my specs as well along with the ‘only’ method John Barton mentioned above.

    However, I used an instance class in spec_helper.rb instead of going the module/extend approach, ie:

    class Hash

    def except(*keys)
    self.reject do |k,v|
    keys.include? k.to_sym
    end
    end

    def only(*keys)
    self.dup.reject do |k,v|
    !keys.include? k.to_sym
    end
    end

    end

    This way, it’s no longer necessary to extend every hash you want to use these functions on, the functionality is just there.

    I’m not criticizing your approach, but I am curious if there was a particular reason you decided to go the route you did with it?

  6. peter Says:

    I think the ‘only’ method has its uses and I initially had it defined but when I posted I realized I wasn’t using it in practice. I found starting from a golden object that I stripped as needed to conform to the validation I was testing. I could see the only method being used in other cases.

    As to extending the hash instance instead of extending the Hash class within the spec_helper or somewhere with similar effect was I didn’t want to extend all my Hash instances in non test code. I wasn’t planning on adding except/only to the application and extending the Hash class would make the functionality present and potentially used. So I guess I was protecting myself from Ruby’s extensibility by limiting the extension to explicit declarations.

    That said on a subsequent rails project I ended up adopting ‘except’ and ‘only’ for the entire application as I wanted to use the extension and ended up extending Hash in the environment.rb.

    This is starting to lead me to Ruby Facets which provides numerous useful core extensions.

  7. trauro Says:

    Вот решил вам немного помочь и послал этот пост в социальные закладки. Очень надеюсь ваш рейтинг возрастет.

Leave a Reply