Rails Integration Testing - How to learn

30 Jul 2006

I decided to try filling up my ./test/integration folder today because it was empty and I felt I must have been missing out. I can’t stand the idea that all the other Rails devs are enjoying some feature I haven’t found out about yet.

Integration tests seem pretty handy because functional tests are pretty basic - effectively unit tests for controllers rather than tests of real interactivity. So I figured I’d google around a bit to discover this integration testing thing and this is what I found:

Rails Source regarding integration tests

The original announcement on the rails weblog

Jamis Buck’s personal blog (which currently doesn’t have a web server so I had to grab the page from Google’s cache).

That’s it. Those three meager pages are all the info I could find on Rails integration testing. In fact, I have reason to believe that the Rails Recipes book just re-used the code from Jamis Buck’s post - so even they don’t have any more for us.

Well, that’s not acceptable. I’m trying to follow a particular user’s experience through my app and I want to get this working without having to resort to SeleniumOnRails or some other browser-emulator.

My code so far

I’ve got a particular user type Athlete that logs in and checks their profile. They update a couple pieces of information and we check that the info was saved. Should be easy. (note: to understand this code check out Jamis’s article - you might need to look in the Google Cache for it)

    class AthleteSessionTest < ActionController::IntegrationTest

      fixtures  :users,
                :athlete_contacts,
                :coach_contacts,
                :junior_colleges,
                :combines


      def test_randy_visits
        randy = new_session_as('Cliffton Player')
        randy.goes_to_profile_edit
        randy.updates_profile_with :first => 'Randy', :last => 'BadAsserson'
        randy.
      end

      private

        module AthleteSessionDSL

          def goes_to(place)
            get place
          end

          def goes_to_login
            get '/auth/login'
            assert_response :success
            assert_template 'auth/login'
          end

          def goes_to_profile_edit
            get '/athletes/profile_edit'
            assert_response :success
            assert_template 'athletes/profile_edit'
          end

          def logs_in_as(athlete)
            @current_user = users(athlete)
            post 'auth/login', :user => {:email => @current_user.email, :password => @current_user.password }
            assert_response :redirect
            follow_redirect!
            assert_template 'athletes/profile'
          end

          def updates_profile_with(contact)
            post 'athletes/update_contact', :contact => contact,
                                            :athlete_id => @current_user.id
            assert_flash_equal 'This profile has been updated with new information', :success
            assert_response :redirect
            follow_redirect!
            assert_template 'athletes/profile'
            contact.each { |k,v| assert_equal v, @current_user.profile.attributes[k.to_s] }
            end
          end

        end

        def new_session
          open_session do |sess|
            sess.extend(AthleteSessionDSL)
            yield sess if block_given?
          end
        end

        def new_session_as(person)
          new_session do |sess|
            sess.goes_to_login
            sess.logs_in_as(person)
            yield sess if block_given?
          end
        end

    end

The Problem

So this is fine. So far it’s working great. The problem is that I’ve set it up so that everything my user does is part of my AthleteSessionDSL module in it’s own method. Everything randy does has to be a method within the AthleteSessionDSL module.

I wanted to try some non-modular navigation to give Randy a bit more of a free spirit. I thought I’d see if I could take some of the functionality out of the module and do it directly:

    def test_randy_visits
      randy = new_session_as('Cliffton Player')
      randy.goes_to_profile_edit
      randy.updates_profile_with :first => 'Randy', :last => 'BadAsserson'
      # trying some direct session navigation outside of the randy session object
      get 'athletes/profile'
      assert_response :success
      assert_template 'athletes/profile'
    end

What happened? Well, since randy is really just an instance of a Rails session with some handy methods attached to it I immediately left the session when I stopped using randy. My request to get the “athletes/profile” page resulted in a redirect to my login page because that method is authentication protected.

Well, what’s the solution? Do I have to make a million methods for everything I want Randy to do?

The Solution

It turns out to be quite simple. The methods we have been using like assert_response and get (and all the rest) are, in fact, methods of the session. The open_session call in the new_session module creates a session object with all the standard methods on it.

So not only can we group functionality into methods and roll those into included modules - we can also call things directly:

    def test_randy_visits
      randy = new_session_as('Cliffton Player')
      randy.goes_to_profile_edit
      randy.updates_profile_with :first => 'Randy', :last => 'BadAsserson'
      randy.get '/athletes/profile'
      randy.assert_response :success
      randy.assert_template 'athletes/profile'
    end
  • MG said: i really like this style of programming, seems very readable, maybe i try with my next project, keep up the good work :)

Please if you found this post helpful or have questions.