More CRUD with Rails Scaffolds

Part 4 Genres

In the last post we removed the database cleaner gem and converted our feature test into a new and improved systems test. In this post we will return to the Rails generate scaffold command to build full CRUD functionality for our genres model of our Bookshelf App. We will also create some scopes and methods on our new genre model to facilitate future reporting of reading metrics by genre.

Our genre model will be very basic. The long term purpose of this model is to be able to get metrics by genre of all the books in our book shelf app. For example, I would like to easily be able to see how many history books I read last month or how many scifi books I read since the start of the year. To do so, our model will need a name attribute.

We will also want a boolean flag on genre that tells us if the genre is fiction or nonfiction.

Rails Scaffold

Let's run the following command and let Rails do its thing.

$ rails g scaffold genre name:string fiction:boolean --no-styelsheets --no-assets --no-scaffold-stylesheet --no-controller-specs --no-view-specs --no-helper-specs --no-helper --no-javascripts --no-jbuilder

It almost feels like cheating!

Migration

The rails scaffold gives us the following migration.

class CreateGenres < ActiveRecord::Migration[5.1]
  def change
    create_table :genres do |t|
      t.string :name
      t.boolean :fiction

      t.timestamps
    end
  end
end

Before we run this migration, let's add a default value to our fiction attribute. By default genres should be fiction: false or nonfiction. To do so, all we need to do is set the default in the create_table block of our migration.

class CreateGenres < ActiveRecord::Migration[5.1]
  def change
    create_table :genres do |t|
      t.string :name
      t.boolean :fiction, default: false

      t.timestamps
    end
  end
end

System Test

Next let's create a system test file:

$ touch spec/system/genre_spec.rb

We want to test create, read, update, and delete functionality so let's write some helper methods along the lines of using the web interface create a new genre, view all genres, edit a genre, and delete a genre.

# spec/system/genre_spec.rb
require 'rails_helper'

describe 'genre crud', type: :system do
  scenario 'using web interface' do
    create_new_genre
    view_all_genres
    edit_genre
    delete_genre
  end

  def create_new_genre
    visit new_genre_path
    fill_in 'Name', with: 'History'
    click_button 'Create Genre'

    expect(page).to have_content 'History'
    expect(page).to have_content 'Fiction: false'
  end

  def view_all_genres
    create :genre, name: 'Philosophy'
    visit genres_path
    expect(page).to have_css 'tr', count: 3
    expect(page).to have_content 'History'
    expect(page).to have_content 'Philosophy'
  end

  def edit_genre
    visit edit_genre_path(Genre.first)
    fill_in 'Name', with: 'Historical Fiction'
    check 'Fiction'
    click_button 'Update Genre'

    expect(page).to have_content 'Historical Fiction'
    expect(page).to have_content 'Fiction: true'
  end

  def delete_genre
    visit genres_path
    first(:link, 'Destroy').click
    expect(page).to have_css 'tr', count: 2
  end
end

The above system tests takes us through all four crud actions. We first create a 'History' genre. Next we visit the genre index to verify that we can see all genres in the database. Next we test editing a genre by changing the name to Historical Fiction and setting fiction equal to true by checking the fiction checkbox. Finally we delete a genre.

We have a pending migration to run since we took advantage of Rail scaffolds to add the genre model to our app.

$ rake db:migrate db:test:prepare

Running Rspec shows that all tests are passing. That's usually a red flag for me, but if we walk through the app in a browser we can confirm that all the functionality is there. First start our server by typing:

$ rails s

Sure enough we have a genre form:

Screen Shot 2017-10-28 at 4.41.25 PM.png

We also have a show page for our genres:

Screen Shot 2017-10-28 at 4.41.35 PM.png

We can edit a genre:

Screen Shot 2017-10-28 at 4.41.55 PM.png

We can see a list of all of our genres:

Screen Shot 2017-10-28 at 4.42.25 PM.png

And we can delete a genre by clicking the Destroy link

 

Validations

Just like with our author model, we'll want to add some database validations to make sure that we do not end up with duplicate genres or blank genres without names.

# spec/models/genre_spec.rb
require 'rails_helper'

RSpec.describe Genre, type: :model do
  describe "validations" do
    it { should validate_presence_of(:name) }
    it { should validate_uniqueness_of(:name) }
  end
end

Again, we will use the shoulda_matcher gem to validate that the genre name must exist and be unique. Running the above tests will give us:

To implement these validations on our model we will add the following code to our genre.rb file.

# models/genre.rb
class Genre < ApplicationRecord
  validates_presence_of :name
  validates_uniqueness_of :name
end

Fiction vs. NonfictION

It would also be nice to be able to ask a genre if it is fiction or nonfiction and have it return a boolean answer. Let's write tests for fiction? and nonfiction? methods on our genre model.

describe "#fiction?" do
    context "when a genre is fictional" do
      let(:genre) { create :genre, fiction: true }

      it "returns true" do
        expect(genre.fiction?).to eq true
      end
    end

    context "when a genre is nonfiction" do
      let(:genre) { create :genre, fiction: false }

      it "returns true" do
        expect(genre.fiction?).to eq false
      end
    end
  end

  describe "#nonfiction?" do
    context "when a genre is fictional" do
      let(:genre) { create :genre, fiction: true }

      it "returns true" do
        expect(genre.nonfiction?).to eq false
      end
    end

    context "when a genre is nonfiction" do
      let(:genre) { create :genre, fiction: false }

      it "returns true" do
        expect(genre.nonfiction?).to eq true
      end
    end
  end

Our tests expect genre to have a fiction? and nonfiction? method which return true or false if the genre being questioned falls into their respective categories. We set up a fiction genre by creating a genre with factory bot and setting fiction to be true. We set fiction to be false for our non fiction genre. The name of the genre does not matter for our purpose here.

Running our tests now will give us four undefined method errors for the four tests we added. We can go ahead and define these methods in our genre file:

Screen Shot 2017-10-28 at 3.04.44 PM.png
# models/genre.rb
class Genre < ApplicationRecord
  validates_presence_of :name
  validates_uniqueness_of :name

  def fiction?

  end

  def nonfiction?

  end
end

These methods all return nil when we run our tests:

Screen Shot 2017-10-28 at 3.26.19 PM.png

To implement our fiction? and nonfiction? methods we just have to invoke and negate respectively the fiction attribute of the genre model, which is already a boolean.

# models/genre.rb
class Genre < ApplicationRecord
  validates_presence_of :name
  validates_uniqueness_of :name

  def fiction?
    fiction
  end

  def nonfiction?
    !fiction
  end
end

Scopes

If we imagine wanting to review reading metrics grouped by fiction and non fiction genres, we can move forward and implement these categories as scopes on the genre model. The goal here is to be able to say Genre.fiction or Genre.nonfiction and get all the fiction and nonfiction genres back respectively. Let's start with a test:

describe "Scopes" do
  let!(:fiction) { create :genre, name: 'Science Fiction', fiction: true }
  let!(:nonfiction) { create :genre, name: 'Biology', fiction: false }

  describe "fiction" do
    let(:result) { described_class.fiction }

    it "returns all fictional genres" do
      expect(result.count).to eq 1
      expect(result.first).to eq fiction
    end
  end

  describe "nonfiction" do
    let(:result) { described_class.nonfiction }
    
    it "returns all nonfiction genres" do
      expect(result.count).to eq 1
      expect(result.first).to eq nonfiction
    end
  end
end

In our test we will create two genres, one fiction and the other non fiction. Notice that we are using let! (bang), which forces this creation before each test instead of lazily instantiating these variables. The keyword let without the bang lazily instantiates the variable once it is called later on in the test.

Next, we expect that when we call fiction on our described_class, Genre we get back a Genre::ActiveRecord_Relation with a count of one. When we explore the only genre returned in our relation (.first) we expect it to equal the fiction genre in the case of the fiction scope test and the nonfiction genre in the case of the nonfiction scope test.

When we run this test we will get two undefined method errors because the class Genre does not have a method fiction or nonfiction at this point.

We can use some Rails syntactic sugar to create our scopes by adding the following:

scope :fiction, -> { where(fiction: true) }
scope :nonfiction, -> { where(fiction: false) }

Which is shorthand for:

def self.fiction
  where(fiction: true)
end

def self.nonfiction
  where(fiction: false)
end

Either implementation will pass our tests. The scope syntax is nice because it becomes easy to group scopes like these at the top of a model file for easy reference in the future.

You can start from this point of the live blog journey by cloning / forking this branch of my bookshelf repo.

Next we'll be creating books for our bookshelf. Thanks for reading!