CSC/ECE 517 Fall 2012/ch2a 2w32 mk

From PG_Wiki
Jump to: navigation, search

SaaS - 5.4 - More Controller Specs and Refactoring

Contents

Introduction

This ia a textbook section that covers the online lectures on Controller Specs and Refactoring. The main focus is to write expectations that drive development of the controller method. While writing the tests for a controller method, it is discovered that it must collaborate with its model method. Instead of coding a model method, a stub model could be coded that acts as the code we wish we had (CWWWH). The main idea is to isolate the code of the controller method from the model method. It is an important idea useful in software design but more specifically useful in software testing.

Key Idea - to break dependency between the method under test and its collaborators. This is what seams are designed to do.

The Code You Wish You Had

Example

TMDb : The Movie Database rails application

New Feature : Search TMDb for movies

When the controller method receives the search form:

1. As explained in the previous textbook section , the controller method should call a method that will search TMDb for a specified movie.

2. If a match is found, the controller method should select "Search Results" view to display the match. This involves two specs - the controller should first decide to render Search Results, this is particularly important when different views can be rendered depending on outcome. The controller should also make the list of matches available to the rendered view.

Should and Should Not

In order to accomplish both of these specs, an expectation construct should is used. should is a method in a module that is mixed into the Object class. In Ruby, all the classes inherit from object class. Hence, when running RSpec, all the objects are capable of responding to the should method.

obj.should match-condition

RSpec defines some built-in matchers that can be used as the match-condition. We can also define some methods of our own.

count.should == 5                     (Syntactic sugar for count.should.==(5))

5.should(be.<(7))                     (be creates a lambda that tests the predicate expression)

5.should be < 7                       (Syntactic sugar allowed)

5.should be_odd                       (use method_missing to call odd? on 5)

result.should include(elt)            (Calls Enumerable#include?)

result.should match(/regex/)

In all the above cases, should_not can also be used in place of should.

Check for Rendering

There is an RSpec construct render_template that checks whether a controller method would render a template with a particular name.

result.should render_template('search_tmdb')

The RSpec method post simulates posting a form so that the controller method gets called. Once the post is done, there is another RSpec method called response() that returns the controller's response object. The render_template matcher can use the response object to check what view the controller would have tried to render.

    require 'spec_helper'
     
    describe MoviesController do
      describe 'searching TMDb' do
        it 'should call the model method that performs TMDb search' do
          Movie.should_receive(:find_in_tmdb).with('hardware')
          post :search_tmdb, {:search_terms => 'hardware'}
        end
        it 'should select the Search Results template for rendering' do
          Movie.stub(:find_in_tmdb)
          post :search_tmdb, {:search_terms => 'hardware'}
          response.should render_template('search_tmdb')
        end
      end
    end

Post and render_template are the extensions in Rails that have been added specifically by RSpec to test rails code.

Controller specs are like functional tests. They test more than one thing, not just call the controller method in isolation. They do the same thing a real browser does. The controller method does a post and the url is going to touch the routing subsystem, the dispatcher is going to call the controller method and when the controller method tries to call the view, the view should exist. Post will try to do the whole MVC flow, including rendering the view.

Make search results available to template

When you setup instance variables in the controller, those are available in the view for access. There is another RSpec-rails addition assign(), which when passed a symbol that stands for a controller instance variable, it returns the value of the instance variable that the controller has assigned to it. If the controller has never assigned a value to it, it would return Nil.

    require 'spec_helper'
     
    describe MoviesController do
      describe 'searching TMDb' do
        before :each do
          @fake_results = [mock('movie1'), mock('movie2')]
        end
        it 'should call the model method that performs TMDb search' do
          Movie.should_receive(:find_in_tmdb).with('hardware').
            and_return(@fake_results)
          post :search_tmdb, {:search_terms => 'hardware'}
        end
        it 'should select the Search Results template for rendering' do
          Movie.stub(:find_in_tmdb).and_return(@fake_results)
          post :search_tmdb, {:search_terms => 'hardware'}
          response.should render_template('search_tmdb')
        end
        it 'should make the TMDb search results available to that template' do
          Movie.stub(:find_in_tmdb).and_return(@fake_results)
          post :search_tmdb, {:search_terms => 'hardware'}
          assigns(:movies).should == @fake_results
        end
      end
    end

@movies instance variable is passed to assigns method and the value of @fake_results is assigned to @movies. The general strategy is to decouple the behavior that is being tested from the other behavior that it depends on. The controller should make the results returned by model method find_in_tmdb to the view. Either the actual results can be returned or the behavior can be mimicked to return fake results. The movie stub can be forced to return the fake results. Mock objects are going to stand in for the real movie objects. It doesn't matter whether the model returns real movie objects for the purposes of this test. In this test, the only thing being checked is whether the results passed by the model are being displayed in the view. The fake_results does not even have to be an array of movies, it could even be a string. Our major concern is whether the results from the model object are being sent correctly to the view.

Seam Concepts

Seams are used to enable just enough functionality for some specific behavior under test.

stub

It is similar to should_receive. But, should_receive also monitors whether the method gets called or not whereas the stub method doesn't care whether the method is called or not. If the stub gets called, we can chain and_return to the end of it to control the return value.

mock

It is a kind of 'stunt double' object. It can be used to stub individual methods on it. For example, we can make the stub method return the value of the title as 'Rambo' even though the mock object is not of movie type.

m = mock('movie1')
m.stub(:title).and_return('Rambo')

-shortcut: m = mock('movie1', :title=>'Rambo')

Test Cookery

1. Each spec should test just one behavior.

2. Use seams as needed to isolate that behavior.

3. Determine which explanation you will use to check that behavior.

4. Write the test and make sure it fails for the right reason.

5. Add code until test is green.

6. Look for opportunities to refactor/beautify.

References

1. https://www.youtube.com/watch?v=BU9k5t1yYgQ

2. https://www.youtube.com/watch?v=ZWvtrc-ysa4&feature=relmfu

Personal tools
Namespaces
Variants
Actions
Navigation
Toolbox