Learn the First Best Practices for Rails and RSpec

Khi chúng ta code Sample App hoặc code 1 dự án for fun 1 2 thành viên chúng ta rất hay bỏ qua bước viết test vì chúng ta cảm thấy project của chúng ta chẳng thêm mới được gì hay thậm chí tự cảm thấy mình xử lý logic chuẩn không cần chỉnh nữa rồi. Nhưng khi tham gia 1 dự án nghiêm túc với khách hàng và hoạt động theo team đông người thì việc viết test là vô cùng quan trọng để giảm thiểu tối đa cá thiếu và sai sót trong logic xử lý. Sau đây mình sẽ trình bày cách viết Rspec đơn giản cho một ứng dụng Ruby on Rails

Rails Application

rails new myapp

Installing RSpec

Chúng ta sẽ thêm gem rspec-rails trong group developmenttest trong Gemfile

group :development, :test do
  gem 'byebug'
  gem 'rspec-rails', '~> 3.4'
end

chạy bundle:

bundle install

Và generate rspec:

rails generate rspec:install

Câu lệnh trên sẽ tạo ra 1 vài file như sau:

Shoulda-Matchers and Database Cleaner

Chúng ta sẽ thêm 2 gem shoulda-matcherdatabase_cleaner tại group test trong Gemfile:

group :test do
  gem 'shoulda-matchers', '~> 3.0', require: false
  gem 'database_cleaner', '~> 1.5'
end

Sau đó chạy bundle install

Shoulda-Matchers Configuration

Shoulda Matchers cung cấp cách viết 1 dòng lệnh Rspec dùng để test các chức năng của Rails. Nó giúp bạn viết test 1 cách ngắn gọn, rõ ràng và ít gặp lỗi hơn
Chúng ta sẽ thêm những dòng sau vào spec/rails_helper.rb

require 'shoulda/matchers'

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

Database Cleaner Configuration

Database Cleaner giúp bạn làm sạch database giữa các lần chạy thử. Chúng ta config như sau: spec/rails_helper.rb:

config.use_transactional_fixtures = false

Tạo thư mục support trong thư mục spec

mkdir spec/support

Trong đó tạo 1 file mới tên database_cleaner.rb và thêm nhưng dòng sau:

RSpec.configure do |config|

  config.before(:suite) do
    DatabaseCleaner.clean_with(:truncation)
  end

  config.before(:each) do
    DatabaseCleaner.strategy = :transaction
  end

  config.before(:each, :js => true) do
    DatabaseCleaner.strategy = :truncation
  end

  config.before(:each) do
    DatabaseCleaner.start
  end

  config.after(:each) do
    DatabaseCleaner.clean
  end
end

Vậy là chúng ta đã config xong việc clean database giữa các lần test

Capybara Setup

Capybara là một khung tự động hóa được sử dụng để tạo các thử nghiệm chức năng, mô phỏng cách người dùng sẽ tương tác với ứng dụng của bạn.
Thêm gem capybara vào group :development, :test trong Gemfile, cùng group với gem rspec-rails

gem 'capybara', '~> 2.5'

và chạy bundle install Sau đó vào spec_helper.rb require gem capybara

Faker and Factory Girl Setup

Faker rất hữu ích trong việc tạo dữ liệu ngẫu nhiên cho thử nghiệm của bạn. Thêm gem faker vào group :test trong Gemfile

gem 'faker', '~> 1.6.1'

Factory Girl cho phép bạn tạo các đối tượng mà bạn cần trong các thử nghiệm có thể bao gồm các giá trị mặc định. Với faker, bạn sẽ có thể tạo các đối tượng ngẫu nhiên cho thử nghiệm của mình thay vì chỉ sử dụng một giá trị mặc định.
Thêm gem factory_girl_rails trong group :development, :test

gem 'factory_girl_rails', '~> 4.5.0'

Sau đó bundle install Gemfile của bạn sẽ trông dư này:

source 'https://rubygems.org'


gem 'rails', '4.2.4'
gem 'sqlite3'
gem 'sass-rails', '~> 5.0'
gem 'uglifier', '>= 1.3.0'
gem 'coffee-rails', '~> 4.1.0'
gem 'jquery-rails'
gem 'turbolinks'
gem 'jbuilder', '~> 2.0'
gem 'sdoc', '~> 0.4.0', group: :doc

group :development, :test do
  gem 'byebug'
  gem 'rspec-rails', '~> 3.4'
  gem 'factory_girl_rails', '~> 4.5'
  gem 'capybara', '~> 2.5'
end

group :development do
  gem 'web-console', '~> 2.0'
  gem 'spring'
end

group :test do
  gem 'shoulda-matchers', '~> 3.0', require: false
  gem 'database_cleaner', '~> 1.5'
  gem 'faker', '~> 1.6.1'
end

Creating a Factory

Tạo 1 folder tên factories trong folder spec đồng thời tạo 1 file cùng tên với 1 model của bạn. Ví dụ: contacts.rb

***spec/factories/contacts.rb***

FactoryGirl.define do
  factory :contact do
    full_name     { Faker::Name.name }
    email         { Faker::Internet.email }
    phone_number  { Faker::PhoneNumber.phone_number }
    address       { Faker::Address.street_address }
  end
end

Model Specs

Chúng ta muốn đảm bảo rằng Factory chúng ta tạo ở trên là hợp lệ cho model. Tạo một tệp trong thư mục spec/model cho model của bạn:

***spec/models/contact_spec.rb***

require 'rails_helper'

RSpec.describe Contact, type: :model do
  it "has a valid factory" do
    expect(contact).to be_valid
  end
end

Trong terminal bạn chạy lệnh:

bundle exec rspec spec/models/contact_spec.rb

Nó sẽ hiện lỗi vì chúng ta chưa có model Contact. Nên chúng ta cần phải tạo:

rails g model Contact full_name:string email:string phone_number:integer address:text
rails db:migrate

à nếu bạn chạy lệnh đầu tiên nó sẽ bắt ghi đè file spec/factories/contacts.rb trong terminal bạn nhớ chọn "No" nhé
Chúng ta sẽ sử dụng shoulda-matchers để kiểm tra presence của full name, email, phone number và address nên trong contact_spec.rb các bạn thay

RSpec.describe Contact, type: :model do
  it "has a valid factory" do
    expect(contact).to be_valid
  end
end

bằng

  describe Contact do
    it { is_expected.to validate_presence_of(:full_name) }
    it { is_expected.to validate_presence_of(:email) }
    it { is_expected.to validate_presence_of(:phone_number) }
    it { is_expected.to validate_presence_of(:address) }
  end

Sau dó chạy lại bundle exec rspec spec/models/contact_spec.rb
nó sẽ bị lỗi vì trong model chúng ta không yêu cầu nó phải presence. Để sửa chúng ta chỉ cần thêm trong models/contact.rb như sau:

class Contact < ActiveRecord::Base
  validates_presence_of :full_name, :email, :phone_number, :address
end

Và kết quả nhận được như sau: Vậy là đã pass!

RSpec DSL Pieces

RSpec là một DSL để tạo các ví dụ thực thi về cách mã được dự kiến sẽ hành xử, được tổ chức theo nhóm.

describe tạo ra một nhóm ví dụ. Nó cần một đối số cho biết thông số kỹ thuật là gì. Đối số có thể là một lớp, mô-đun, phương thức hoặc mô tả chuỗi.

it tạo ra một example và lấy một mô tả về example đó

expect cho phép bạn thể hiện kết quả mong đợi trên một đối tượng trong một ví dụ

Controller Spec

Controller spec sẽ thực hiện test nếu 1 new contact được tạo ra với các valid attributes
Chúng ta generate controller:

rails g controller Contacts

Nó sẽ đồng thời tạo luôn cho chúng ta spec/controllers/contacts_controller_spec.rb. Trong đó chúng ta thêm test action create như sau:

require 'rails_helper'

RSpec.describe ContactsController, type: :controller do

  describe "POST #create" do
    context "with valid attributes" do
      it "create new contact" do
        post :create, contact: attributes_for(:contact)
        expect(Contact.count).to eq(1)
      end
    end
  end
end

Chúng ta thêm trong config/routes.rb

***config/routes.rb***
...
resources :contacts

Và trong app/controllers/contacts_controller.rb:

class ContactsController < ApplicationController
  def new
    @contact = Contact.new
  end

  def create
    @contact = Contact.new(contact_params)

    respond_to do |format|
      if @contact.save
        format.html { redirect_to @contact }
        format.json { render :show, status: :created, location: @contact }
      else
        format.html { render :new }
        format.json { render json: @contact.errors, status: :unprocessable_entity}
      end
    end
  end

  private

  def contact_params
    params.require(:contact).permit(:full_name, :email, :phone_number, :address)
  end
end

Run spec:

bundle exec rspec spec/controllers/contacts_controller_spec.rb

Theo spec ở trên, hãy viết một spec sử dụng các thuộc tính không hợp lệ để tạo một contact mới. Spec này cần kiểm tra rằng liên hệ không được tạo:

***spec/controllers/contacts_controller_spec.rb***

context "with invalid attributes" do
  it "does not create a new contact" do
    post :create, contact: attributes_for(:invalid_contact)
      expect(Contact.count).to eq(0)
    end
  end
end

Tổng kết

Tài liệu them khảo: https://www.sitepoint.com/learn-the-first-best-practices-for-rails-and-rspec/