CSC/ECE 517 Fall 2012/ch2a 2w31 up

From PG_Wiki
Jump to: navigation, search

Contents

SaaS - 5.3 - The TDD cycle: red-green-refactor[1]

Introduction

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

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" [2]. TDD helps to capture this behavior directly using test cases. Thus TDD captures low level requirements whereas BDD captures high level requirements.

Concepts

The following topics provide an overview of a few concepts which would be helpful in understanding the TDD cycle and its example better.

Seams

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".
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[3]
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

RSpec[4] is a great testing tool, which provides features like :

[1,2,3].should include(1, 2)
obj.should_receive(a).with(b)

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.

RedGreen – Refactor

The following steps define the TDD cycle :

Add a Test

Implement the feature

Refactor

Iterate

Examples

TMDb : The Movie Database rails application

New Feature : Search TMDb for movies

Controller Action : Setup
  1. Add the route to config/routes.rb :
    To add a new feature to this Rails application, we first add a route, which maps a URL to the controller method [6]

    # Route that posts 'Search TMDb' form
    post '/movies/search_tmdb'

    This route would map to MoviesController#search_tmdb owing to Convention over Configuration, that is, it would post to the search_tmdb "action" in the Movies "controller".

  2. Create an empty view:
    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. "touch" unix command is used to create a file of size 0 bytes.

    touch app/views/movies/search_tmdb.html.haml

    The above creates a view in the right directory with the file name, same as Movie controller's method name. (Convention over Configuration)
    This view can be refined in later iterations and user stories are used to verify if the view has everything that is needed.

  3. Replace fake “hardwired” method in movies_controller.rb with empty method:
    If the method has a default functionality to return an empty list, then replace the method to one that does nothing.

    def search_tmdb
    end

What model method?

It is the responsibility of the model to call TMDb and search for movies. But, no model method exists as yet to do this.
One may wonder that to test the controller's functionality, one has to get the model method working. Nevertheless, that is not required.
Seam is used in this case, to test the code we wish we had(“CWWWH”). Let us call the "non-existent" model method as Movie.find_in_tmdb

Testing plan
  1. Simulate POSTing search form to controller action.
  2. Check that controller action tries to call Movie.find_in_tmdb 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.
  3. The test will fail (red), because the (empty) controller method doesnʼt call find_in_tmdb.
  4. Fix controller action to make the test pass (green).
Test MoviesController : Code[7]
-------- movies_controller.rb --------

class MoviesController < ApplicationController

  def search_tmdb
  end

end

-------- movies_controller_spec.rb --------

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
  end
end

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.

The line Movie.should_receive(:find_in_tmdb).with('hardware') creates an expectation that the Movie class should receive the find_in_tmdb method call with a particular argument. An assumption is made here, that the user has actually filled in hardware in the search_terms box on the page that says Search for TMDb.

Once the expectation is setup, we simulate the post using rspec-rails post :search_tmdb, {:search_terms => 'hardware'} as if it were a form and was submitted to the search_tmdb method in the controller (after looking up a route). The hash in this call is the contents of the params, which quacks like a hash, and can be accessed inside the controller method.

Thus, the test would fail if:

Testing

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.

Run the test written above.

The test FAILS .
Reason for error : MoviesController searching TMDb should call the model method that performs TMDb search

                  FailureError: Movie.should_receive(:find_in_tmdb).with('hardware')
                                expected: 1 time
                               received: 0 times


The test is expressing what is expected (identifying the right reason of failure).
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

-------- movies_controller.rb --------

class MoviesController < ApplicationController

  def search_tmdb
    Movie.find_in_tmdb(params[:search_terms])
  end

end

Movie.find_in_tmdb(params[:search_terms]) invokes the model's method with the value of search_Terms from the params hash.

Tests are run again. The test PASSES saying MoviesController searching TMDb should call the model method that performs TMDb search pass with 0 failures.
The following explains how invoking the non-existent method works and why the test case passed.

Use of Seams

The test fails as the controller is empty and the method does not call find_in_tmdb. The test case is made to pass by having the controller action invoke "Movie.find_in_tmdb" (which is, the code we wish we had) with data from submitted form. So here the concept of Seams comes in.
should_receive uses Rubyʼs open classes to create a seam for isolating controller action from behavior of a missing or buggy controller function. Thus, it overrides the find_in_tmdb 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

In this example find_in_tmdb 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 find_in_tmdb 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

Advantages and Disadvantages

Advantages

Disadvantages

References

  1. Video :SaaS - 5.3 - The TDD cycle: red-green-refactor
  2. TDD vs BDD
  3. Working Effectively With Legacy Code by Michael Feather
  4. RSpec
  5. What is Test Driven Development?
  6. Routing in Rails
  7. TMDb : MoviesController Test Code
  8. TDD Advantages
  9. Disadvantages of TDD
  10. Shortcomings of TDD

See Also

Books

Personal tools
Namespaces
Variants
Actions
Navigation
Toolbox