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.
ModelSpeccer requires that the following gems or Rails plugins are installed:
-
rspec-rails (github.com/dchelimsky/rspec-rails)
-
factories-and-workers (github.com/dfl/factories-and-workers)
A factory must be defined for each model that ModelSpeccer is going to be used on. Factories are defined in spec/factories.rb .
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 = [])
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!
Written by Nick Hoffman ([email protected]).