Monday, October 24, 2011

Guidelines to writing specs for ROR.

Specs for
1) Controllers

  a) In most cases it is safe and advisable to set-up the test environment for the specs to be run. So the following code becomes a habit at the beginning of each controller's spec

Note that the line numbers indicate the position of code relative to each other in the actual specs file.

1.   describe ExampleController, :type => :controller do
2.   before :each do
3.     request.env["HTTP_HOST"] = "test"
4.     User.delete_all
5.   end
6.   ...
7.   end

Though the part after the name of the controller on the first line is not absolutely necessary, adding it provides good heads-up to the reader of the code.

  b) To test any method of the controller, begin by describing it as the following lines in place of line 6 in the code.

6.  describe "#method_name" do
7.    # Tests
8.  end

c)  Then add some steps which you might require before the test begins by setting up pseudo-variables and values.

6.   describe "#method_name" do

7.    before do
8.      @user = Factory.create(:user)
9.      @objects = Factory.create(:object, :parameter_name => parameter_value, ...)
10.    # Tests
11.  end

In this step you should generate all objects needed by the spec to run properly. This is done so as to mimic to Rails that an actual object is being passed to the controller and it can use any of its properties that it may need in this methods. Hence, you have to pass all such values, which will be used for that object in the concerned method, at the point of creation of that variable.
    In this case it is @objects which can be based on any model and parameter_names would be the list of attributes of @object which will be used in the method_name method. You should pass them on to the controller with which it can complete the method execution, without which it will fail, as the object is a dummy one and would not have any value you don't explicitly assign. Having said that, it is not necessary to pass all the attributes. You need only pass those values which are of specific concern to the method being tested. For eg., 
    @user object will generally have a lot of test values assigned to it. If for any reason, you want the email-id, for example, to be of some specific domain for your method to complete, then we must pass its value as "" at the time of creation of @user.

d)  From this point on, it is time for getting in to the details of the method.
          So different methods have different nature, and they should be tested in their own ways.
          1)  Simple - Methods which are straight-forward in their behavior and proceed along a straight path of execution may fall into this category.
                       They only place calls to few other methods and at the end may/may not generate some specific kinds of pages.
                       You should add a "it" line which gives a sense to the reader of the code. Or a "context" line would also serve the same purpose.

6.    describe "#method_name" do
12.      (it/context) "should do this and that and return successfully" do
13.       controller.should_receive(:method1_name)
14.       controller.should_receive(:method2_name)

These are the names of the methods which will be called during the method_name's period. So we expect the controller to receive calls to them. If in the method you are calling method_other of any other model, say ModelA, then that should also be tested in here, by :- 

15.       ModelA.should_receive(:method_other)

If you are certain that a method will be called x number of times and want to verify that,

16.     controller/ModelA.should_receive(:method_other).exactly(x).times

2) Complex - These are the methods which create a number of objects in their period and also execute some statements which find specific objects from other models and tables. Then they use that to fire other complex operations and eventually fire validations on these objects received.
                    At this point you would have to create a number of fake objects by either stub-ing them or mock-ing them from their models.

@user = Factory.stub(:user) OR
@object_name = mock(ModelName, :parameter_name1 => 
parameter_value ...)

Note that the parameter_name1 has to be pre-pended with a : for Rails to identify properly an attribute of the ModelName being referred to but the parameter_value is mostly in the form of values, as in "value1" for string types or 123 for number types and so on.
                    These lines come mostly in the before do sections of the method or the before :each section of the controller, if the values being created are to be used throughout the controller code, which is a very rare case.
                    Once you have all the objects ready, comes the part where you have to stub method definitions, which is the most complex part.
                    We stub methods, meaning provide false definitions to the controller for those methods which you don't want the controller to run. You would do this because you don't write specs to monitor the entire method definition, but only to test specific portions of the method. So if there are some methods which might not run on the server/local because of some setup-limitations, so instead of letting the spec to fail, one should stub it out.
                    We do this, generally in the before do section, by:
8.  ModelName.stub(:additional_method_name).and_return(@variable)
                  Now the second part is extremely crucial if the method_name expects additional_method_name to return some values and use it in some future validations. If you simply stub the additional_method_name, any dependencies on it would cause the spec to fail. Hence if the additional_method_name returns an object of type ModelC, we generally apply the following procedure to stub it out safely

6.   before do
7.     @objectC = mock(ModelC, list_of_required_parameters...)    
8.     ModelName.stub!(:additional_method_name).and_return(@objectC)

Which tells Rails to assume that the additional_method_name returned the object @objectC and use it for future operations. 
               Once all of this is set-up, you are ready to place a call to the method_name. It can be done in one of the two ways:

17.    get :method_name, :parameter_name => parameter_value ...

which is the standard and most common way of calling the method with the set of values it expects.
                    But sometimes, there can be validations in the method_name to check if the request type had been post, only then continue, else return. In which case the previous statement becomes like:

17.    post :method_name, :parameter_name => parameter_value ...

Now that calling the method is done, it becomes essential to see the return from the method and test it against various conditions, which may be among the following:

18.  response.response_code.should == 200  (Checks for a successful return)
response.should  render_template('template_name')    (Checks that after the method, the expected template is being rendered)
response.should redirect_to(:controller => 'controller_name', :action => 'action_name', ...)

(this line checks that the page should be redirected to the action_name method of the controller_name controller and with a set of parameters. This line is mostly a copy of the statement in the controller method, wherein we verify if the expected thing actually happened.)
It might also be possible that the method_name creates objects of certain types, say ModelX, and you need to check if it really got created.

18.   object_variable = ModelX.find(:all, :conditions => { list of condition hash})

this line tries to find the object being created by passing a known set of conditions. Since the method must have created such an object, we verify it by:

19.   object_variable.should_not == nil

But to be 100% sure that this object had not been existing in the DB before this method spawned, you have to delete the table in the before do section of the method. Be rest assured it does not harm the data as all of this creation and deletion is only done in the test environment.

7.    ModelX.delete_all 

2) Models

    1.  describe ModelClassName, :type => :model do
    To note is, that the ModelClassName must be the same as the model name for which the spec is being written.
    In models the only difference comes in the way of calling the method in line 17.
    line 17 becomes
    17.   ModelName.method_name(list of parameters)
3) Helpers

  If you need to stub the helper with some values,

  1.  describe  HelperClassName, :type => :helper do
  To note is, that the HelperClassName must be the same as the name of the helper for which the spec is being written.

  helper.stub!(:param_name).and_return({list of values in hash style})
  In order to call the method, you would
  helper.method_name.should == #something.
  and the method expectancy tests go on.

No comments:

Post a Comment