Sử dụng Rspec viết unit test cho Controller trong ứng dụng Rails
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 ...
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.
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 describe và context để 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.
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)
Những action trong controller có phương thức GET là #index, #new, #show và #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.
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
Ở 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.
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.
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.