Testing a Devise OmniAuth view with RSpec

So, in the process of building FANZO’s latest experiment I’ve been creating a new web application with Rails.  This is my first foray into Ruby and Rails, so I’ve been bouncing all over google and stack overflow picking up the bits and pieces necessary to duck tape the first experiment together.

After working through Michael Hartl’s excellent tutorial on building a simple twitter-like app, I started off by putting together a simple rails app.  I then looked into how to integrate user authentication and application authorization into the registration workflow.  Devise has a nice integration with OmniAuth which I thought would speed this development flow up, so I installed those gems and I was off.

Once I got into the details, however, I find a dearth of information about how to effectively test views that are generated with Devise.  Turns out there are a bunch of steps, which I pieced together from various stack overflow questions and blog posts, but I didn’t see the last mile.  So I thought I’d blog about what I ended up with, in the hopes that someone with either benefit from it or tell me that I’m doing it all wrong…:)

So, why am I testing a view?  Because I have some logic in it to handle the case where a new users authenticates through twitter, but I still want to capture their email address and allow them to set their own password (I will generate and email the user a password if they come through facebook, then they can reset it at their convenience). So, without further ado, here is my spec for the view for the new action that registers a new user:

describe "devise/registrations/new" do
  before do
    view.should_receive(:resource).at_least(1).times.and_return(User.new)
    view.should_receive(:resource_name).at_least(1).times.and_return("user")
  end

  it "renders default look for completely new user" do
    view.should_receive(:devise_mapping).at_least(1).times.and_return(Devise.mappings[:user])
    view.should_receive(:resource_class).at_least(1).times.and_return(Devise.mappings[:user].to)
    render
    view.should render_template( partial: "_new_user" )
    view.should_not render_template( partial: "_twitter_authed_new_user")
  end

  it "should show post twitter auth fields" do
    OmniAuth.config.mock_auth[:twitter] = OmniAuth::AuthHash.new(
       { uid: '12345',
        info: { nickname: "wilma" },
       extra: { access_token: { token: "a token", secret: "a secret"} }
       })

    session["devise.twitter_data"] = OmniAuth.config.mock_auth[:twitter]
    render

    view.should_not render_template( partial: "_new_user")
    view.should render_template( partial: "_twitter_authed_new_user" )
    rendered.should have_selector("#user_twitter_user_id", value: '12345')
    rendered.should have_selector("#user_twitter_user_token", value: 'a token')
    rendered.should have_selector("#user_twitter_user_secret", value: 'a secret')
    rendered.should have_selector("#user_twitter_username", value: 'wilma')
  end

end

There are a couple key pieces that took a while to figure out how to include:

How to properly create the mock auth hash. When using the mock, valuable not only in view specs, but in controller and request specs, it needs to have the type OmniAuth::AuthHash or you will get a bunch of undefined method errors.

How to set the environment up with appropriate mocks so that the form in the view will properly execute without a bunch of undefined objects. This code:

view.should_receive(:resource).at_least(1).times.and_return(User.new)
view.should_receive(:resource_name).at_least(1).times.and_return("user")

is necessary to render the form. This code:

view.should_receive(:devise_mapping).at_least(1).times.and_return(Devise.mappings[:user])
view.should_receive(:resource_class).at_least(1).times.and_return(Devise.mappings[:user].to)

is necessary to render the links partial, which devise creates by default.

The view code that makes this pass looks like this:

<% if session["devise.twitter_data"] %>
<%= render "twitter_authed_new_user" %>
<% else %>
<%= render "new_user" %>
<% end %>

The new_user partial looks like this:

<h2>Sign up</h2>

<%= form_for(resource, :as => resource_name, :url => registration_path(resource_name)) do |f| %>
  <%= devise_error_messages! %>

  <div><%= f.label :email %><br />
  <%= f.email_field :email %></div>

  <div><%= f.label :password %><br />
  <%= f.password_field :password %></div>

  <div><%= f.label :password_confirmation %><br />
  <%= f.password_field :password_confirmation %></div>

  <div><%= f.submit "Sign up", id:'commit' %></div>
<% end %>

<%= render "devise/links" %>

and finally the twitter_authed_new partial:

<h1>Almost there</h1>

<% resource.twitter_user_id = session["devise.twitter_data"].uid %>
<% resource.twitter_user_token = session["devise.twitter_data"].extra.access_token.token %>
<% resource.twitter_user_secret = session["devise.twitter_data"].extra.access_token.secret %>
<% resource.twitter_username = session["devise.twitter_data"].info.nickname %>

<%= form_for(resource, :as => resource_name, :url => registration_path(resource_name)) do |f| %>
  <%= devise_error_messages! %>

  <div id="hidden_twitter_fields">
      <%= f.hidden_field :twitter_user_id %>
      <%= f.hidden_field :twitter_user_token %>
      <%= f.hidden_field :twitter_user_secret %>
      <%= f.hidden_field :twitter_username %>
  </div>

  <div><%= f.label :email %><br />
  <%= f.email_field :email %></div>

  <div><%= f.label :password %><br />
  <%= f.password_field :password %></div>

  <div><%= f.label :password_confirmation %><br />
  <%= f.password_field :password_confirmation %></div>

  <div><%= f.submit "Complete", id: 'commit' %></div>
<% end %>

And for those interested, here are the relevant cucumber tests that drove the feature:

Feature: Creating Accounts

  Scenario: Create account via email and password
    Given a user visits the registration page
    When the user submits valid email and password
    Then he should see his profile page
      And he should see a signout link

  Scenario: Create account via facebook
    Given a user visits the registration page
    When he clicks the facebook link
    Then he should see his profile page
      And he should see a signout link
      And his facebook data should be stored in the DB

  Scenario: Create account via twitter
    Given a user visits the registration page
    When he clicks the twitter link
    Then he should see almost there page
      And there should be hidden twitter data
    When the user submits valid email and password
    Then he should see his profile page
      And his twitter data should be stored in the DB

Enjoy!

About Paul

Startup founder, father of 3 girls, Notre Dame alumnus, agile practitioner and tech enthusiast. I love building great companies to solve cool problems.
This entry was posted in Devise, OmniAuth, RSpec, Ruby Gems, Ruby on Rails, TDD and tagged , , , , , . Bookmark the permalink.

One Response to Testing a Devise OmniAuth view with RSpec

  1. Sharon says:

    cool!

Leave a Reply

Your email address will not be published. Required fields are marked *