Building a Relational Connection Between Models in Rails Using TDD
In this guide we're going to implement a connection between two models in Rails and we're going to use TDD to build the feature.
Guide Tasks
  • Read Tutorial
  • Watch Guide Video
Video locked
This video is viewable to users with a Bottega Bootcamp license

In this guide we're going to implement a connection between two models in Rails and we're going to use TDD to build the feature.

For this guide, we need to implement some test helpers, and we are going to add them to our Rails helper file. Let's open the rails_helper.rb, and add these helpers:

# spec/rails_helper.rb

include Warden::Test::Helpers
Warden.test_mode!

We are using these helpers to mimic the process of a user signing in, as this is outside the scope of Capybara.

Now, open the /spec/features/post_spec.rb file. As you'll see, we already have the ability to visit a new post in our before block, so let's add a user to it and leverage the login_as method to log a user in.

# spec/features/post_spec.rb

describe 'creation' do
  before do
    user = User.create(email: "test@test.com", password: "asdfasdf", password_confirmation: "asdfasdf", first_name: "Jon", last_name: "Snow")
    login_as(user, :scope => :user)
    visit new_post_path
  end

  # Other methods not shown...
end

Essentially, before each new post, this code is going to log in a user and mimic a user session.

Next, we'll create a test to see if a user is associated with the post. The code for that is the following:

# spec/features/post_spec.rb

it 'will have a user associated it' do
  fill_in 'post[date]', with: Date.today
  fill_in 'post[rationale]', with: "User Association"
  click_on "Save"

  expect(User.last.posts.last.rationale).to eq("User Association")
end

Let's see what happens when you run rspec.

large

To get this test passing, open the models/post.rb file, and add a belongs_to call, like this:

# app/models/post.rb

class Post < ActiveRecord::Base
  belongs_to :user
  validates_presence_of :date, :rationale
end

Now, go to user.rb, and update the class definition:

# app/models/user.rb

class User < ActiveRecord::Base
  has_many :posts
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :trackable, :validatable

  validates_presence_of :first_name, :last_name
end

All we're doing here is adding the code has_many :posts right below where we're declaring the class.

If you run rspec, it'll say that a column called posts.user_id does not exist. Let's take a look at our database schema. In the posts table, we need a new column called user_id. To make this change, first run:

bundle exec rake db:setup

To clear all the bad data from the database. Then, run:

rails g migration add_users_to_posts user:references

This code will not only create a user_id in the posts table, but also an index and foreign key relation.

To create the column in database, run the command:

bundle exec rake db:migrate

Now, if you open the db/schema.rb file, you should see the user_id column.

large

If you run rspec, it just says that there is an undefined method called rationale.

large

To fix this, update the posts_controller.rb file. Since we are using devise, we have access to a method called current_user. When a user is logged in, we have their details stored inside a cookie. So, all that we have to do is pass the id of the current user to the user_id column of post.

# app/controllers/posts_controller.rb

def create
  @post = Post.new(post_params)
  @post.user_id = current_user.id # this is the new line added

  if @post.save
    redirect_to @post, notice: 'Your post was created successfully'
  else
    render :new
  end
end

Run rspec now, and everything is working fine.

Let's also check if it works in the browser. Open the url localhost:3000/posts/new, enter the values and click on save. This throws an error.

large

This is because we haven't signed in yet. So, let's register again. Open the url localhost:3000/users/sign_up and sign up.

If you click the sign up button, it'll throw another error because first name and last name were made mandatory in our code, but there are no fields in the form. You can actually fix this in the terminal. Open the rails console, and create a user manually like this:

User.create!(first_name: "Jordan", last_name: "Hudgens", email: "asdf@asdf.com", password: "asdfasdf", password_confirmation: "asdfasdf")

Next, restart the server and open the localhost:3000/users/sign_in page. Enter the email and password we created in the console and log in. Now, go to localhost:3000/posts/new and create a new post. This should bring up the below screen:

large

As you'll see, a user_id is printed for you. So, everything is working properly.

To prevent this error in the future, let's open our application_controller.rb file, and have a before_action for authentication, like this:

# app/controllers/application_controller.rb

class ApplicationController < ActionController::Base
  # Prevent CSRF attacks by raising an exception.
  # For APIs, you may want to use :null_session instead.
  protect_from_forgery with: :exception
  before_action :authenticate_user!
end

Now, if a user tries to access any page without signing in, it will redirect them to the sign in page which is exactly what you want in an enterprise application.

Resources