Wednesday, July 6, 2011

Single table inheritance and testing with Cucumber and Factory Girl

To get familiar with Single Table Inheritance, please, refer to this article

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.