0

Tôi đã test một Rails Application như thế nào? Phần 4: Request Spec và Feature Spec

Nếu như trước đây thì chúng ta sau khi test model thì chúng ta sẽ chuyển sang test controller nhưng hiện nay thì controller testing không tốt khi sử dụng trong Rails. Thay vào đó chúng ta sẽ Test Request và test Feature, trong bài viết này mình sẽ nói về test request. Nếu như các bạn vẫn muốn biết về controller testing thì kết thúc series này mình sẽ viết.

Như các bạn cũng biết Action Cable là một trong những tính nay mới trong Rails 5 và nhận được rất nhiều sự chú ý, tuy nhiên điều mà chúng ta cần quan tâm nhất trong series này là một thay đổi lớn về controller-level testing. Mình sẽ có một bài viết chi tiết về những sự thay đổi trong bài viết tới

Từ Controller Specs đến Request Spec

Dưới đây là một đoạn code ở trong troller spec viết để test rằng guest, hoặc user chưa log in vào hệ thống sẽ redirect đến login form thay vì submit dữ liệu vào database

describe "guest access" do
  describe 'GET #new' do
    it "requires login" do
      get :new
      expect(response).to redirect_to login_url
    end
  end

  describe "POST #create" do
    it "requires login" do
      post :create, contact: FactoryGirl.attributes_for(:contact)
      expect(response).to redirect_to login_url
    end
  end

  # other examples ...
end

Làm thế nào chúng ta có thể tăng độ coverage mà không dựa vào controller spec?

Một lựa chọn là sử dụng request specs. Các ví dụ này sử dụng các phương pháp đơn giản của Rack :: Test để chuyển HTTP request, cùng với các parameter tới ứng dụng của bạn. (Nếu bạn đã quen thuộc với Capybara, hãy lưu ý rằng bạn không thể sử dụng các phương thức như visit hoặc has_content trong request spec, mà bạn sẽ cần phải sử dụng một feature specs chi tiết hơn về điều đó trong phần dưới.

Đây là những gì một request spec với cùng mức test coverage với controller spec có thể có như sau:

require "rails_helper"

describe "Public access to contacts", type: :request do
  it "denies access to contacts#new" do
    get new_contact_path
    expect(response).to redirect_to login_url
  end

  it "denies access to contacts#create" do
    contact_attributes = FactoryGirl.attributes_for(:contact)

    expect {
      post "/contacts", { contact: contact_attributes }
    }.to_not change(Contact, :count)

    expect(response).to redirect_to login_url
  end
end

Sự khác biệt về cấu trúc và cú pháp giữa controller spec cũ và request spec mới không quá lớn. Nhưng có 2 điều chúng ta có nhìn thấy rõ nhất về sự cải tiến mà controller spec không có. Thứ nhất, nó thực sự hit vào các HTTP endpoint trong ứng dụng của bạn, ngược với việc gọi các phương thức điều khiển trực tiếp. Nó cũng cho biết thêm một số phạm vi ảnh hưởng của các route trong ứng dụng - trong trường hợp này, chúng ta có thể thấy rằng new_contact_pathlogin_url được nhận ra bởi app và pass request đúng nơi, đúng controller. Cách tiếp cận này là optional; bạn cũng có thể tham khảo các đường dẫn trực tiếp đã được thể hiện trong ví dụ POST.

Bây giờ, controller spec không sử dụng deprecated behavior mà mình đã đề cập trong phần giới thiệu, và đó là một trong những tính năng rất quan trọng để có thể nâng cao test coverage. Sẵn sàng để nâng cấp lên Rails 5, việc thay đổi từ controller specs sang feature specs và request specs là một quyết định rất khôn ngoan. Vừa giảm thiểu được những lỗi không đáng có, nâng cao coverage và giảm được performance

Từ controller specs đến feature specs

Một trong những điều mà mình rất ngưỡng mộ về đội ngũ phát triển Rails là sự sẵn sàng của họ để loại bỏ các tính năng mà không còn ý nghĩa trong framework của mình. Trong Rails 5.0, 2 method assignsassert_template không nhận được sử hưởng ứng nhiều từ người dùng. Rất nhiều ý kiến đã cho rằng, "controller testing không phải là một ý tưởng hay." Mình đồng ý với quan điểm trên. Liệu một trình duyệt có quan tâm xem tệp đó có trách nhiệm đối với HTML mà bạn nhận được khi request từ server? Liệu nó cần phải biết được các instance variable đã được assign như thế nào trong Ruby worker để response? Không và rất có thể, các test của bạn cũng không cần biết điều này.

Nếu bạn phải sử dụng helper trong Rails 5, bạn có thể khôi phục chúng thông qua rails-controller-testing. rails-controller-testing sẽ giữ cho cái test cũ của bạn vẫn có thể chạy tốt khi bạn nâng cấp các app hiện tại lên Rails 5.0 nhưng nó không được khuyến khích cho các app mới

Dưới đây mình có viết phần test cover cho index và show action, chia ra làm 6 lần test, và sử dụng cả assignassert_template, chúng sẽ gọi RSpec thông qua `expect(response).to render_template(...). Nó cũng đưa ra các behavior của model Contact, tăng tốc độ và sự cô lập (isolation) trong các test case.

describe 'GET #index' do
  context 'with params[:letter]' do
    it "populates an array of contacts starting with the letter" do
      smith = FactoryGirl.create(:contact, lastname: 'Smith')
      jones = FactoryGirl.create(:contact, lastname: 'Jones')
      get :index, letter: 'S'
      expect(assigns(:contacts)).to match_array([smith])
    end

    it "renders the :index template" do
      get :index, letter: 'S'
      expect(response).to render_template :index
    end
  end

  context 'without params[:letter]' do
    it "populates an array of all contacts" do
      smith = FactoryGirl.create(:contact, lastname: 'Smith')
      jones = FactoryGirl.create(:contact, lastname: 'Jones')
      get :index
      expect(assigns(:contacts)).to match_array([smith, jones])
    end

    it "renders the :index template" do
      get :index
      expect(response).to render_template :index
    end
  end
end

describe 'GET #show' do
  let(:contact) { FactoryGirl.build_stubbed(:contact,
    firstname: 'Lawrence', lastname: 'Smith') }

  before :each do
    allow(Contact).to receive(:persisted?).and_return(true)
    allow(Contact).to \
      receive(:order).with('lastname, firstname').and_return([contact])
    allow(Contact).to \
      receive(:find).with(contact.id.to_s).and_return(contact)
    allow(Contact).to receive(:save).and_return(true)

    get :show, id: contact
  end

  it "assigns the requested contact to @contact" do
    expect(assigns(:contact)).to eq contact
  end

  it "renders the :show template" do
    expect(response).to render_template :show
  end
end

Chúng ta define các scenario để xác định các case khi mà visitor access vào 1 site, hay xem 1 phần thu mục, fillter v...v.. nói đơn giản là tạo ra các kịch bản mà người dùng thực tế sẽ sử dụng, để kiểm tra xem các kịch bản đó có lỗi hay không. Nhiều người cho rằng việc test này chỉ là phụ nhưng trên thực tế, đôi khi việc logic code của bạn bị sai không gây ra ảnh hưởng nghiêm trọng bằng việc khi người dùng sử dụng theo những thao tác cơ bản, đơn giản thuần người dùng mà không thành công. Hãy thử tưởng tượng, bạn có một logic sai trong phần contact về việc hiển thị chữ hoa chữ thường hay là, thứ tự first name, last name, nhưng mặc dù sai nhưng app của bạn vẫn có thể chạy được. Nhưng hãy thử tưởng tượng xem khi mà user sử dụng theo 1 kịch bản đơn giản là vào website -> Click vào mục danh bạ mà bỗng dưng danh bạ lại không vào được hoặc vào 1 trang khác thì sao? Như vậy là sai kịch bản của người dùng dẫn đến việc ảnh hưởng rất nhiều đến việc người dùng sử dụng app. Không giống như các ví dụ về request spec ở phần trên của bài viết, chúng ta sẽ sử dụng các Capybara method ví dụ như visit hay click_link để có thể mô phỏng các tương tác trong trình duyện và cung cấp cho chúng ta những thông số kĩ thuật dễ đọc hơn rất nhiều

require "rails_helper"

feature "Public access to contacts", type: :feature do
  scenario "visitor finds contact by filtering by letter" do
    smith = FactoryGirl.create(:contact, firstname: "John", lastname: "Smith")
    FactoryGirl.create(:contact, firstname: "Sally", lastname: "Jones")

    visit root_path

    expect(page).to have_content "John Smith"
    expect(page).to have_content "Sally Jones"

    click_link "S"

    expect(page).to have_content "John Smith"
    expect(page).to_not have_content "Sally Jones"

    click_link "John Smith"

    expect(current_path).to eq contact_path(smith)

    within "h1" do
      expect(page).to have_content "John Smith"
    end
  end
end

Tôi thích kiểu test này vì nó tốt hơn nhiều. Nó dễ đọc và dễ tìm ra nguyên nhân, chúng ta có thể đọc một kịch bản duy nhất để hiểu được tính năng này của ứng dụng. Nó cung cấp mức độ cover cao hơn, qua nhiều endpoint. Một điều nữa để thích về nó: Nó giúp chúng ta refactor các lỗ hổng trong code, những vị trí có nguy cơ xảy ra lỗi trong controller.

def index
  if params[:letter]
    @contacts = Contact.by_letter(params[:letter])
  else
    @contacts = Contact.order('lastname, firstname')
  end
end

Tổng kết

assignsassert_template đã bị loại bỏ khỏi Rails vì rất nhiều lý do và bây giờ là thời điểm để thay đổi, đặc biệt là với controller-level testing. Thay vì sử dụng controller tests với feature style integration tests yêu cầu nhiều hơn và đòi hỏi tốt nhiều effort hơn rất nhiều việc sử dụng request specs hay feature specs nhưng vẫn đảm bảo được việc cover, chưa kể trong rất nhiều trường hợp còn có có thể cover được rất nhiều trường hợp mà controller test không thể tìm ra được. Với controller test mang thiên hướng về logic nhiều hơn và đôi khi không cover được hết các kịch bản của người dùng, mặc dù đơn giản những là lại những điều thiết yếu. Trong bài viết tới mình sẽ nói về việc sử dụng controller testing vì sẽ có rất nhiều bạn vẫn cần phải sử dụng nó vì những yêu cầu của dự án, hay đơn giản là để có thể thay đổi thì cần phải biết cái cũ của chúng ta như thế nào. Xin cám ơn các bạn đã đón đọc và mong sẽ có những phản hồi từ các bạn.

Trong bài viết tiếp theo trong loạt bài này, chúng tôi sẽ xem xét một đặc điểm mà không đào sâu vào việc thực hiện bộ điều khiển và đặc biệt sử dụng những người được ủy nhiệm không được hỗ trợ và những người trợ giúp assert_template.

Cho đến lúc đó, hãy bắt đầu xem các bài kiểm tra bộ điều khiển của ứng dụng cho cơ hội để chứng minh trong tương lai với yêu cầu kỹ thuật. Và nếu bạn bổ sung thêm chức năng mới vào ứng dụng Rails của mình, hãy xem xét việc thay thế các phép thử mới ở mức điều khiển.


All Rights Reserved

Viblo
Let's register a Viblo Account to get more interesting posts.