Rails TDD - RSpec Model Specs

Question Click to View Answer

Create a new Rails application called model_tdd. Add rspec_rails to the Gemfile. Install RSpec.

$ rails new model_tdd

# Gemfile
group :development, :test do
  gem "rspec-rails"
end

$ bundle exec rails generate rspec:install

Create a User model with an email attribute.

$ rails g model User email
$ rake db:migrate

Add a test to make sure that the email attribute is populated when a User record is created.

# spec/models/user_spec.rb
it "is invalid without an email" do
  expect(User.new(email: nil)).to have(1).errors_on(:email)
end

Notice that a User is instantiated, but not persisted in the database. It's faster to instantiate objects than create records in the database, so it's always preferable to instantiate objects in the test suite when possible.

Verify that the test is failing by running $ bundle exec rspec spec/models/user_spec.rb. Then write some code to make the test pass.

# app/models/user.rb
validates :email, presence: true

Write a test to make sure that the user's email is unique.

# app/models/user_spec.rb
it "duplicate emails are invalid" do
  User.create(email: 'bob@gmail.com')
  user = User.new(email: 'bob@gmail.com')
  expect(user).to have(1).errors_on(:email)
end

This test creates one user in the database with the database with the email bob@gmail.com, so when another user is instantiated with the same email, it will be invalid. In this test, one user needs to be created in the database, but the second user only needs to be instantiated.

Update the code to make the test pass.

# app/models/user.rb
validates :email, uniqueness: true

Create a Post model with title, link, and user_id attributes.

$ rails g model Post title link user_id:integer
$ rake db:migrate

Associate the Post and User models.

# app/models/user.rb
has_many :posts

# app/models/post.rb
belongs_to :user

Write a test that checks post titles are unique for a given user. A given user cannot create a post with the same link twice, but another user can create a post with that link.

# spec/models/post_spec.rb
it "doesn't allow a given user to create multiple posts with the same link" do
  user = User.create(email: 'bob@gmail.com')
  user.posts.create(link: 'http://codequizzes.wordpress.com/')
  post = user.posts.new(link: 'http://codequizzes.wordpress.com/')
  expect(post).to have(1).errors_on(:link)
end

Write code to make the test pass.

validates :link, uniqueness: { scope: :user_id }

Write a test to make sure the Post class has a short_link method that returns the host of a link. For the link 'http://codequizzes.wordpress.com/#something-thats-real', the short_link method should return 'codequizzes.wordpress.com'.

# spec/models/post_spec.rb
context "#short_link" do
  it "summarizes the link" do
    post = Post.new(link: 'http://codequizzes.wordpress.com/#something-thats-real')
    expect(post.short_link).to eq "codequizzes.wordpress.com"
  end
end

Write code to make the test pass.

# app/models/post.rb
def short_link
  URI.parse(link).host
end

Add a test to make sure that the Post link's format is valid. The link "blah blah blah" should be considered invalid.

# spec/models/post_spec.rb
it "doesn't allow links with invalid formats" do
  post = Post.new(link: "blah blah blah")
  expect(post).to have(1).errors_on(:link)
end

Make the test pass (hint: use the Ruby URI library).

# app/models/post.rb
validate :link_format

private

def link_format
  unless valid_link?
    errors.add(:link, "Invalid link format")
  end
end

def valid_link?
  begin
    !!URI.parse(link)
  rescue URI::InvalidURIError
    false
  end
end

Add a test to make sure the User model has a class method called by_letter that takes a letter and returns a sorted array of all users with an email that starts with that letter.

context ".by_letter" do
  it "returns a sorted array of emails that start with a certain letter" do
    bob = User.create(email: 'bob@gmail.com')
    frank = User.create(email: 'frank@gmail.com')
    bill = User.create(email: 'bill@gmail.com')
    expect(User.by_letter('b')).to eq [bill, bob]
  end
end

Write code to make the test pass.

# app/models/user.rb
def self.by_letter(letter)
  where("email LIKE ?", "#{letter}%").order(:email)
end