Alex S
Alex S

Reputation: 105

Rspec & Capybara: ActionView::Template::Error: undefined method `' for nil:NilClass

I know the ActionView::Template::Error: undefined method [] for nil:NilClass is a common error but none of the other pages seem to to answer my issue. I have a very simple test that logs in, goes to the new project page and makes a new project by filling in the form. But When I log in and rails loads the dashboard in Rspec / Capybara does not read the page correctly and the page fails before we even test the form.

Rspec / Capybara does not seem to know what class project and thinks its a NilClass, though it has no issue with the devise form on the login page.

Here is the spec file:

require 'rails_helper'

describe "Creating a new project" do

  let!(:client) { Client.create(name: "abc", code: "abc") }
  let!(:project) { Project.create(name: "abc", client: client) }
  let!(:user) { User.create(email: "[email protected]", password: "123123123") }

  # Sign In with user
  before :each do
    visit root_url
    fill_in 'user[email]', with: '[email protected]'
    fill_in 'user[password]', with: '123123123'
    find('input[name="commit"]').click
  end

  it "saves new project and returns to list of projects on root" do
    visit root_url
    click_on 'New Project'
    expect(current_path).to eq(new_project_path)

    fill_in "Name", with: "My New Cool Project"
    fill_in "Details", with: "Praise the sun!"
    select "urgency_fire", :from => "Urgency"
    select "status_in_progress", :from => "Status"
    select "ABC - abc", :from => "Client"
    fill_in "Due date", with: Time.now.to_s
    find('input[name="commit"]').click

    expect(current_path).to eq(root_path)
    expect(page).to have_text("My New Cool Project")

  end
end

Here is the test output:

1) Creating a new project saves new project and returns to list of projects on root
 Failure/Error: %td= project.client.code

 ActionView::Template::Error:
   undefined method `code' for nil:NilClass
 # ./app/views/dashboard/index.html.haml:17:in `block in _app_views_dashboard_index_html_haml__3165971454273894605_70112485977840'
 # ./app/views/dashboard/index.html.haml:15:in `_app_views_dashboard_index_html_haml__3165971454273894605_70112485977840'
 # ./spec/features/new_project_form_spec.rb:14:in `block (2 levels) in <top (required)>'
 # ------------------
 # --- Caused by: ---
 # NoMethodError:
 #   undefined method `code' for nil:NilClass
 #   ./app/views/dashboard/index.html.haml:17:in `block in _app_views_dashboard_index_html_haml__3165971454273894605_70112485977840'

Here is the dashboard controller:

class DashboardController < ApplicationController
  before_action :authenticate_user!
  def index
     @projects = Project.includes(:client).all
  end
end

And here is dashboard html (haml):

.container
  .row
    .col-sm-12
      %p= link_to 'New Project', new_project_path, title: 'New Project'
        %table
          %thead
            %tr
              %th Client
              %th Project   
              %th Urgency   
              %th Status    
              %th Due Date  
              %th Assigned To   
          %tbody
          - @projects.each do |project|
            %tr
              %td= project.client.code
              %td= link_to project.name, project
              %td= project.urgency 
              %td= project.status 
              %td= project.due_date 
              %td= project.user.count

And I have the route set up so if your not logged in you see the log in page, and if you are logged in you see the dashboard:

Rails.application.routes.draw do

    # devise gem routes
    devise_for :users

    # Dashboard
    authenticated :user do
        root :to => 'dashboard#index'
    end

    # Login
    devise_scope :user do
        root to: "devise/sessions#new"
    end

    # Projects
    resources :projects

    # Send all unknown pages to root
    if ENV["RAILS_ENV"] == "production"
        get '*path' => redirect('/')
    end

end

Upvotes: 2

Views: 1826

Answers (2)

Thomas Walpole
Thomas Walpole

Reputation: 49910

This has nothing to do with Capybara not reading the page correctly, rather, would indicate that you have a Project object but there is no client associated with it. Most likely reason, given your code, is failing model validation when attempting to create the Client object. You should switch to create! since that will raise if the create fails, immediately informing you of an issue, whereas create will just return an unsaved object. Additionally check your test.log for warnings about validation failures when creating your Client and Project.

You have a couple of other issues in your test that you should fix to avoid flaky tests going forward.

  1. Your before block should have an assertion at the end to make sure the login has completed before the next visit is called. Something like

    expect(page).to have_content('You are now logged in!')
    

    or

    expect(page).to have_current_path('whatever path the user should be directed to on successful login')
    
  2. Never use the eq matcher with current_path. Once you move to a JS capable driver using eq with current_path will lead to all sorts of test flakiness. Instead use the Capybara provide have_current_path matcher which has waiting/retrying behavior built-in. So instead of expect(current_path).to eq(new_project_path) you should do

    expect(page).to have_current_path(new_project_path)
    

Upvotes: 1

Greg Tarsa
Greg Tarsa

Reputation: 1642

The error is suggesting that there there is no project object. You are creating that with your let! calls. The let! variables are being collected for execution within the example blocks. It is a little tricky to use them the way you are as they may not be instantiated until an example block starts and you are explicitly before :each example with this code. However, you don't have to use let in this case. Since you do not reference the values in the tests you can try something like:

# Given an existing project and logged-in user
before :each do
  project = Project.create(name: 'abc',
    client: Client.create(name: "abc", code: "abc"),
    code: 'abc')
  user = User.create(email: "[email protected]", password: "123123123")

  visit root_url
  fill_in 'user[email]', with: '[email protected]'
  fill_in 'user[password]', with: '123123123'
  find('input[name="commit"]').click
end

From the docs: Use let to define a memoized helper method. The value will be cached across multiple calls in the same example but not across examples.

Upvotes: 2

Related Questions