Sujimichi / DeceptiCon

A way to simplify your basic rails controller tests

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

DeceptiCon

Dont’ Endure Coding Endless resPonse Tests In CONtrollers

DeceptiCon allows you to write basic response tests as a simple mapping between action/format and expected response.

  • Simplify your Rails controller specs by generating the simple response tests

  • Reduce the amount of repetitive test code needed.

  • Write Controller specs which your boss can read!

Still a work in progress. More changes, tests and docs to come soon.

Reduced Controller Test Code

require 'spec_helper'
describe NotesController do
  @test_formats = [:html, :ajax] #by default :xml and :json would also be included. :ajax == :js

  @action_mapping = { 
    :index => {:html => true,  :ajax => false},
    :show =>  {:html => true,  :ajax => false},
    :new =>   {:html => false, :ajax => true},
    :create =>{:html => false, :ajax => true},
    :edit =>  {:html => false, :ajax => true},
    :update =>{:html => true,  :ajax => true},
    :destroy=>{:html => false, :ajax => false},
  }

  assert_mapping #generate tests which describe the above expectations.

  it 'should leave me free to now test the important details'
end

This controller spec results in 14 tests; two tests for each action for each format (:html and :js).

Each test uses the appropriate request methods from rspec-rails( get, post, put, delete and xhr) and makes assertion on the response depending on the true/false value (also takes :redirect and will soon take status codes). The tests also make assumptions about the params to send in the requests, based on which action is being called (see later for details).

UseCase

Every one of your controllers is 100% resourceful, they either provide just the seven deadly actions (:index, :show, :new, :create, :edit, :update, :destroy) or a subset. All you controller actions are wrapped in respond_to blocks and the different formats (:html, :js, :xml and :json) will result in different outcomes.

Ideally you should test each of the seven actions on each controller and you should probably test each format against each action. So that is at least 28 tests per controller. In the end, especially as a lot of the controllers only fulfill a subset of the seven actions and primarily only respond to :html and :js requests, you only test the actions which are significant.

The aim of DeceptiCon is to enable a very quick, blanket set of tests which cover every action with every format and simply make assertions about the responses’ status code.

For example if you had a controller that only provides the index action to certain users but allows other users the show action too, then you can just write;

describe NotesController do
  describe "for logged OUT user" do 
    before(:each) { #logOut user }
    @action_mapping = { :index => {:html => true } } #only include the actions expected to be successful.
    assert_mapping 
  end   
  describe "for logged IN user" do 
    before(:each) { #logIn user }
    @action_mapping = { 
      :index => {:html => true },
      :show =>  {:html => true,  :ajax => true}
    }
    assert_mapping 
  end         
end

This would test that a logged out user is only get a successful response from the index action with an html request.

All other formats (:js, :xml, :json) should not be successful to index and none of the other actions should be successful with either format. It then tests that a logged in user can also access the show action with either html or js requests, but all other actions are still blocked.

This is NOT designed to replace other controller tests, merely to make it dead easy to test the blocked actions, freeing you to test the more interesting stuff. You should now include further tests to assert what is assigned etc, but you only need to focus on the required actions.

Assumptions about params

By default DeceptiCon will make tests to hit each of the seven actions (you can exclude actions - see options later) and it therefore needs to have appropriate data in the params.

For each of the actions it makes assumptions about what to include in the params of the request.

  • In the case of :index and :new no params are supplied in the request. ie; params = {}

  • For :create the params include attributes for the appropriate object. ie; params = {:note => {:text => nil}}

  • For :update the params include attributes for the object and an :id. ie; params = {:note => {:text => nil}, :id => 1}

  • All other actions (:show, :edit, :destroy) just get an :id. ie; params = {:id => 1}

You can pass in other data to go the params, see Extending the params later.

In order to populate the params with an id and attributes the tests need to be able to construct an appropriate valid object. The valid object can then be returned by the controller and in the case of the create action the class has :new stubbed to return the valid_object. See more in Setup about this.

example of generated tests

it "should respond to index:html"
it "should respond to new:ajax"
it "should NOT respond to new:html"
it "should redirect requests to edit:html"

Setup

First add the gem to your Gemfile (probably in the :test and :development groups);

group :test, :development do
  gem 'decepticon'
end

DeceptiCon is not available on Rubygems yet. You need to clone this repo and then build and install the gem locally like this;

git clone git@github.com:Sujimichi/DeceptiCon.git && cd DeceptiCon && rake build && rake install

Then include it in your spec_helper (or in a specific controller)

include DeceptiCon

In each of the controller_specs you want to use it in you must add an @action_mapping variable to define the expectations and add a call to assert_mapping See more about defining the @action_mapping in Usage

Finally, you must define a Factory or ‘valid_object’ method to return a valid object for each controller, ie a valid instance of Note for the NotesController.

Factories and valid_objects

Each controller will probably be expecting to do actions on the object class of its name, ie Note for NotesController. For all the actions, aside from :index and :new, DeceptiCon needs to be able to create a valid instance of that object so it can send its id and or attributes in the request params. If you have Factories set up it will use Factory.build(class_name).

It also allows you to have valid_object methods ie; valid_note or valid_user_comment to return an object. A valid_object method will be used in preference to a Factory if both are available. The class of the object is derived from the controller name, but can be over-ridden (see settings).

It is the expectation that an object returned by either a Factory or a valid_object method will return true for .valid? If it can’t find a Factory or valid_object method it will attempt to create an object and populate its attributes (with string or ints) to try to make it valid but this only work for objects with very simple validations.

Usage

In the controller_specs which you want DeceptiCon to generate tests for, you need to define an @action_mapping variable.

It should be defined inside a describe block, but not inside a before filter.

@action_mapping is a map of controller actions and request formats to the expectations for responses in the form of a hash.

Each action is represented by a key which entails another hash that defines formats and corresponding expectations for response.success? ie;

{:action => {:format_a => <expectation>, :format_b => <expectation>}}

The formats can be :html, :ajax, :xml and :json (:js and :ajax can be used synonymously) The expectations can be either true, false or :redirect. (I will extend it to take status codes soon)

After each definition of @action_mapping add a call to assert_mapping, ie;

describe "this" do 
  @action_mapping = { :index => {:html => true, :ajax => false} }
  assert_mapping
end
describe "that" do 
  @action_mapping = { :index => {:html => false, :ajax => true} }
  assert_mapping
end

Be Lazy

Only the actions and formats that have the expectation of a successful response need to be defined. However including them is more expressive to any reader. Any action/format which is not included will be considered to not have a successful response.

@action_mapping = { :index => {:html => true} }

Without other options (see later), using the above action_mapping would result in all seven actions being tested for their response to each of the four formats (html, js, xml, json) but only the html response to index is expected to be successful. By default all four formats (html, js, xml, json) are tested for all seven actions. You can adjust this for all controllers or just for the current. See more in Options later.

Options

By Default all seven actions will be tested for all four formats, you might not want this.

You can set which formats are tested for all controllers by adding a $default_test_formats array where you include DeceptiCon in the spec_helper. Note, this var needs to be set before it is included.

$default_test_formats = [:html, :ajax] #limit to just :html and :ajax formats
include DeceptiCon

You can also control which formats to test on a per-controller basis by including a @test_formats array alongside the @action_mapping.

@test_format = [:html, :xml]

This over-rides anything set by $default_test_formats

You can control which actions are tested on a per-controller basis. By default all seven actions are tested but you can include an array (@skip_actions) to have some actions ignored.

@skip_actions = [:edit, :update, :destroy] #these three will not be tested.

In the tests an object will be created. The class of this object is derived from the name of the controller being tested. ie; Note for NotesController. This can be over-ridden by adding @object alongside @action_mapping, ie;

@object = DifferentNote

Extending the params

You can also pass in additional params to be added to the request, ie;

@action_mapping = {
   :index => {:html => true,  :ajax => false, :html_args => {:limit => 5} },
   :show =>  {:html => true,  :ajax => false},
   :new =>   {:html => false, :ajax => true,  :ajax_args => {:published => false} },
   :create =>{:html => true,  :ajax => true,  :args => {:assign_to_user => "valid_user.id"}, :ajax_args => {:preview => true} },
   :edit =>  {:html => false, :ajax => true},
   :update =>{:html => true,  :ajax => true},
   :destroy=>{:html => false, :ajax => false},
 }

Additional params supplied with the key :args will be included in requests to all formats. To include params for specific formats use the key :<format>_args, ie :html_args.

Note the args being passed to the create action; :args => {:user_id => "valid_user.id"}. The valid_user.id is a string not a method or variable. Before additional params are added any which are strings are eval’d. This allows methods and variables to be called in the scope of the test (could not make work with Procs). Its not ideal.

Other Stuff

As well as the main assert_mapping method the DeceptiCon class also provides another method called fetch. This is used to unify the way you make requests to different formats.

For example to make html requests you write;

get :index
post :update, params

To make js requests you write;

xhr :get, :index
xhr :post, :update, params

To make either :xml or :json requests you add the line;

@request.env['HTTP_ACCEPT'] = "application/#{format}"

where format is either “xml” or “json”, and then use get, post, put or delete just like an html request.

So, thats a little vexatious when trying to use it programatically.

So now you can write

fetch :html, :index
fetch :ajax, :index
fetch :json, :index
fetch :html, :update, params
fetch :ajax, :update, params
fetch :json, :update, params

get, post etc can take four args

get(:show, {'id' => "12"}, {'user_id' => 5}, {'message' => 'booya!'})

fetch take five args and passes the last four on. Only the first two are required.

fetch(:html, :show, {'id' => "12"}, {'user_id' => 5}, {'message' => 'booya!'})

Example Controller Spec

A controller spec which tests all seven actions, both html and js formats for three different types of User.

require 'spec_helper'

describe HelpDocumentsController do

  describe "Accessibility by non-logged-in users" do
    @action_mapping = {  } #no mapping of actions => completely blocked, all actions are {:html => false, :ajax => false}
    assert_mapping
  end

  describe "Accessibility by logged-in users without roles" do
    before(:each) do
      assume_logged_in_user
    end
    @action_mapping = {  } #no mapping of actions => completely blocked, all actions are {:html => false, :ajax => false}
    assert_mapping
  end

  describe "Accessibility by logged-in users with admin role" do
    before(:each) do
      assume_logged_in_user
      @current_user.add_role :admin
    end

    @action_mapping = {
      :index => {:html => true,  :ajax => false},
      :show =>  {:html => false, :ajax => false},
      :new =>   {:html => false, :ajax => false},
      :create =>{:html => true,  :ajax => false},
      :edit =>  {:html => true,  :ajax => false},
      :update =>{:html => true,  :ajax => true},
      :destroy=>{:html => true,  :ajax => false}
    }
    assert_mapping
  end

end

About

A way to simplify your basic rails controller tests


Languages

Language:Ruby 100.0%