Friday 12 December 2008

Integration testing

I found this to be quite frustrating. Integration testing appears to be a relatively recent addition to Rails (since 1.1 I think), and pehaps for that reason there is not much on the web that explains the basics of how to do it. Coupled to that, my application is using an authentication system that I copied from a web site, and so I was just not that familiar with what is supposed to happen (at least, when I started; I know it a lot better after going through this). Also, the authentication system has some odd quirks, like defining session (rather than sessions) as a route, for a controller called sessions_controller.

Integration testing can be thought of functional testing for multiple controllers. This allows you to set up involved "stories" in which the user does a sequence of actions. Rails automatically generates unit and functional tests for you when you create a scaffold, but not an integration test (as it is not specific to one model/controller). However, there is a script to do the job for you.
ruby script/generate integration_test GeneralStories

Here is a test for the user authentication mentioned before:
  def test_authentication
user_bob = {:login => 'bob', :username => 'Robert',
:email => 'test@here.com', :password => '12345678',
:password_confirmation => '12345678', }
new_password = 'new_secret'

goes_to_home_page
goes_to_login
fails_to_log_in_with user_bob
goes_to_signup
signs_up_with user_bob
goes_to_login
logs_in_with user_bob
goes_to_change_password
changes_password user_bob, new_password
logs_out
goes_to_login
logs_in_with user_bob
goes_to_home_page
end

The code here is easy enough. A test method has to start test_ so Rails knows it is a test. First a hash is set up with details for a user, as well as a new password. Thereafter, it runs through a sequence of methods, each one corresponding to a single page download to the user (purely because that seems a convenient way to break it up). Reading down the sequence, we can read the story. The user goes to the home page, attempts to log in, but fails, then signs up, successfully logs in, changes his password, logs off, and logs on again, ending up back at the home page.

All the tricky stuff is, of course, hidden away in those methods. I am just going to look at the two involved in changing the password to illustrate the methodology.
def goes_to_change_password
get "/change_password"
assert_equal 200, status
assert_equal "/change_password", path
end

This corresponds to the user clicking on the "Change Password" link. The get command is equivalent to an HTTP GET. You then check the HTTP result is 200, and the returned page is correct. The next method is rather more complicated.
def changes_password(user_options, new_password)
post "/accounts/update",
:old_password => user_options[:password],
:password => new_password,
:password_confirmation => new_password
follow_redirect!
assert_equal 'Password successfully updated.', flash[:notice]
assert_equal "/", path
assert_equal 200, status
user_options[:password] = new_password
user_options[:password_confirmation] = new_password
end

Now the post command is invoked, for HTTP POST. The values on the form are sent in the form of a hash. Rails usually expects a hash of a hash here, which would look like this (and in the controller would be accessed with params[:user]):
  post "/accounts/update",
user => {:old_password => user_options[:password],
:password => new_password,
:password_confirmation => new_password}

I am not sure why my authentication system was set up differently; perhaps for older versions of Rails. If the action is successful, the user is redirected, and we want to see where that goes, so the next statement, follow_redirect!, does just that. Then we check everything: the flash notice should confirm the change, the path should be at the root and the HTTP status should be 200. Finally, a little bit of house-keeping; the user hash is updated with the new password ready to test logging on in a later step.

Multiple Sessions
In the background, Rails has a session object on the go, and this allows your test to proceed. If you want to you can invoke the session explicitly. One reason to do that would be to test what happens if multiple users are logged in at the same time (though I am suspicious that people use this when it is not necessary). It goes something like this (based in part on code here):
def test_signup_new_person
user_bob = {:login => 'bob', :username => 'Robert',
:email => 'test1@here.com', :password => '12345678',
:password_confirmation => '12345678'}
user_mary = {:login => 'mary', :username => 'Mary',
:email => 'test2@here.com', :password => '12345678',
:password_confirmation => '12345678'}
new_password = 'new_secret'

open_session do bob
open_session do mary
bob.extend(MyTestingDSL)
mary.extend(MyTestingDSL)

bob.goes_home
bob.goes_to_login
bob.fails_to_log_in_with user_bob
bob.goes_to_signup
bob.signs_up_with user_bob
mary.goes_to_signup
bob.goes_to_login
bob.logs_in_with user_bob
mary.signs_up_with user_mary
bob.goes_to_change_password
bob.changes_password user_bob, new_password
mary.goes_to_login
mary.logs_in_with user_mary
bob.logs_out
bob.goes_to_login
bob.logs_in_with user_bob
bob.goes_home
end
end
end

The test itself is more or less the same, except that the sequence is wrapped up in a block for open_session, and the session is addressed directly. This allows two sessions to be set up, bob and mary. The methods are set up the same, but again are wrapped up, this time in a module called MyTestingDSL. The module is mixed in to the session before the methods are invoked.

Finally...
I found it helpful to end my assertion with a call to this method:
def flash_me
"Error encountered. Flash[:error]=#{flash[:error]} Flash[:notice]=#{flash[:notice]}"
end

For example:
assert_equal 200, status, flash_me

If the assertion fails, you will get told the contents of flash. The method definition needs to be inside the module, if you are using sessions explicitly.

I found trouble-shooting a test failure to be something of a nightmare. The test informs you at which line in the test line it failed, but that gives you no clue about when in your actual code the problem is. It will report the same error (<200> expected but was <500>) for anything from an action that is not recognised, to an exception thrown in your model or a misnamed column. The best solution I found was to pepper the code with statements assigning values to the flash, which would then show up using the flash_me method above. Here is an example (with additional lines in red):
def qualify
flash[:notice] = 'here'
begin

@worksheet_meta_data = WorksheetMetaData.find(params[:id])
flash[:error] = '@worksheet_meta_data = nil' if @worksheet_meta_data.nil?
s = @worksheet_meta_data.qualify current_user.username
if s.nil?
flash[:notice] = "WorksheetMetaData was qualified (#{@worksheet_meta_data.qualified_by})."
else
flash[:error] = "Qualification failed: #{s}"
end
rescue Exception => ex
flash[:error] = "ex=#{ex}"
end

redirect_to(@worksheet_meta_data)
end

I also commented out the redirect! statement, so after the get or post, there is the assert, with a flash_me to see the result. The first extra line will confirm that the method is being invoked (are you using the right controller, action, and id; these might give a 404 error if not). Then everything up to the redirect is wrapped in a rescue block so any errors thrown get recorded in flash[:error]. Inside the block, I check

I would be interested to hear of any better ideas.

Struggling with Ruby: Contents Page

1 comment:

Unknown said...

Hello,
The Article on Integration testing gives amazing detail information about the testing .Thanks for Sharing the information about it. Software Testing Company