Let's assume, we have a web site requiring authentication and we have a User class that allows every user to log in, created with the gems Clearance and Suspenders such as described here, with a user name field added.
Now, let's have 2 types of users with different functionality besides logging in: Company and Customer. Then we can have 2 classes inheriting from User.
class User < ActiveRecord::Base
include Clearance::User
end
class Customer < User
end
class Company < User
end
In order to make the tables inherit, let's add a "type" column to the "users" table.
$ rails generate migration add_type_to_users type:string
The 2 types of users will be processed by the same controller UsersController and will be routed with the same paths.
users_controller_spec.rb
-----------------------------
require 'spec_helper'
describe UsersController, 'routes' do
it { should route(:get, 'users/1').to(:action => 'show', :id => 1) }
end
and the corresponding mapping in routes.rb:
resources :users, :controller => 'users', :only => [:show, :create]
To test the models, we need to create separate factories for different user types. We will use FactoryGirl and FactoryGirl inheritance.
spec/factories/user.rb
--------------------------
Factory.sequence :email do |n|
"user#{n}@example.com"
end
Factory.define :user do |user|
user.email { Factory.next :email }
user.name { "user" }
user.password { "password" }
user.password_confirmation { |instance| instance.password }
end
Factory.define :email_confirmed_user, :parent => :user do |user|
user.after_build { warn "[DEPRECATION] The :email_confirmed_user factory is deprecated, please use the :user factory instead." }
end
#add those 2 definitions
Factory.define :company, :class => Company, :parent => :email_confirmed_user do |user|
user.type { "Company" }
end
Factory.define :customer, :class => Customer, :parent => :email_confirmed_user do |user|
user.type { "Customer" }
end
After that, we would like to write some Cucumber features to test a user creating a profile of a certain type. The features were already created by Clearance, we only need to put checks for the extra attributes "name" and "user type".
Feature: Sign up
In order to get access to protected sections of the site
As a visitor
I want to sign up
Background:
When I go to the sign up page
Then I should see an email field
Scenario: Visitor signs up with invalid data
When I fill in "Email" with "invalidemail"
And I select "company" from "User type"
And I fill in "User name" with "user"
And I fill in "Password" with "password"
And I fill in "Confirm password" with ""
And I press "Sign up"
Then I should see error messages
Scenario: Visitor signs up with valid data
When I fill in "Email" with "email@person.com"
And I select "company" from "User type"
And I fill in "User name" with "user"
And I fill in "Password" with "password"
And I fill in "Confirm password" with "password"
And I press "Sign up"
Then I should be on the "company" profile page for "user"
And I should see "signed up"
Scenario: Visitor tries to sign up without a user name
When I go to the sign up page
And I fill in "Email" with "email@person.com"
And I select "company" from "User type"
And I fill in "Password" with "password"
And I fill in "Confirm password" with "password"
And I press "Sign up"
Then the "User name" field should have the "can't be blank" error
We make the first 2 scenarios pass by adding "type" and "user name" fields to the form, adding the "user name" to the model and the validation, creating a "show" view for "user", and defining the missing path. I leave all of it as an exercise. We will also need to override the create method for Clearance, since it doesn't handle User subtypes.
In UsersController add the following (it overrides the Clearance method)
#overriden from Clearance to take care of inheritance
def create
@user = params[:user][:type].constantize.new params[:user]
#used to be
#@user = ::User.new params[:user]
if @user.save
flash_notice_after_create
sign_in(@user)
redirect_to(url_after_create)
else
render :template => 'users/new'
end
end
However, the third scenario still will have trouble passing since for some reason Rails refers to the inherited model when returning to the same page, instead of User. To fix this, we need to override the model_name method in User, so it returns the superclass model name.
class User < ActiveRecord::Base
include Clearance::User
validates_presence_of :name
def self.inherited(child)
child.instance_eval do
def model_name
self.superclass.model_name
end
end
super
end
end
Now, all the Cucumber tests should pass.