Viết Rspec cho Controller

Viết Rspec là 1 phần không thể thiếu trong quá trình phát triển ứng dụng, bên cạnh những phần test logic trong Model thì phần viết test cho controller cũng là 1 phần khá quan trọng của việc viết Rspec.

Tổ chức test.

'Describe' và 'Context' là 2 thành phần giúp cho phần tổ chức test của chúng ta được trở nên rõ ràng và dễ đọc dựa trên các controller action và các trường hợp mà chúng ta test. Betterspecs.org cung cấp những điều cơ bản về viết test, nó sẽ giúp chúng ta viết test 1 cách đẹp mắt và tốt hơn.

The purpose of 'describe' is to wrap a set of tests against one functionality while 'context' is to wrap a set of tests against one functionality under the same state. Describe vs. Context in RSpec by Ming Liu

=> Mục đích của 'describe' là bao gồm 1 set các tests đối với 1 chức năng cụ thể.Trong khi 'context' bao gồm các tests đối với 1 chức năng trong cùng 1 trạng thái.

Chúng ta sẽ đặt mỗi HTTP session trong mỗi 'describe' khác nhau cho: stories_controller_spec.rb.

describe "Stories" do
  describe "GET stories#index" do
    context "when the user is an admin" do
      it "should list titles of all stories"
    end
    
    context "when the user is not an admin" do
      it "should list titles of users own stories" do
    end

Khi bạn muốn kiểm soát authorization access, bạn có thể tạo context mới cho mỗi trường hợp, chảng hạn context cho đăng nhập và không:

    context "when the user is logged in" do
     it "should render stories#index"
   end
   
   context "when the user is logged out" do
     it "should redirect to the login page"
   end
 end

Mặc định, RSpec-Rails configuration disable chức năng render templates cho controller spec.Bạn có thể bật lại nó bằng các add render_views.

  1. Tổng thể, add vào RSpec.configure block trong rails_helper.rb hoặc rspec_helper file.
  2. Đối với mỗi nhóm riêng lẻ:
  describe "GET stories#show" do
  it "should render stories#show template" do
  end
end

describe "GET stories#new" do
  it "should render stories#new template" do
  end
end

Nó sẽ rất thông dụng cho việc check valid hay invalid thuộc tính trước khi save vào DB:

  describe "POST stories#create" do
  context "with valid attributes" do
    it "should save the new story in the database"
    it "should redirect to the stories#index page"
  end
  
  context "with invalid attributes" do
    it "should not save the new story in the database"
    it "should render stories#new template"
  end
end

Chuẩn bị data test.

Chúng ta sử dụng factories để tạo data cho controller specs với gem FactoryBot, ví dụ:

FactoryBot.define do
factory :story do
  user
  sequence(:title) { |n| "Title#{n}" }
  sequence(:content) { |n| "Content#{n}" }
end
end

Test các action.

1.#index

def index
  @stories = Story.view_premissions(current_user).
end

Trong model story.rb

def self.view_premissions(current_user)
  current_user.role.admin? ? Story.all : current_user.stories
end

với những thông tin trên ta có thể tạo test GET stories#index như sau:

describe "GET stories#index" do
  context "when the user is an admin" do
    it "should list titles of all stories" do
      admin = create(:admin)
      stories = create_list(:story, 10, user: admin)
      login_as(admin, scope: :user)
      visit stories_path

      stories.each do |story|
        page.should have_content(story.title)
      end
    end
  end

  context "when the user is not an admin" do
    it "should list titles of users own stories" do
      user = create(:user)
      stories = create_list(:story, 10, user: user)
      login_as(user, scope: :user)
      visit stories_path

      stories.each do |story|
        page.should have_content(story.title)
      end
    end
  end
end

Bạn có thể thấy, ta tạo 2 context khác nhau của user role(admin và không phải admin). Sử dụng create(:user) hoặc create_list(:story, 10, user: user) bạn có thể tạo nhiều story khác nhau cho 1 user.Mottj cách khác để tạo user là dùng let or before blocks.

  1. #show Bạn có thể tạo test cho show bằng cách tương tự, sự khác biệt thì tùy vào code của bạn cho action show:
describe "GET stories#show" do
  it "should render stories#show template" do
    user = create(:user)
    story = create(:story, user: user)

    login_as(user, scope: :user)
    visit story_path(story.id)

    page.should have_content(story.title)
    page.should have_content(story.content)
  end
end
  1. #new và #create
# GET stories#new
def new
  @story = Story.new
end

# POST stories#create
def create
  @story = Story.new(story_params)
  if @story.save
    redirect_to story_path(@story), success: "Story is successfully created."
  else
    render action: :new, error: "Error while creating new story"
  end
end

private

def story_params
  params.require(:story).permit(:title, :content)
end

Action new render stories#new template, nó là 1 form được điền các thông tin của 1 story mới trước khi được tạo:

describe "POST stories#create" do
  it "should create a new story" do
    user = create(:user)
    login_as(user, scope: :user)
    visit new_stories_path

    fill_in "story_title", with: "Ruby on Rails"
    fill_in "story_content", with: "Text about Ruby on Rails"

    expect { click_button "Save" }.to change(Story, :count).by(1)
  end
end
  1. #update
def update
  if @story.update(story_params)
    flash[:success] = "Story #{@story.title} is successfully updated."
    redirect_to story_path(@story)
  else
    flash[:error] = "Error while updating story"
    redirect_to story_path(@story)
  end
end

private

def story_params
  params.require(:story).permit(:title, :content)
end

Khi 1 story được tạo, nó có thể cho phép ta update, bằng cách visit edit page:

describe "PUT stories#update" do
  it "should update an existing story" do
    user = create(:user)
    login_as(user, scope: :user)
    story = create(:story)
    visit edit_story_path(story)
    
    fill_in "story_title", with: "React"
    fill_in "story_content", with: "Text about React"
    
    click_button "Save"
    expect(story.reload.title).to eq "React"
    expect(story.content).to eq "Text about React"
  end
end
  1. #delete
def destroy
  authorize @story
  if @story.destroy
    flash[:success] = "Story #{@story.title} removed successfully"
    redirect_to stories_path
  else
    flash[:error] = "Error while removing story!"
    redirect_to story_path(@story)
  end 
end

test case cho delete:

describe "DELETE stories#destroy" do
  it "should delete a story" do
    user = create(:admin)
    story = create(:story, user: user)
    login_as(user, scope: :user)
    visit story_path(story.id)
    page.should have_link("Delete")
    expect { click_link "Delete" }.to change(Story, :count).by(-1)
  end
end

ở #delete, để check story đã bị xóa, ta có thể check count Story thay đổi (-1).