|
|
(16 intermediate revisions by the same user not shown) |
Line 1: |
Line 1: |
| = SaaS - 5.4 - More Controller Specs and Refactoring<ref>[https://www.youtube.com/watch?v=ZWvtrc-ysa4&feature=relmfu Video :SaaS - 5.4 - More Controller Specs and Refactoring]</ref> =
| | <h2>SaaS - 5.4 - More Controller Specs and Refactoring </h2> |
| | __TOC__ |
| | ==Introduction== |
| | This ia a textbook section that covers the online [https://www.youtube.com/watch?v=BU9k5t1yYgQ lectures] on [https://www.youtube.com/watch?v=ZWvtrc-ysa4&feature=relmfu 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. |
|
| |
|
| == Introduction ==
| | '''Key Idea''' - to break dependency between the method under test and its collaborators. This is what [http://expertiza.csc.ncsu.edu/wiki/index.php/CSC/ECE_517_Fall_2012/ch2a_2w31_up#Seams seams] are designed to do. |
| [http://en.wikipedia.org/wiki/Test-driven_development Test Driven Development(TDD)] is an evolutionary approach to software development that requires a developer to write test code before the actual code and then write the minimum code to pass that test. This process is done iteratively to ensure that all units in the application are tested for optimum functionality, both individually and in synergy with others. This produces applications of high quality in less time. | |
|
| |
|
| === BDD Vs TDD === | | ==The Code You Wish You Had== |
| Behavior driven development (BDD) is a software development process built on TDD. BDD helps to capture requirement as user stories both narrative and scenarios. "User stories in BDD are written with a rigid structure having a narrative that uses a Role/Benefit/Value grammar and scenarios that use a Given/When/Then grammar" <ref>[http://neelnarayan.blogspot.com/2010/07/bdd-is-more-than-tdd-done-right.html TDD vs BDD] </ref>. TDD helps to capture this behavior directly using test cases. Thus TDD captures low level requirements whereas BDD captures high level requirements.
| | '''Example''' |
|
| |
|
| === Concepts ===
| | '''TMDb : The Movie Database rails application''' |
|
| |
|
| The following topics provide an overview of a few concepts which would be helpful in understanding the TDD cycle and its example better.
| | '''New Feature : Search TMDb for movies''' |
|
| |
|
| ==== Seams ====
| | When the controller method receives the search form: |
| The concept of CWWWH(code we wish we had) is about a missing/buggy piece of code in TDD. In test driven development, we generally write a test and then the implement the functionality. But it may happen that the program which implements a certain feature, is dependent on some other feature which is not yet implemented or has errors. That piece of code is named as "CWWWH".
| |
| <br/> Given that scenario, a test case for such a functionality is expected to fail owing to the dependency. Nevertheless, the tests can be made to pass, with a concept called Seams, defined by Michael Feather's in his book Working Effectively With Legacy Code<ref>[http://www.objectmentor.com/resources/articles/WorkingEffectivelyWithLegacyCode.pdf Working Effectively With Legacy Code by Michael Feather]</ref>
| |
| <br/>
| |
| A seam is a place where one can alter the application's behavior without editing the actual code. This helps us to isolate a code from its dependent counterpart. Thus, one can work with a given code, abstracting out the implementation of its dependency, and assuming it works right. This is explained clearly with an example below.
| |
|
| |
|
| ==== RSpec ====
| | 1. As explained in the previous [http://expertiza.csc.ncsu.edu/wiki/index.php/CSC/ECE_517_Fall_2012/ch2a_2w31_up#New_Feature_:_Search_TMDb_for_movies textbook section] , the controller method should call a method that will search TMDb for a specified movie. |
| RSpec<ref>[http://rspec.info/ RSpec]</ref> is a great testing tool, which provides features like :
| |
| * textual descriptions of examples and groups ([http://rubydoc.info/gems/rspec-core/frames rspec-core])
| |
|
| |
|
| * extension for Rails ([http://rubydoc.info/gems/rspec-rails/frames rspec-rails])<br/>If we are testing a rails application specifically (as opposed to an arbitrary Ruby program), we need to be able to simulate<br/>* posting to a controller action<br/>* the ability to examine the expected view
| | 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. |
|
| |
|
| * extensible expectation language ([http://rubydoc.info/gems/rspec-expectations/frames rspec-expectations]), letting an user express expected outcomes of an object.<br/>Uses instance methods like "should" and "should_not" to check for equivalence, identity, regular expressions, etc.
| | ===Should and Should Not=== |
| <pre>[1,2,3].should include(1, 2)</pre>
| | 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. |
|
| |
|
| * built-in mocking/stubbing framework ([http://rubydoc.info/gems/rspec-mocks/frames rspec-mocks]): <br/>Rspec has built-in facility to setup mock methods or stub objects. There can be several dependencies of a method, that is tested. It is important to test(unit test) only a particular behavior and mock out the other methods that are called from there. Rspec provides "should_receive" clause which overwrites the foreign method implementation and makes sure that missing methods or buggy methods do not affect the current behavior that is tested.
| | <pre> |
| <pre>obj.should_receive(a).with(b)</pre>
| | obj.should match-condition |
| For any given object, we can set up an expectation that the object should receive a method call. In this case the method name is specified as "a" and it is called on object "obj". The method could be optionally called with an argument which is specified using "with". If arguments exist then two things are checked-- a) whether the method gets called b) whether correct arguments are passed. If arguments are absent then the second check is skipped.
| | </pre> |
|
| |
|
| == <span style="color:#FF0000">Red</span> – <span style="color:#32CD32">Green</span> – Refactor ==
| | RSpec defines some built-in matchers that can be used as the match-condition. We can also define some methods of our own. |
| The following steps define the TDD cycle :
| |
| === Add a Test ===
| |
| * <b>Think about one thing the code should do :</b> The developer identifies a new functionality from the use cases and user stories, which contain detailed requirements and constraints.
| |
| * <b>Capture that thought in a test, which<span style="color:#FF0000"> fails </span> :</b> An automated test case (new or a variant of an existing test) is then written, corresponding to the new feature, taking into consideration all possible inputs, error conditions and outputs. Run all the automated tests. The new test inevitably fails because it is written prior to the implementation of the feature. This validates that the feature is rightly tested and would pass if the feature is implemented correctly, which drives a developer to the next step.
| |
|
| |
|
| === Implement the feature ===
| | <pre> |
| * <b>Write the simplest possible code that lets the test <span style="color:#32CD32"> pass </span> :</b> Minimal code is written to make the test pass. The entire functionality need not be implemented in this step. It is not uncommon to see empty methods or methods that simply return a constant value. The code can be improved in the next iterations. Future tests will be written to further define what these methods should do. The only intention of the developer is to write "just enough" code to ensure it meets all the tested requirements and doesn't cause any other tests to fail. <ref>[http://ruby.about.com/od/advancedruby/a/tdd.htm What is Test Driven Development?]</ref> Run the tests again. Ideally, all the tests should pass, making the developer confident about the features implemented so far.
| | count.should == 5 (Syntactic sugar for count.should.==(5)) |
|
| |
|
| === Refactor ===
| | 5.should(be.<(7)) (be creates a lambda that tests the predicate expression) |
| * <b>DRY out commonality with other tests :</b> Remove duplication of code wherever possible. Organizational changes can be made as well to make the code appear cleaner so it’s easier to maintain. TDD encourages frequent refactoring. Automated tests can be run to ensure the code refactoring does not break any existing functionality
| |
|
| |
|
| === Iterate ===
| | 5.should be < 7 (Syntactic sugar allowed) |
| * Continue with the next thing (new or improvement of a feature), the code should do.
| |
| * Aim to have working code always.
| |
|
| |
|
| ==Examples==
| | 5.should be_odd (use method_missing to call odd? on 5) |
|
| |
|
| === TMDb : The Movie Database rails application ===
| | result.should include(elt) (Calls Enumerable#include?) |
| ====New Feature : Search TMDb for movies====
| |
|
| |
|
| ===== Controller Action : Setup =====
| | result.should match(/regex/) |
| #Add the route to <span style="color:blue"> config/routes.rb </span>:<br/>To add a new feature to this Rails application, we first add a route, which maps a URL to the controller method <ref>[http://guides.rubyonrails.org/routing.html Routing in Rails]</ref> <br/><br/><code><span style="color:grey"># Route that posts 'Search TMDb' form </span><br/><span style="color:blue"> post </span> '/<span style="color:red">movies</span>/<span style="color:green">search_tmdb</span>'</code><br/>This route would map to <code> <span style="color:red">Movies</span><span style="color:blue">Controller#</span><span style="color:green">search_tmdb</span></code> owing to [http://en.wikipedia.org/wiki/Convention_over_configuration Convention over Configuration], that is, it would post to the search_tmdb "action" in the Movies "controller".<br/><br/>
| | </pre> |
| #Create an empty view: <br/> When a controller method is triggered, it gets some user input, does some computation and renders a view. So, the invocation of a controller needs a view to render, which we need to create even though it is not required to be tested. We start with an empty view. "[http://en.wikipedia.org/wiki/Touch_(Unix) touch]" unix command is used to create a file of size 0 bytes. <br/> <br/><code><span style="color:blue">touch app/views/<span style="color:red">movies</span>/<span style="color:green">search_tmdb</span>.html.haml </span></code><br/><br/>The above creates a view in the right directory with the file name, same as Movie controller's method name. (Convention over Configuration) <br/>This view can be refined in later iterations and user stories are used to verify if the view has everything that is needed.<br/><br/>
| |
| #Replace fake “hardwired” method in <span style="color:blue">movies_controller.rb</span> with empty method: <br/>If the method has a default functionality to return an empty list, then replace the method to one that does nothing.<br/><br/><code>def search_tmdb<br/>end</code><br/>
| |
|
| |
|
| =====What model method?=====
| | In all the above cases, '''should_not''' can also be used in place of '''should'''. |
| It is the responsibility of the model to call TMDb and search for movies. But, no model method exists as yet to do this.<br/>
| |
| One may wonder that to test the controller's functionality, one has to get the model method working. Nevertheless, that is not required.<br/>
| |
| Seam is used in this case, to test the code we wish we had(“<span style="color:red">CWWWH</span>”). Let us call the "non-existent" model method as <span style="color:blue">Movie.find_in_tmdb</span>
| |
|
| |
|
| =====Testing plan===== | | ===Check for Rendering=== |
| #Simulate POSTing search form to controller action.
| | There is an RSpec construct '''render_template''' that checks whether a controller method would render a template with a particular name. |
| #Check that controller action tries to call <span style="color:blue">Movie.find_in_tmdb</span> with the function argument as data from the submitted form. Here, the functionality of the model is not tested, instead the test ensures the controller invokes the right method with the right arguments.
| |
| #The test will fail (<span style="color:red">red</span>), because the (empty) controller method doesnʼt call find_in_tmdb.
| |
| #Fix controller action to make the test pass (<span style="color:green">green</span>).
| |
| | |
| =====Test MoviesController : Code<ref>[http://pastebin.com/zKnwphQZ TMDb : MoviesController Test Code]</ref>=====
| |
|
| |
|
| <pre> | | <pre> |
| -------- movies_controller.rb --------
| | result.should render_template('search_tmdb') |
| | </pre> |
|
| |
|
| class MoviesController < ApplicationController
| | 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. |
|
| |
|
| def search_tmdb
| | <pre> |
| end
| | require 'spec_helper' |
| | | |
| end
| | describe MoviesController do |
| | | describe 'searching TMDb' do |
| -------- movies_controller_spec.rb --------
| | it 'should call the model method that performs TMDb search' do |
| | | Movie.should_receive(:find_in_tmdb).with('hardware') |
| require 'spec_helper' | | post :search_tmdb, {:search_terms => 'hardware'} |
| describe MoviesController do | | end |
| describe 'searching TMDb' do
| | it 'should select the Search Results template for rendering' do |
| it 'should call the model method that performs TMDb search' do
| | Movie.stub(:find_in_tmdb) |
| Movie.should_receive(:find_in_tmdb).with('hardware')
| | post :search_tmdb, {:search_terms => 'hardware'} |
| post :search_tmdb, {:search_terms => 'hardware'}
| | response.should render_template('search_tmdb') |
| | end |
| | end |
| end | | end |
| end
| |
| end
| |
| </pre> | | </pre> |
|
| |
|
| The above test case for the MoviesController has an 'it' block, which has a string defining what the test is supposed to check. A do-end block to that 'it' has the actual test code. <br/>
| | Post and render_template are the extensions in Rails that have been added specifically by RSpec to test rails code. |
|
| |
|
| The line <code><b>Movie</b>.<span style="color:violet">should_receive</span>(<span style="color:red">:find_in_tmdb</span>).<span style="color:violet">with</span>('<span style="color:brown">hardware</span>')</code> creates an expectation that the <b>Movie</b> class should receive the <span style="color:red">find_in_tmdb</span> method call with a particular argument. An assumption is made here, that the user has actually filled in <span style="color:brown">hardware</span> in the search_terms box on the page that says Search for TMDb. <br/>
| | 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. |
|
| |
|
| Once the expectation is setup, we simulate the post using rspec-rails <code><b>post</b> <span style="color:red">:search_tmdb</span>, {<b>:search_terms</b> => '<span style="color:brown">hardware</span>'}</code> as if it were a form and was submitted to the <span style="color:red">search_tmdb</span> method in the controller (after looking up a route). The hash in this call is the contents of the <span style="color:green">params</span>, which quacks like a hash, and can be accessed inside the controller method. <br/>
| | ===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. |
|
| |
|
| Thus, the test would fail if:
| | <pre> |
| * once the post action is completed, should_receive finds that the method find_in_tmdb was not invoked.
| | require 'spec_helper' |
| * and if the method was indeed called, that single argument 'hardware' was not passed.
| | |
| | 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 |
| | </pre> |
|
| |
|
| ===== Testing =====
| | @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. |
|
| |
|
| [https://github.com/rspec/rspec/wiki/autotest Autotest] runs continuously and watches for any change in a file. Once the changes are saved, the test corresponding to the change is automatically run and the result is reported immediately.
| | ==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. |
|
| |
|
| Run the test written above.<br/>
| | ===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. |
|
| |
|
| The test <span style="color:red"> FAILS </span>. <br/>
| |
| Reason for error : MoviesController searching TMDb should call the model method that performs TMDb search <br/>
| |
| FailureError: Movie.should_receive(:find_in_tmdb).with('hardware')
| |
| <span style="color:red"> expected: 1 time
| |
| received: 0 times</span>
| |
| <br/>
| |
| The test is expressing what is expected (identifying the right reason of failure).<br/>
| |
| To make the test pass, we change the MovieController's search_tmdb method to invoke the Model's find_in_tmdb method.
| |
|
| |
| Changes made to the controller
| |
| <pre> | | <pre> |
| -------- movies_controller.rb --------
| | m = mock('movie1') |
| | m.stub(:title).and_return('Rambo') |
|
| |
|
| class MoviesController < ApplicationController
| | -shortcut: m = mock('movie1', :title=>'Rambo') |
| | |
| def search_tmdb
| |
| Movie.find_in_tmdb(params[:search_terms])
| |
| end
| |
| | |
| end
| |
| </pre> | | </pre> |
|
| |
|
| <code>Movie.find_in_tmdb(params[:search_terms])</code> invokes the model's method with the value of search_Terms from the params hash.<br/>
| | ==Test Cookery== |
| | | 1. Each spec should test just one behavior. |
| Tests are run again. The test <span style="color:green">PASSES</span> saying MoviesController searching TMDb should call the model method that performs TMDb search pass with 0 failures.
| |
| <br/> The following explains how invoking the non-existent method works and why the test case passed.
| |
| | |
| ===== Use of Seams=====
| |
| The test <span style="color:red">fails</span> as the controller is empty and the method does not call <span style="color:blue">find_in_tmdb</span>. The test case is made to <span style="color:green">pass</span> by having the controller action invoke "Movie.<span style="color:blue">find_in_tmdb</span>" (which is, the code we wish we had) with data from submitted form. So here the concept of Seams comes in.<br/>
| |
| <span style="color:violet">should_receive</span> uses Rubyʼs open classes to create a seam for <b><i>isolating controller action from
| |
| behavior of a missing or buggy controller function</i></b>. Thus, it overrides the <span style="color:blue">find_in_tmdb</span> method. Although it does not implement the logic of the method, it checks whether it is being called with the right argument. Even if the actual code for find_in_tmdb existed, the method defined in should_receive would have overwritten it. This is something we would need, as we don't want to be affected by bugs in some other code that we are not testing. This is an example of stub and every time a single test case is completed, all the mocks and stubs are automatically refreshed by Rspec. This helps to keep tests independent. | |
|
| |
|
| ===== Return value from should_receive =====
| | 2. Use seams as needed to isolate that behavior. |
| In this example <span style="color:blue">find_in_tmdb</span> should return a set of movies for which we had called the function. Thus we should be checking its return value. However this should be checked in a different test case.
| |
| Its important to remember that each "it" clause or each spec should test only one clause/behavior. In this example the first requirement was to make sure that <span style="color:blue">find_in_tmdb</span> is called with proper arguments and the second requirement is make sure that the result of search_tmdb is passed to the view so that it can be rendered. We have two different requirements and hence there must be two different test cases.
| |
|
| |
|
| == Conclusion ==
| | 3. Determine which explanation you will use to check that behavior. |
|
| |
|
| === Advantages and Disadvantages ===
| | 4. Write the test and make sure it fails for the right reason. |
|
| |
|
| ==== Advantages ====
| | 5. Add code until test is green. |
| * ensures the code is tested and enables you to retest your code quickly and easily, since it’s automated.
| |
| * immediate feedback
| |
| * improves code quality
| |
| * less time spent for debugging
| |
| * faster identification of the problem
| |
| * early and frequent detection of errors prevent them from becoming expensive and hard problems later<ref>[http://en.wikipedia.org/wiki/Test-driven_development#Benefits TDD Advantages]</ref>
| |
|
| |
|
| ==== Disadvantages ====
| | 6. Look for opportunities to refactor/beautify. |
| * Does not scale well with web-based GUI or database development <ref>[http://www.pnexpert.com/files/Test_Driven_Development.pdf Disadvantages of TDD]</ref>
| |
| * reliant on refactoring and programming skills
| |
| * increases the project complexity and delivery time
| |
| * tightly coupled with the developer's interpretation, since developer writes the test cases mostly <ref>[http://en.wikipedia.org/wiki/Test-driven_development#Shortcomings Shortcomings of TDD]</ref>
| |
|
| |
|
| == References == | | ==References== |
| <references/> | | <references/> |
| | 1. https://www.youtube.com/watch?v=BU9k5t1yYgQ |
|
| |
|
| == See Also ==
| | 2. https://www.youtube.com/watch?v=ZWvtrc-ysa4&feature=relmfu |
| *[http://en.wikipedia.org/wiki/Test-driven_development Wiki page of TDD]
| |
| *[http://searchsoftwarequality.techtarget.com/definition/test-driven-development Definition of TDD]
| |
| *[http://www.slideshare.net/Skud/test-driven-development-tutorial TDD in different languagues]
| |
| *[http://net.tutsplus.com/tutorials/php/the-newbies-guide-to-test-driven-development/ The Newbie’s Guide to Test-Driven Development]
| |
| *[http://blog.pluralsight.com/2012/09/11/tdd-vs-bdd/ TDD vs BDD]
| |
| === Books ===
| |
| * [http://www.amazon.com/Test-Driven-Development-By-Example/dp/0321146530 Test Driven Development: By Example , Kent Beck]
| |
| * [http://www.agiledata.org/essays/tdd.html Disciplined Agile Delivery (DAD): A Practitioner’s Guide to Agile Software Delivery in the Enterprise by Scott W. Ambler and Mark Lines, IBM Press, ISBN: 0132810131]
| |
SaaS - 5.4 - More Controller Specs and Refactoring
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
<references/>
1. https://www.youtube.com/watch?v=BU9k5t1yYgQ
2. https://www.youtube.com/watch?v=ZWvtrc-ysa4&feature=relmfu