Sử dụng Rspec viết unit test cho Controller trong ứng dụng Rails

Những điều cơ bản để test trong Controller

Controller spec được tách nhỏ ra bởi phương thức controller, mỗi test case được dựa trên một action và có thể gửi kèm params hoặc không. Ví dụ như sau:

it "redirects to the home page upon save" do
  post :create, contact: Factory.attributes_for(:contact)
  expect(response).to redirect_to root_url
end

Dưới đây là những điều cần chú ý:

  • Mô tả của mỗi test case cần phải được viết một cách rỏ ràng và dễ đọc.
  • Mỗi test case chỉ có 1 expect.
  • Factory sẽ có nhiệm vụ tạo ra data test để truyền vào controller.

Mô tả test case

Chúng ta sẽ sử dụng ứng dụng Address Book để làm ví dụ, dưới đây là những thứ mà chúng ta cần phải test:

# spec/controllers/contacts_controller_spec.rb
require 'spec_helper'

describe ContactsController do
  describe "GET #index" do
    it "populates an array of contacts"
    it "renders the :index view"
  end
  
  describe "GET #show" do
    it "assigns the requested contact to @contact"
    it "renders the :show template"
  end
  
  describe "GET #new" do
    it "assigns a new Contact to @contact"
    it "renders the :new template"
  end
  
  describe "POST #create" do
    context "with valid attributes" do
      it "saves the new contact in the database"
      it "redirects to the home page"
    end
    
    context "with invalid attributes" do
      it "does not save the new contact in the database"
      it "re-renders the :new template"
    end
  end
end

Trong model spec ở trên, chúng ta đã sử dụng block describecontext để mô tả test case một cách dễ hiểu hơn, tất cả đều dựa vào nội dung của action trong controller. Trong trường hợp này, trường hợp test đúng là một user sẽ truyền attributes hợp lệ vào controller, trường hợp test sai là truyền attributes không hợp lệ. Nếu ứng dụng của bạn có thực hiện việc xác thực thì bạn có thể thêm nó vào trong context, test với trường hợp có login và không login, hoặc test phân quyền trong ứng dụng.

Tạo data test

Cũng như spec trong model, spec trong controller cũng cần data. Chúng ta sẽ sử dụng Factory để tạo dữ liệu mẫu.

# spec/factories/contacts.rb
factory :contact do |f|
  f.firstname { Faker::Name.first_name }
  f.lastname { Faker::Name.last_name }
end

Bây giờ chúng ta sẽ thêm một contact không hợp lệ nữa:

factory :invalid_contact, parent: :contact do |f|
  f.firstname nil
end

Chú ý sự khác biệt là :invalid_contact sử dụng :contact làm đối tượng cha (Trong trường hợp này, firstname là của chính nó, tất cả các attributes còn lại sẽ lấy từ :contact)

Testings GET methods

Những action trong controller có phương thức GET là #index, #new, #show#edit. Dựa vào nội dung những cái chúng ta đã nêu ra ở trên thì chúng ta thêm vào những test case sau:

# spec/controllers/contacts_controller_spec.rb

describe "GET #index" do
  it "populates an array of contacts" do
    contact = Factory(:contact)
    get :index
    expect(assigns(:contacts)).to eq([contact])
  end
  
  it "renders the :index view" do
    get :index
    expect(response).to render_template :index
  end
end

describe "GET #show" do
  it "assigns the requested contact to @contact" do
    contact = Factory(:contact)
    get :show, id: contact
    expect(assigns(:contact)).to eq(contact)
  end
  
  it "renders the #show view" do
    get :show, id: Factory(:contact)
    expect(response).to render_template :show
  end
end

Chúng ta hoàn toàn dựa trên code để viết được test case.

Testing POST methods

Chúng ta hãy chuyển sang phương thức #create của controller. Khi mà user truyền các attributes cho một contact hợp lệ và không hợp lệ, ta sẽ viết các test case tương ứng:

# spec/controllers/contacts_controller_spec.rb

describe "POST create" do
  context "with valid attributes" do
    it "creates a new contact" do
      expect{
        post :create, contact: Factory.attributes_for(:contact)
      }.to change(Contact,:count).by(1)
    end
    
    it "redirects to the new contact" do
      post :create, contact: Factory.attributes_for(:contact)
      expect(response).to redirect_to Contact.last
    end
  end
  
  context "with invalid attributes" do
    it "does not save the new contact" do
      expect{
        post :create, contact: Factory.attributes_for(:invalid_contact)
      }.to_not change(Contact,:count)
    end
    
    it "re-renders the new method" do
      post :create, contact: Factory.attributes_for(:invalid_contact)
      expect(rsponse).to render_template :new
    end
  end 
end

Testing PUT methods

Ở trong action #update, chúng ta sẽ kiểm tra hai trường hợp, attributes được truyền vào trong method sẽ được assign đến model mà chúng ta update, thứ 2 là redirect path. Sau đó chúng ta sẽ kiểm tra những trường hợp trên có đúng không nếu truyền vào params không hợp lệ.

# spec/controllers/contacts_controller_spec.rb

describe 'PUT update' do
  before :each do
    @contact = Factory(:contact, firstname: "Lawrence", lastname: "Smith")
  end
  
  context "valid attributes" do
    it "located the requested @contact" do
      put :update, id: @contact, contact: Factory.attributes_for(:contact)
      expect(assigns(:contact)).to eq(@contact)      
    end
  
    it "changes @contact's attributes" do
      put :update, id: @contact, 
        contact: Factory.attributes_for(:contact, firstname: "Larry", lastname: "Smith")
      @contact.reload
      @contact.firstname.should eq("Larry")
      @contact.lastname.should eq("Smith")
    end
  
    it "redirects to the updated contact" do
      put :update, id: @contact, contact: Factory.attributes_for(:contact)
      expect(response).to redirect_to @contact
    end
  end
  
  context "invalid attributes" do
    it "locates the requested @contact" do
      put :update, id: @contact, contact: Factory.attributes_for(:invalid_contact)
      expect(assigns(:contact)).to eq(@contact)      
    end
    
    it "does not change @contact's attributes" do
      put :update, id: @contact, 
        contact: Factory.attributes_for(:contact, firstname: "Larry", lastname: nil)
      @contact.reload
      @contact.firstname.should_not eq("Larry")
      @contact.lastname.should eq("Smith")
    end
    
    it "re-renders the edit method" do
      put :update, id: @contact, contact: Factory.attributes_for(:invalid_contact)
      expect(response).to render_template :edit
    end
  end
end

Những test case trên đã verify được các trường hợp đã nói ban đầu, nó đã thực sự được update, Chú ý là chúng ta đã gọi hàm reload trong @contact để kiểm tra đã update thành công.

Testing DELETE methods

Test cho action #destroy là tương đối đơn giản:

# spec/controllers/contacts_controller_spec.rb

describe 'DELETE destroy' do
  before :each do
    @contact = Factory(:contact)
  end
  
  it "deletes the contact" do
    expect{
      delete :destroy, id: @contact        
    }.to change(Contact,:count).by(-1)
  end
    
  it "redirects to contacts#index" do
    delete :destroy, id: @contact
    expect(response).to redirect_to contacts_url
  end
end

Test case đầu tiên là kiểm tra xem object đã thực sự được xóa chưa, test case thứ 2 là xác nhận user đã redirect ngược lại trang index.

Tổng kết

Thông qua việc test cho controller, có lẽ giờ bạn đã khá thông thạo trong việc test cho controller, nhưng mà bạn nên làm nhiều ví dụ hơn, tìm hiểu nhiều kỷ thuật hơn trong việc test với Rspec, Factory Girl và những helper khác để cho test case của bạn trở nên chuyên nghiệp hơn. Cảm ơn bạn đã đọc.