Skip to content

nickhoffman/modelspeccer

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

15 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Description

ModelSpeccer is a module for generating RSpec specifications on-the-fly for Rails model attributes. It helps keep your specs DRYer and more focussed on speccing valid and invalid values. See the examples below for more information, or read the code comments.

Dependencies

ModelSpeccer requires that the following gems or Rails plugins are installed:

A factory must be defined for each model that ModelSpeccer is going to be used on. Factories are defined in spec/factories.rb .

Usage

Quick

If you just want to start using ModelSpeccer, read the comments above each of the methods in the module’s source file. Each method is well-documented.

The ModelSpeccer module has three methods:

* describe_model_factory(model)
* decribe_model_attribute(model, attribute, valid_vales = [], invalid_values = [])
* check_model_attributes_when_nil(model, required_attributes, attributes_with_error_messages = [])

Long, Complete Example

We’re going to step through the process of making a very basic Rails app, and use ModelSpeccer to generate specs for a model.

Create a new Rails application:

$ rails model_speccer_test
$ cd model_speccer_test

Install the necessary plugins:

$ script/plugin install git://github.com/dchelimsky/rspec.git
$ script/plugin install git://github.com/dchelimsky/rspec-rails.git
$ script/plugin install git://github.com/dfl/factories-and-workers.git
$ script/plugin install git://github.com/nickhoffman/modelspeccer.git

Run the RSpec generator:

$ script/generate rspec

Correct a bug in factories-and-workers. As of 2008-10-09, running “rake spec” will fail because vendor/plugins/factories-and-workers/rails/init.rb tries to access a non-existant “config” variable. One solution is to replace the following line in factories-and-workers/init.rb :

require File.dirname(__FILE__)+'/rails/init.rb' if RAILS_ENV=='test'

with this:

if RAILS_ENV == 'test'
end

and put the contents of factories-and-workers/rails/init.rb into the above if-statement. When completed, factories-and-workers/rails/init.rb should contain:

#require 'fileutils'
#
#config.after_initialize do
#  %w(spec/factories.rb spec/factory_workers.rb test/factories.rb test/factory_workers.rb).each do |file|
#    path = File.join(Dir.pwd, file)
#    require path if File.exists?(path)
#  end
#end

and factories-and-workers/init.rb should contain:

require 'factories-and-workers'

if RAILS_ENV == 'test'
  require 'fileutils'

  config.after_initialize do
    %w(spec/factories.rb spec/factory_workers.rb test/factories.rb test/factory_workers.rb).each do |file|
      path = File.join(Dir.pwd, file)
      require path if File.exists?(path)
    end 
  end 
end

Next, we need to configure RSpec to load the ModelSpeccer module for model specs. This is done by adding the following to the “Spec::Runner.configure” block in spec/spec_helper.rb :

config.include ModelSpeccer, :type => :model

Alright, now we’re really ready to begin. Create the User model:

$ script/generate rspec_model Person name:string age:integer
$ rake db:migrate

Confirm that the auto-generated specs are successful:

$ rake spec

We want to create a Person factory, so let’s spec that out. Replace the contents of spec/models/person_spec.rb with:

require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')

ModelSpeccer.describe_model_factory Person

The code above tells ModelSpeccer to generate specs that check that factory methods exist for the Person class.

Now run the Person model spec, and watch it fail:

$ script/spec --format specdoc spec/models/person_spec.rb

Two errors are generated:

undefined method `build_person' for Object:Class
undefined method `create_person' for Object:Class

Creating a Person factory will provide the Object class with those missing methods. The file spec/factories.rb should not exist yet. Create and add the following to it:

include FactoriesAndWorkers::Factory

factory :person, {
  :name   => 'Factory Person',
  :age    => nil,
  }

Run the Person model spec again, and watch it pass:

$ script/spec --format specdoc spec/models/person_spec.rb

Person factory
- should build a valid Person
- should create a valid Person

Finished in 0.433015 seconds

2 examples, 0 failures

Great! We’re on the right track. Next, we want to add some validations to the Person model. The ‘name’ attribute should be required, and between 4 and 128 characters long. First, we spec it out. Add the following to spec/models/person_spec.rb , just above the call to ModelSpeccer#describe_model_factory :

# Test data {{{
required_person_attributes = %w(name)

valid_names = [
  'if it is 4 characters',      'Abcd',
  'if it is 128 characters',    'Maximum lengthxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
  ]
invalid_names = [
  'if it is nil',               nil,
  "if it is ''",                '',
  ]
# End test data }}}

The first “column” in the arrays above defines the RSpec behaviour that we expect. Essentially, what you would usually pass to #it when writing specs. The second “column” defines valid and invalid values for the ‘name’ attribute of the Person class.

So, how do we use these valid and invalid values with the Person model? All you need to do is add this to the end of spec/models/person_spec.rb :

ModelSpeccer.describe_model_attribute Person, :name, valid_names, invalid_names

Let’s run the Person model spec again, and watch them fail:

$ script/spec --format specdoc spec/models/person_spec.rb

As expected, that generated two errors; one for each pair of elements in the ‘invalid_names’ array. Read the spec doc that was spat out to see the exact specs that ModelSpeccer is generating for you.

Obviously, we want our specs to pass. So let’s validate the Person model’s ‘name’ attribute. Add this to app/models/person.rb :

validates_length_of :name,
  :in           => 4..128,
  :message      => "Please provide a name that's between 4 and 128 characters long.",
  :allow_blank  => false

Run the Person model spec again, and everything passes with flying colours:

$ script/spec --format specdoc spec/models/person_spec.rb

Person factory
<..snip..>

Person attribute 'name' should be valid
- if it is 4 characters
- if it is 128 characters

Person attribute 'name' should be invalid
- if it is nil
- if it is ''

Finished in 0.43423 seconds

6 examples, 0 failures

The specs we currently have for the Person model ‘name’ attribute are pretty bare. Let’s add some more. We want to restrict which characters are allowed in a name. In spec/models/person_spec.rb , replace the ‘valid_names’ and ‘invalid_names’ arrays with:

valid_names = [
  'if it is 4 characters',            'Abcd',
  'if it is 5 characters',            'Abcde',
  'if it is 127 characters',          'Almost at maximum lengthxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
  'if it is 128 characters',          'Maximum lengthxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
  'if it starts with a letter',       'Some Name',
  "if it has a '-' in the middle",    'With A-Dash',
  "if it has a ' in the middle",      "Some N'ame",
  ]
invalid_names = [
  'if it is nil',                 nil,
  "if it is ''",                  '',
  'if it is only 1 character',    'x',
  'if it is only 2 characters',   'xx',
  'if it is only 3 characters',   'xxx',
  'if it is 129 characters',      'Too longxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
  "if it starts with a '!'",      "!Not allowed",
  "if it starts with a '-'",      "-Not allowed",
  "if it starts with a '*'",      "*Not allowed",
  'if it has a newline',          "Newlines\nnot allowed",
  ]

Run the specs again, and watch them fail:

$ script/spec --format specdoc spec/models/person_spec.rb

Now add the validation to the Person model in app/models/person.rb :

validates_format_of :name,
  :with       => /\A['a-z][-' a-z]+\Z/i,
  :message    => "Please provide a name. Letters, spaces, dashes and apostrophes are allowed."

Run the Person model specs, and they’ll be happier than Oprah on a baked ham:

$ script/spec --format specdoc spec/models/person_spec.rb

Person factory
<..snip..>

Person attribute 'name' should be valid
- if it is 4 characters
- if it is 5 characters
- if it is 127 characters
- if it is 128 characters
- if it starts with a letter
- if it has a '-' in the middle
- if it has a ' in the middle

Person attribute 'name' should be invalid
- if it is nil
- if it is ''
- if it is only 1 character
- if it is only 2 characters
- if it is only 3 characters
- if it is 129 characters
- if it starts with a '!'
- if it starts with a '-'
- if it starts with a '*'
- if it has a newline

Finished in 0.539288 seconds

19 examples, 0 failures

The Person model’s ‘name’ attribute is pretty good now. So let’s do the ‘age’ attribute next. It’s going to be an optional attribute, with valid values between 0 and 120 inclusive. Add the valid and invalid values for the ‘age’ attribute to the test data section of spec/models/person_spec.rb :

valid_ages = [
  'if it is 0',           0,
  'if it is 1',           1,
  'if it is 119',         119,
  'if it is 120',         120,
  ]
invalid_ages = [
  'if it is -2',          -2
  'if it is -1',          -1,
  'if it is 121',         121,
  'if it is 122',         122,
  ]

Now let’s tell ModelSpeccer to generate some specs for the ‘age’ attribute. Add this to the end of spec/models/person_spec.rb :

ModelSpeccer.describe_model_attribute Person, :age, valid_ages, invalid_ages

And what do we do next? Run the specs and watch them fail!

$ script/spec --format specdoc spec/models/person_spec.rb

To make them succeed, let’s stuff another validation into the Person model, in app/models/person.rb :

validates_inclusion_of :age,
  :in           => 0..120,
  :allow_blank  => true,
  :message      => 'Please provide a valid age, from 0 to 120.'

With the validation in place, let’s check our specs:

$ script/spec --format specdoc spec/models/person_spec.rb

Person factory
<..snip..>

Person attribute 'name' should be valid
<..snip..>

Person attribute 'name' should be invalid
<..snip..>

Person attribute 'age' should be valid
- if it is 0
- if it is 1
- if it is 119
- if it is 120

Person attribute 'age' should be invalid
- if it is -2
- if it is -1
- if it is 121
- if it is 122

Finished in 0.666177 seconds

27 examples, 0 failures

Huzzah!

Now, there’s one more method within the ModelSpeccer module that we haven’t used yet. ModelSpeccer#check_model_attributes_when_nil is used for iterating through each attribute in a model to ensure that an instance of a model:

  • is valid when an optional attribute is nil,

  • is invalid when a required attribute is nil,

  • has an error message on certain attributes when those attributes are nil

The first two points listed above are covered if you remember to put nil values in your ‘invalid_*’ test data arrays. However, I still use #check_model_attributes_when_nil , as it makes me feel good to know that each model attribute is being tested for nil values.

So let’s get on with this. It’s easier than you think. Just add this to the end of your Person model spec, in spec/models/person_spec.rb :

ModelSpeccer.check_model_attributes_when_nil Person, required_person_attributes, required_person_attributes

Give the ol’ specs a kick, and watch the magic:

$ script/spec --format specdoc spec/models/person_spec.rb

Person factory
<..snip..>

Person attribute 'name' should be valid
<..snip..>

Person attribute 'name' should be invalid
<..snip..>

Person attribute 'age' should be valid
<..snip..>

Person attribute 'age' should be invalid
<..snip..>

Person with 'name' set to nil
- should be invalid
- should have an error message for 'name'

Person with 'age' set to nil
- should be valid

Finished in 0.568431 seconds

30 examples, 0 failures

And voila, we’re done with this tutorial!

Credits

Written by Nick Hoffman (nick@deadorange.com).

About

Keep your RSpec specs DRYer by generating specs for your model attributes on-the-fly.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages