An in-depth look at unit testing Rails models with RSpec with TDD.

This episode covers:

  • Creating a Console model (0:45)
  • Testing model validations (3:28)
  • Testing model methods (12:17)
  • Testing model scopes (15:12)
  • Refactoring the consoles array of hashes into database queries (20:12)

View the code.

View the diff.

View the playlist.

Key code from the episode:

# db/migrate/20171111171300_create_consoles.rb
class CreateConsoles < ActiveRecord::Migration[5.1]
  def change
    create_table :consoles do |t|
      t.string :name, null: false
      t.string :manufacturer, null: false

      t.timestamps
    end
  end
end
# app/models/console.rb
class Console < ActiveRecord::Base
  validates :name, presence: true
  validates :manufacturer, presence: true

  scope :nintendo, -> { where(manufacturer: 'Nintendo') }

  def formatted_name
    "#{manufacturer} #{name}"
  end
end
# spec/models/console_spec.rb
require 'rails_helper'

RSpec.describe Console do
  subject { described_class.new(manufacturer: 'Nintendo', name: 'Wii') }

  describe 'validations' do
    describe 'name' do
      it 'must be present' do
        expect(subject).to be_valid
        subject.name = nil
        expect(subject).to_not be_valid
      end
    end

    describe 'manufacturer' do
      it 'must be present' do
        expect(subject).to be_valid
        subject.manufacturer = nil
        expect(subject).to_not be_valid
      end
    end
  end

  describe '#formatted_name' do
    it 'returns the manufacturer and name in a string' do
      expect(subject.formatted_name).to eql('Nintendo Wii')
    end
  end

  describe '.nintendo' do
    it 'returns an ActiveRecord::Relation of consoles manufacturered by Nintendo' do
      wii = described_class.create(manufacturer: 'Nintendo', name: 'Wii')
      switch = described_class.create(manufacturer: 'Nintendo', name: 'Switch')
      described_class.create(manufacturer: 'Sony', name: 'PS4')

      expect(described_class.nintendo).to contain_exactly(
        wii,
        switch
      )
    end
  end
end
# app/controllers/consoles_controller.rb
class ConsolesController < ApplicationController
  def index
    if params[:manufacturer].present?
      consoles = Console.where(manufacturer: params[:manufacturer])
    else
      consoles = Console.all
    end

    render(json: { 'consoles' => consoles.map(&:formatted_name) })
  end
end
# spec/requests/consoles_spec.rb
require 'rails_helper'

RSpec.describe 'Consoles requests' do
  before do
    Console.create(name: 'NES', manufacturer: 'Nintendo')
    Console.create(name: 'SNES', manufacturer: 'Nintendo')
    Console.create(name: 'Wii', manufacturer: 'Nintendo')
    Console.create(name: 'Genesis', manufacturer: 'Sega')
    Console.create(name: 'Xbox', manufacturer: 'Microsoft')
    Console.create(name: 'Switch', manufacturer: 'Nintendo')
    Console.create(name: 'PS1', manufacturer: 'Sony')
    Console.create(name: 'PS2', manufacturer: 'Sony')
  end

  describe 'GET /consoles' do
    it 'returns an array of the consoles' do
      get('/consoles')
      expect(response_json['consoles']).to contain_exactly(
        'Nintendo NES',
        'Nintendo SNES',
        'Nintendo Wii',
        'Sega Genesis',
        'Microsoft Xbox',
        'Nintendo Switch',
        'Sony PS1',
        'Sony PS2'
      )
    end

    it 'supports specifying consoles for a specific manufacturer' do
      get('/consoles', params: { manufacturer: 'Nintendo' })
      expect(response_json['consoles']).to contain_exactly(
        'Nintendo NES',
        'Nintendo SNES',
        'Nintendo Wii',
        'Nintendo Switch'
      )
    end
  end
end

Software used:

  • Ruby 2.4.2
  • RSpec 3.6.1
  • Rails 5.1.4
  • Mac OS
  • iTerm 2
  • Vim
  • Tmux