Saturday, 15 November 2008

The Controller Part 4 - Functional Testing

Functional testing targets individual methods in your controllers. It tests the user is redirected, gets the right HTTP response and so on, without actually generating any web pages (though it does generate the HTML so will find errors there).

Setting it up
Rails creates functional tests, but there are a few things to be done first... The test database must be set up (though this was probably done when you did your unit testing):
rake db:test:prepare

As with unit testing the require needs altering, if you are using NetBeans 6.1: require 'test_helper' becomes require 'test/test_helper'. You should now be able to run the default tests put there by Rails (depending on the changes you have made to the controller and model). Outside of an IDE, the easiest way is using rake.
rake test (run all tests)
rake test:functionals (run all functional tests)
rake test:recent (run all test files modified in last 10 minutes)

There are typically three parts to a test method. The first part is setting up, and might include, for instance, logging a user on. The second, and only required, part calls the method and checks changes to the database. The third part checks the HTTP result.
def test_should_get_new
login
get :new
assert_response :success
end

Method Calls
By default, controller method calls have all the parameters you would expect (eg :controller), but often you want to add more. In this example, an extra parameter is given. This is equivalent to ?run=199 at the end of the URL.
get :new, :run => '199'
In his example, parameters for a model are given. This is equivalent to data in a form, as usually accessed via params. I found that this was essential for create, otherwise there is no data in your model, and so nothing gets saved and the test fails.
post :create, :sample => { :sample_reference => 'P12345', :sample_type => 'PP1'}

Checking the results
While your view can, of course, access any instance variable in the controller, you do not have direct access to those variables from within your tests. Instead, they can be accessed though the assigns method. This allows you to test against values.
assert_not_nil assigns(:samples)
assert_equal 1, assigns(:samples).length
assert_equal 'glc', assigns(:sample).analysis
# Test value of a specific attribute for one record
To check the returned HTTP code, just use the standard method:
assert_response :redirect
Other options are :success, :missing and :error. Not sure why you would test for the last two.

You can also test the template used. This is for those occasions when you specified a template (like this; render :template => 'folder/template'), otherwise the template is nil.
assert_template 'samples/new'

The last check is where the page has been redirected to. You can use the same parameters for assert_redirected_to that you can for redirect_to.
assert_redirected_to :controller => 'samples', :action => 'home'
assert_redirected_to samples_path
assert_redirected_to sample_path(assigns(:sample))

See also:
http://api.rubyonrails.com/classes/ActionController/Assertions/ResponseAssertions.html

You can also check what has happened to your database table with the assert_difference method. Wrap this method around your method call, and it will check that the number of records has changed by the appropriate amount. This example checks the number of records for a model called Post has increased by one (the default value) when a new record is created:
def test_should_create_post
assert_difference('Post.count') do
post :create, :post => { }
end
assert_redirected_to post_path(assigns(:post))
end

The destroy method needs to check that the number of records has decreased by one, so in that case the method call looks like this:
    assert_difference('Post.count', -1) do

What assert_difference does is to determine the value of the first parameter (Post.count in the example), then run the block, and then determine the value of the first parameter again. It then compares the value before and after, and checks that difference against the second parameter (which defaults to +1).

Fixtures
Fixtures are a way to easily generate example data. You can point your tests to a fixtures file with:
fixture :my_data

By default, rails includes the statement fixture :all in tester_helper.rb, so all the fixtures get loaded for all your tests.

Rails will look for test/fixtures/my_data.yml (or test/fixtures/my_data.csv), and use the data in there to create a data structure, my_data (actually, I suspect my_data is a method that returns the data_structure, but the effect is the same), as well as in your database table. Here is an example of a fixtures file, employees.yml.
:one
name: Fred Smith

:two
name: Mary Jones

You can access specific records from your fixtures using their label. For the employees fixture, to access Fred Smith, use:
employees(:one)

Functional testing gets a little more complicated once you have a model that belongs_to another (see here for how to set up the model). Let us suppose the companies.xml fixture file look like this:
:one
name: The Excellent Software Company

:two
name: MegaSoft Ltd

In the Employees fixture file, employees.yml, you can set up the association for the company very easily. Just add a key named after your associated model, with the value equal to the label you used for your entry (without the colon):
:one
name: Fred Smith
company: one

:two
name: Mary Jones
company: one

To assign a company to an employee in a test, use something like this:
post :create, :employee => {:name => 'Tom Johnson', :company => companies(:one) }


Preventing something when testing
It may be desirable to prevent some actions happening while testing, for example, confirmation e-mails being sent whenever a new user account is set up. This is easily done by checking ENV['RAILS_ENV'].
def my_method
return if ENV['RAILS_ENV'] == 'test' # Do not during testing
do_stuff_only_if_not_in_test_environment
end

Obviously this could cause potential problems, as you are specifically not testing some of your code, so use with care!

Testing functions that require a logged in user
This is what works for me...

At the start of each relevant method, put in a line like this:
login_as_admin(@request)

In test_helper.rb, define the login method, something like this:
def login_as_admin(request)
user = User.new
user.login = "tester"
user.email = "tester@domain.com"
user.username = "Test Administrator"
#...
# Set up other details, permissions, etc.
#...
request.session[:user_id] = user.id
end

You should also test that a user will get redirected if not logged in (and that a user logged in but with permissions gets redirected too). Here is how:
def test_should_be_refused_without_login
get :new # Example method to test
assert_response :redirect
assert_redirected_to :controller => 'session', :action => 'new'
end

Similar methods can be set up for other user roles, to test how the system behaves.

Testing helper methods
You can test your helpers (in application_help.rb) in your functional tests.
class HelperTest < Test::Unit::TestCase
include ActionView::Helpers::TextHelper
include ActionView::Helpers::TagHelper
include ApplicationHelper
# include whatever helpers you want to test here, sometimes you'll need
# to include some of the Rails helpers, as I've done above.

def test_some_helper
end
end

However, you do not have access to the usual Rails helpers, and so any of your methods that rely on them are going to generate errors. There may well be a way around that that I have yet to discover, though as some Rails helpers use the context of the web user to determine their behavior (eg, the output of link_to depends on the current web page) there may not.

Credit: http://blog.lathi.net/articles/2006/03/31/testing-rails-helpers

Testing filter methods
You can test filter methods via the usual fuctional tests, for example having one test in which a user is signed in, and another in which the user is not. However, I feel it is better to also test the method itself. I would describe that as unit testing, but as your filter may well do a redirect, you will have to test it in your functional tests. This will be a protected, instance method, so can be invoked like this:
MyModelsController.new.send(:check)

However, as far as I can find, there is no way to set the params variable, so you cannot test a filter that, for example checks an id exists (see forum thread here for more).

What to test
My opinion is that you need one functional test for each outcome of each action in your controller. If an action can result in either of two pages being rendered, then that action needs two tests (that said, I rarely bother to test for when a save fails). This rule of thumb does rely on you having pretty much nothing in your controller besides setting up the instance variable, and deciding what page to display; all the complicated stuff is in the models, and that is adequately covered in your unit tests.

Things to watch out for
If you get a error complaining about a database table or column missing check that the table or column really is there via SQL - have you updated your test tables after modifying them in the development environment? Alternatively, if there is not supposed to be a table or column of that name (eg a subclass in STI or a model you later removed), check the test/fixtures directory; Rails may be trying to put data from here into your old table - just delete the .yml file for an erroneous table, or edit the data to remove references to the erroneous column.

If you are getting 300 responses when you expect successes, it might be because you need to set up a session with an appropriate user.

I write tests by copy-and-pasting existing tests. This leaves me liable to having two tests with the same name. Ruby will give no warning about this, but will only perform one of the tests, which can lead to a false sense of security.

You cannot rely on the values of the id field. In the development database they number sequentially from 1, but in the test database they start at some huge number.

I had a method that found the most recent entry of a certain type in the database and used that as a template for a new one. This is difficult to test, because all the entries in your fixtures file get created at the same time, so you have no idea which one will be selected as the most recent. Fortunately, in my case I had a reference number for each record and I could use that instead.

The data in fixture files gets loaded without validation, which is fair enough. However, validation will apply to the method calls in your tests. So if your update test uses update_attributes, and is working with a record that will not validate (perhaps the record has a required field missing, or the same supposedly unique field as another), the result will be that update_attributes will return false, with no clue as to why. It took me about a long time to realise this.

Similarly, if your create method is failing to increase the count in your database table, it may be because the validation fails.

Be aware that functional testing will not check any of your links actually go anywhere, or that your web pages make sense.

I have to say that in setting up functional tests for my project, most of the errors it turned up were down to the testing, rather than real bugs...

Struggling with Ruby: Contents Page

3 comments:

Unknown said...

Hello,
The Article on Functional Testing along with Detail information about the Programming Stage is good.. It gives detailed information about it. Thanks for Sharing the information about the Functional testing For More information check the detail on Functional testing check,
Software Testing Services

Yasmeen Yas said...

Pretty good post. I just stumbled upon your blog and wanted to say that I have really enjoyed reading your blog posts. Any way I'll be subscribing to your feed and I hope you post again soon.
Software Testing Services
Functional Testing Services
Test Automation Services
QA Automation Testing Services
Regression Testing Services
API Testing Services
Compatibility Testing Services
Performance Testing Services
Security Testing Services
Vulnerability Testing Services

Yasmeen Yas said...

Thank you because you have been willing to share information with us. we will always appreciate all you have done here because I know you are very concerned with our.
Software Testing Services
Software Testing Services in India
Software Testing Companies in India
Software Testing Services in USA
Software Testing Companies in USA
Software Testing Companies
Software Testing Services Company
Software Testing and Quality Assurance