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:

We also have a show page for our genres:

We can edit a genre:

We can see a list of all of our genres:

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:

# 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:

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!