Gustav Ehrenborg fullstack developer

Email symbol LinkedIn logo GitHub logo Instagram logo

Ruby on Rails introduction An introduction to Rails for rookies

Friday, September 7, 2018
Rails command

Setup

Create a new project with the following line. The --api tells rails that we don't want views and the -T that we don't want the standard testing tool, called Minitest.

rails new library --api -T

Rails will create a folder structure with some boilerplate files and something called a Gemfile. It is the file that handles the dependencies that is needed. There's also a Gemfile.lock which specifies exactly which version of the dependencies that should be installed.

Since we removed the standard test framework, we must add another. Here we choose RSpec by adding these lines to the Gemfile:

 gem 'rspec-rails'

We continue by running 'bundle install', this command will install the added dependencies. RSpec then has a special command for initiating things: 'rails generate rspec:install', this is documented here: https://github.com/rspec/rspec-rails

Generation and migration

Rails has commands for generating boilerplate code:

rails generate model Book title author isbn page_count:integer # Gives a model, a migration and a spec file
rails generate resource Book title:string author isbn page_count:integer # Same as model, but also an empty controller and a route
rails generate scaffold Book title author isbn page_count:integer # Same as resource, but the controller includes methods

String is the default type for new attributes, only page_count need to has its type (integer) defined.

New rails projects comes preconfigured with a SQLite database, so run `rake db:migrate` to add the model to the database.

Oops, we forgot one thing. when working with books, it great to look them up by ISBN. Let's add an index to the ISBN field.

rails generate migration AddIndexToIsbn

This will create a new empty migration, called AddIndexToIsbn, prepended with todays date. Populate the file as follows:

class AddIndexToIsbn < ActiveRecord::Migration[5.2]
  def change
    add_index :books, :isbn
  end
end

Run 'rake db:migrate' to add the changes to the database.

Models and validation

Let's add stuff to our Book model:

class Book < ApplicationRecord
  validates :author, :title, :isbn, presence: true
  validates :isbn, uniqueness: true

  def to_s
    "#{title} by #{author}"
  end
end

The validates method might look weird on first site, and it is! First of all, in Ruby, it's alright to leave out the parenthesis on nearly all occasions. The validates method is also a shortcut method that calls other validator methods. For example, the first validates will call the presence validator on author, title and isbn. The second will call the uniqueness validator on the isbn.

The to_s method, defined above, is easier to explain. to_s means 'to string' and is a method that all objects has. We choose to overwrite the standard to_s here, and print the title of the book, followed by 'by' and then the authors name. String interpolation is made with "" och #{}.

Rails console

Here we have a killer feature. Enter 'rails console' in the terminal and you are given a shell! This shell is inside the rails application, which makes it really powerful. Here is some basic things you can do:

irb(main):009:0> 1+1
=> 2
irb(main):011:0> 'hello'.upcase
=> "HELLO"
irb(main):012:0> [3,4,2,5,6,7,4,5].sort
=> [2, 3, 4, 4, 5, 5, 6, 7]
irb(main):014:0> 'Hello ' << 'world'
=> "Hello world"

But as I told you, you are really inside of the application. That means you can play around with the newly created book model.

# Create a new book ('create' will save to the database directly)
irb(main):001:0> Book.create(author: 'Will Writer', title: 'My first book', isbn: '3345353-65', page_count: 129)
=> #<Book id: 1, title: "My first book", author: "Will Writer", isbn: "3345353-65", page_count: 129, created_at: "2020-11-19 12:16:25", updated_at: "2020-11-19 12:16:25">
# Count how many books that exists in the database
irb(main):002:0> Book.count
=> 1
# Fetch the first book and execute the to_s method
irb(main):003:0> Book.first.to_s
=> "My first book by Will Writer"
# Make a new book ('new' will not save to database)
irb(main):004:0> b = Book.new(author: 'T Typewriter', title: 'Murders in Mordor')
=> #<Book id: nil, title: "Murders in Mordor", author: "T Typewriter", isbn: nil, page_count: nil, created_at: nil, updated_at: nil>
# Check if it is valid
irb(main):005:0> b.valid?
=> false
# Add ISBN to make it valid (ISBN must be set and unique)
irb(main):006:0> b.isbn = '3252324-6523'
=> "3252324-6523"
irb(main):007:0> b.valid?
=> true
# Save it to the database
irb(main):008:0> b.save
=> true

It's possible to do pretty much anything with the rails console. Call services, fix things in production, trigger jobs and so on. It has really saved me a couple of times.

Testing

No application is complete without tests. The test framework we chose calls them specs, but it's all the same. The generation commands created a book_spec.rb, let's fill it with specs.

require 'rails_helper'

RSpec.describe Book, type: :model do
  it 'prints correct information on to_s' do
    b = Book.new(author: 'Penny Pen', title: 'Life is Short', isbn: '213465234-235434')
    expect(b.to_s).to eq("#{b.title} by #{b.author}")
  end

  it 'validates the presence of the title' do
    b = Book.new(author: 'Penny Pen', isbn: '213465234-235434')
    expect(b.valid?).to be_falsey
    expect(b.errors.messages[:title]).to eq(['can\'t be blank'])
  end
end

The first spec will check that the to_s method prints what we want. The second spec will make sure that a book without a title is not valid. Writing specs like the second one for every validation and attribute of the model would be a hassle, so let me introduce you to the gem shoulda-matchers. Add the following line to your Gemfile and run 'bundle install'.

gem 'shoulda-matchers'

Shoulda-matchers allows us to write shorthand tests to test validation. Before we can use it, we need to tell the testing framework to include it. In the rails_helper.rb, a file with test framework configuration, add the following lines at the bottom:

Shoulda::Matchers.configure do |config|
  config.integrate do |with|
    with.test_framework :rspec
    with.library :rails
  end
end

Now we can add these line to our book_spec.rb:

  it { should validate_presence_of(:title) }
  it { should validate_presence_of(:author) }
  it { should validate_presence_of(:isbn) }
  it { should validate_uniqueness_of(:isbn) }

Run the specs with the command 'rspec' in the terminal. Hopefully they all pass.

Two other gems that will facilitate testing is the Fabrication and Faker gems. Add the following to the Gemfile and then bundle install.

gem 'fabrication'
gem 'faker'

Create a folder spec/fabricators/. In it, create a file books_fabricator.rb and populate it like this:

require 'faker'

Fabricator(:book) do
  title { Faker::Book.title }
  author { Faker::Book.author }
  isbn { Faker::Code.isbn }
  page_count { rand(500) }
end

The Fabrication gem helps with dummy data creation. If you want a Book to use in a test, you can now write Fabricate(:book) to create a book and save it to the database. Fabricate.build(:book) to create without persisting. You can also do Fabricate.times(3, :book) to get an array with three books.

In the book_fabricator file, we specify what we want to include in our fabricated books. Here we use the Faker gem to fake more realistic data. The Faker gem has methods for book titles and authors, but there are so much more available too. Names, addresses, zipcodes, beer brands, buzz words, movie quotes and so on. You will never have to create a book called 'asfsdff' by author 'sfdsfsdfasdad' again.

We change the book spec to use the book fabricator:

  it 'prints correct information on to_s' do
    b = Fabricate.build(:book)
    expect(b.to_s).to eq("#{b.title} by #{b.author}")
  end

Controllers

A typical book controller would look like the following code.

class BooksController < ApplicationController
  def show
    book = Book.find(params[:id])
    render json: book
  end

  def index
    render json: Book.all
  end

  def create
    book = Book.new(book_params)
    if book.save
      render json: book, status: :created
    else
      render json: { error: :invalid_parameters, message: book.errors.messages, fields: book.errors.keys }, status: 422
    end
  end

  private

  def book_params
    params.require(:book).permit(:title, :author, :isbn)
  end
end

Show, index and create corresponds to the routes GET /books/:id, GET /books and POST /books if the following record is added to the routes.rb. The resource keyword automatically expands into the mentioned patterns, but routes can be specified individually too.

resources :books

Specs for the controller would look like this:

require 'rails_helper'

RSpec.describe BooksController, type: :controller do
  context 'get show' do
    before(:each) do
      @book = Fabricate(:book)
      get :show, params: { id: @book.id }
    end

    it 'has status code 200' do
      expect(response.code.to_i).to eq(200)
    end

    it 'returns book' do
      json = JSON.parse(response.body)
      expect(json['title']).to eq(@book.title)
      expect(json['author']).to eq(@book.author)
    end
  end

  context 'get index' do
    before(:each) do
      Fabricate.times(5, :book)
      get :index
    end

    it 'has status code 200' do
      expect(response.code.to_i).to eq(200)
    end

    it 'returns the books' do
      json = JSON.parse(response.body)
      expect(json.length).to eq(5)
    end
  end

  context 'post create' do
    before(:each) do
      @book_params = Fabricate.attributes_for(:book)
      post :create, params: { book: @book_params }
    end

    it 'has status code 201' do
      expect(response.code.to_i).to eq(201)
    end

    it 'returns book' do
      json = JSON.parse(response.body)
      expect(Book.all.count).to eq(1)
      expect(json['title']).to eq(@book_params['title'])
      expect(json['author']).to eq(@book_params['author'])
    end
  end
end