Monday, 19 August 2013

Listing all columns in all models

As I look to upgrade to Rails 4, and I am looking at strong parameters - more on them later - but something I need on the way is a list of columns for each model. Here is some code that will do that.

First, make sure all your models are loaded (this can take some time) (cache_classes must be on, which it is by default in development mode):

Rails.application.eager_load!

Then get an array of all  ActiveRecord::Base sub-classes. It also get sub-classes of sub-classes, by the way.


ary = ActiveRecord::Base.descendants

Then you just need to list them:

puts ary.map {|m| "#{m.to_s} ~ :#{m.column_names.join ', :'}" }.join("\n")

Here is a useful page about preparing the upgrade to Rails 4, by the way:
https://iprog.com/posting/2013/07/preparing-for-an-upgrade-to-rails-4-0

Monday, 1 July 2013

Code Coverage with SimpleCov


Recently I have been using SimpleCov to test my code coverage. When UI first tried to use it, it threw up errors, but having updated the infrastructure, it seems to work fine (though I do get a ton of warnings). Just so you know, I am using JRuby 1.7.4 and Rails 3.2.13 - not sure which of these was causing the problem.

To get coverage statistics, install the SimpleCov gem, and put it in the gemfile.

group :test do
  gem 'simplecov'
end

In your test/test_helper file require it. Here I require it only if COVERAGE is set in the command line:

require 'simplecov' if ENV["COVERAGE"]

Then set some parameters in that file too. Here I exclude some folders and files, and then tell SimpleCov to group results (again, only if that command line flag is set).

SimpleCov.start do
  add_filter 'test/'
  add_filter 'config/'
  add_filter 'vendor/'
  add_filter 'mod_db.rb'  # One use methods for modifying the database

  add_group 'Controllers', 'app/controllers'
  add_group 'Models', 'app/models'
  add_group 'Helpers', 'app/helpers'
  add_group 'Mailers', 'app/mailers'
  add_group 'Views', 'app/views'
  add_group 'Library', 'lib/my_lib'
end if ENV["COVERAGE"]

It is slower than normal testing, so check that everything passes first. I use this on my command line, so err.txt catches all the warnings.

jruby -S rake test COVERAGE=true 2>err.txt >tmp.txt

The results appear in coverage/index.html, and look like this:



 Clicking on a file name will bring up that file, and any untested lines will be highlighted in red. I found a few places when a page should show a record and a set of associated sub-records, but the code for the associated was missed because I had not set the record to have any sub-records in my test.

Thursday, 6 June 2013

Testing Mailers

The first thing to check when testing your project sends e-mails is that your project is not sending them when you are testing. Rails does this by default by a setting in config/environments/test.rb:

  config.action_mailer.delivery_method = :test

Compare to the line in config/environments/development.rb

  config.action_mailer.delivery_method = :smtp


Setting this to test stops e-mails being sent, and instead they are sent to an array, ActionMailer::Base.deliveries. This array is reset before each test, by the way.

Here is a simple mailer to test.


class Notifier < ActionMailer::Base
  default :from => "DB@mysite.com"
  add_template_helper(ApplicationHelper)
  
  # Sets up an e-mail for notifying the user to activate his account.
  def signup_notification user
    @name = user.username
    @login = user.login
    @url  = "http://mysite.com/activate/#{user.activation_code}"
    mail :to => user.email, :subject => 'Activate your account'
  end

  def page_error(err, request)
    @err = err
    @request = request
    mail :to => 'admin@mysite.com', :subject => 'Page error'
  end
end

In your controller, you might invoke the first like this:

  Notifier.signup_notification(@user).deliver

In the test, you will need to break that up, so you can examine the mail object.

class NotifierTest < ActionMailer::TestCase
  test "signup_notification" do
    # Create a mock user
    user = TestUser.new 'tester', 'tester@nowhere.com'
    # Invoke the mailer method
    mail = Notifier.signup_notification user
    # Deliver the mail
    mail.deliver
    # Check the mail got sent
    assert !ActionMailer::Base.deliveries.empty?
    # Check it is the right mail
    assert_equal 'Activate your account', mail.subject
    assert_equal ["tester@nowhere.com"], mail.to
    assert_equal ["DB@mysite.com"], mail.from
    assert_match "Visit this url to activate your account",
                    mail.body.encoded
  end
end

Here is the TestUser definition.

class TestUser
  attr_reader :username, :email, :login

  def initialize username, email
    @username = username
    @email = email
    @login = username.gsub ' ', ''
  end
  
  def activation_code; "abcd"; end
end

The second method in the mailer above is for sending error reports to the administrator. Here is a method that generates an error, and sends that to the mailer:

  test "page_error" do
    request = TestRequest.new
    begin
      raise "A test error"
    rescue Exception => err
      mail = Notifier.page_error(err, request)
      mail.deliver
      assert !ActionMailer::Base.deliveries.empty?
      assert_equal "Page error", mail.subject
      assert_equal ["admin@mysite.com"], mail.to
      assert_equal ["DB@mysite.com"], mail.from
      assert_match "error encountered!", mail.body.encoded
    end
  end

Here is the TestRequest definition; it simply returns the string "good" if the method name is recognised - that is enough for the mailer to wok with, and will still highlight any mistyped or made-up method names.

class TestRequest
  # Returns the string "good" if the method is recognised
  def method_missing method, *args
    return "good" if [:fullpath, :request_method, :query_parameters,
                      :request_parameters, :referer].include? method
    super  
  end
end
end

Tuesday, 4 June 2013

Testing Protected Methods in a Controller, Part 2

See part 1 here.

Unit Testing Controller Methods

You are not obliged to test controller methods in with your functional tests. You can mix them in with your unit tests, though they do need a separate class (and so ideally a separate file). The trick is to sub-class from ActionController::TestCase, and to then nominate a controller you are testing (this is normally derived from the test name, so would not be necessary if you maintain the naming convention):

class DrumsControllerMethodsTest < ActionController::TestCase
  tests DrumLog::DrumsController

  # tests
end

Whether this is a good idea is debatable. I would argue that testing these protected methods is proper unit testing, and is not functional testing, so that suggests they should be with the rest of the unit tests (as your helper tests already are), however, I think I read that in Rails 4 they will be relabelled as model tests and controller tests, which kind of undermines that argument.

One solution is to move them to a library file (create a module, and include it in ApplicationController), then you can unit test them with your other library files. Obviously this works best for more generic methods.

Using A Mock Controller


Sometimes you want test methods that invoke methods like render and redirect_to that are doing all sorts of things behind the scenes. Better (in my opinion) to create a mock controller, and over-write these methods.

Here is an example. It is testing a method check_security_level (a filter in fact) that will send the user to the log-in page if he tries to access the edit page when not logged in, but does nothing if he tries to access the index page.

# Create a mock class to subvert redirect_to,
# inheriting from ApplicationController.
# You could inherit an actual controller class.
class MockSampleController < ApplicationController
  attr_reader :destination
  
  # Redefine direct_to to just note the destination and do nothing more.
  def redirect_to s; @destination = s; end
end



class MyControllerTest < ActionController::TestCase
  tests MockSampleController
  
  # First test, check_security_level should redirect the user to a login page
  def test_check_security_level_1
    # Set the request parameters (must be strings not symbols)
    @controller.params[:action] = 'edit'  
    @controller.send(:check_security_level)
    assert_equal '/user_log/sessions', @controller.destination[:controller]
  end

  # Second test, check_security_level should do nothing
  def test_check_security_level_2
    @controller.params[:action] = 'index'  
    @controller.send(:check_security_level)
    assert_nil @controller.destination
  end
end

Add New Routing For Tests


Sometimes you want to create a mock controller with its own set of actions for testing methods in your controllers. This is not trivial as you need to have a set of routes for your test actions, and it is bad practice to put them into routes.rb, as they will end up in your production environment.

Creating a routes.rb for testing is not ideal, as it will get out of sync with the real routes.rb, and there is no simple way to append new routes (the Application.routes.draw method clears all existing routes).

The solution is to use the with_routing method, which is a standad part of Rails. However, it is somewhat neglected, as the API shows...

This is the example from 2.3.8, when it was in  ActionController::TestProcess:
http://apidock.com/rails/ActionController/TestProcess/with_routing

 with_routing do |set|
    set.draw do |map|
      map.connect ':controller/:action/:id'
        assert_equal(
          ['/content/10/show', {}],
          map.generate(:controller => 'content', :id => 10, :action => 'show')
      end
    end
  end


Spot the missing right bracket to match the one in "assert_equal("?

In Rails 3 it got moved to ActionDispatch::Assertions::RoutingAssertions...

 with_routing do |set|
    set.draw do |map|
      map.connect ':controller/:action/:id'
        assert_equal(
          ['/content/10/show', {}],
          map.generate(:controller => 'content', :id => 10, :action => 'show')
      end
    end
  end

Still missing that right bracket. And still using Rails 2 style routing!

Here is an example that works:

  def test_render_generic_form
    with_routing do |set|
      set.draw do
        resources :mocks do
          collection do
            get :test_action
          end
        end
      end
    
    
      get :test_action
      # assertions
    end
  end


Friday, 31 May 2013

Testing Protected Methods in a Controller, Part 1

Methods in a controller (including applicatin_controller.rb) can be divided between those that are actions (so invoked by HTTP requests) and those that are not. Those that are not should all be set as protected (or private), and it is these that I am posting about.

The first thing to do is to shift as much as you can out of the controllers. If at all possible, put in in a model or a library, and then just unit test them. However, that is not always easy, say because you want to invoke a redirect from the method or you need quick access to the session (I expect you could handle the session in a library file, but I think it would be messy and more trouble than it is worth).

So we have a bunch of protected methods, and they all need testing.

While you could test them in the same file you test your actions, thematically they are very different, and I think it makes more sense to put them in their own file. Here is an example. The only tricky part is that you need to say what controller it should use with the "tests" command. Oh, and you need to use "send" to access the method being tested, as it is protected.

require 'test/test_helper'

class ControllerMethodsTest < ActionController::TestCase
  tests PostsController
  
  def setup
    user_setup
  end

  def test_current_user
    login_as ['trainer', 'it'], request
    assert_equal 'tester', @controller.send(:current_user).login
  end
end

The "user_setup" methods puts some roles into the testing database. The "login_as" method is one that I have used a lot for testing actions relating to web pages that have the user logged in.

  def login_as roles, request
    create_user
    user = UserLog::User.find_by_login 'tester'
    roles = [roles] unless roles.is_a? Array
    roles.each { |role| assign_role user, role }
    request.session[:user_id] = user.id
    #p "set request.session[:user_id] to #{request.session[:user_id]}"
  end

  def create_user name = "tester"
    user = UserLog::User.new
    user.login = name
    user.email = "#{name}@domain.com"
    user.username = name.titlecase
    user.password = "12345678"
    user.password_confirmation = "12345678"
    user.save(:validate => false)
    user.send(:activate!)
    UserLog::User.find_by_login name
  end

  def assign_role(user, rolename)
    role = UserLog::Role.find_by_rolename(rolename)
    raise "Failed to find role #{rolename}" if role.nil?
    permission = UserLog::Permission.new
    permission.role = role
    permission.user = user
    permission.save(:validate => false)
  end

See part 2 here.

Thursday, 16 May 2013

When functional tests fail to fail

I have come across a couple of instances recently where the project passed the tests, but really should not have.


Check the record really was changed

If you move your models, controllers and views into sub-directories, form data will get the sub-directory name prepended to the parameters key

Parameters: {"utf8"=>"Ô£ô", "authenticity_token"=>"4", "post"=>{"name"=>"My Post", "text"=>"some text"}, "commit"=>"Submit", "id"=>"3"}

... becomes:
Parameters: {"utf8"=>"Ô£ô", "authenticity_token"=>"4", "subfolder_post"=>{"name"=>"My Post", "text"=>"some text"}, "commit"=>"Submit", "id"=>"3"}

Rails does that automatically, so you may not realise it has happened. In your controller, however, the action method is still looking for params[:post]. Rails does not change that, so when the user tries to edit a record, Rails finds no hash of data for params[:subfolder_post], so assumes there is nothing to change. Then it saves your record, and reports back that everything saved okay!

It gets worse. Your functional test might look like this:

  test "should update post" do
    id = Subfolder::Post.first.id
    put :update, :id => id, :post => { :text => 'modified' }
    assert_redirected_to subfolder_post(assigns(:post))
  end

So you know to change the path and the class name, but you forget to update the params key, because Rails did that for you. And the test passes!

The lesson here is to check the record really was changed:

 test "should update post" do
    id = Subfolder::Post.first.id
    put :update, :id => id, :post => { :text => 'modified' }
    assert_redirected_to subfolder_post(assigns(:post))
    assert_equal 'modified', Subfolder::Post.find(id).drum_text
  end

Check against different users

Another thing to test is that pages will work whether logged in or not. I have several pages that display a little differently depending on whether someone is logged in with a specific role - they see extra links or buttons to secure pages. I already had tests that check you have to be logged into those secure pages, but on the original pages, you do not need to be logged in, so no need to check that, right?

Except sometimes thing change, routes are removed or modified and links fail to work. But the tests do not catch that because I was only testing what a user who is not logged in sees.

The lesson here is to check any page with role-dependant output with both a user not logged in and as an admin with all roles.

Tuesday, 14 May 2013

redirect_to :back

Long time since my last post I know, but I am mainly cruising along on what I already know with not much new to write about.


Quick trick I discovered today.

  def destroy
    Post.find(params[:id]).destroy
    flash[:notice] = 'Post deleted.'
    redirect_to :back
  end

The redirect_to :back part will send the user back to the page he was on. Useful if a post can be deleted from the post index page or from a user's page, and you want to send back to the right place.

In your functional test you need to set request.env["HTTP_REFERER"], as this is what :back will look at. You can then check it went there. Any strng will do.

 test "should destroy post" do
    request.env["HTTP_REFERER"] = 'origin'
    assert_difference('Post.count', -1) do
      delete :destroy, :id => @post.id
    end
    assert_redirected_to 'origin'
  end