Tôi đã cải thiện RSpec như thế nào? (Phần 1)

Tram Ho

Mở đầu

Rails là framework cho ngôn ngữ Ruby và đang được sử dụng mạnh mẽ.

RSpec là một trong những testing framework phổ biến nhất cho Ruby.
RSpec-rails là một mở rộng của RSpec, cho phép bạn viết unit tests cho controller, views, helpers và model trong ứng dụng Rails.
Nó cũng cung cấp khả năng viết integration tests với selenium (sử dụng capybara).

Vậy tại sao nên dùng RSpec? Có một vài lí do mà mình thấy từ thực tế khi phát triển dự án đó là:

  • Viết unit test giúp kiểm tra đoạn code vừa viết có chạy hay ko.
  • Giúp dev liệt kê các trường hợp có thể xảy ra với đoạn code mình vừa viết. Qua đó giúp phát hiện các trường hợp lúc code bị thiếu, chưa nghĩ ra. Cũng như có thể tái hiện các trường hợp mà khi test tay chưa test được.
  • Đây là một hình thức giải thích khá hiệu quả, giúp reviewers và teamates đọc hiểu code nhanh hơn.
  • Viết unit test tốt thì khi maintain dự án sẽ giảm thiểu rủi ro và đỡ mất nhiều effort.

Vậy, làm sao để viết rspec ngắn gọn mà vẫn đầy đủ, dễ hiểu?

Cải thiện cách mô tả từng trường hợp

Vì viết rspec để kiểm tra code đã viết với từng trường hợp xem đã đúng hay chưa. Nên việc mô tả các trường hợp này là một phần tương đối quan trọng. Rspec sử dụng describe, context và it để làm việc này.

Describe và context giúp chia unit test thành các khối, có thể coi như là các test case. Chúng giúp mô tả trường hợp đang xảy ra, việc này giúp test trở nên dễ đọc và dễ bao quát hơn. Thông thường, trong describe sẽ bao gồm các context và describe nhỏ hơn. Sự khác biệt giữa describe và context thật ra là không nhiều, nó chỉ mang nghĩ convention là chủ yếu. Describe thì nên dùng khi mô tả các method hoặc function được viết test. Còn context thì mô tả các trường hợp xảy ra đối với method hoặc function đó. Với mỗi trường hợp đó sẽ có hành xử như thế nào thì sử dụng it. Một vài lưu ý về format:

Context thì nên bắt đầu với when/which:

It nên bắt đầu bằng động từ:

Ví dụ:

thì nên viết rspec với cấu trúc kiểu:

Một lưu ý nữa: Để tránh sự phức tạp trong mô tả example, thay vì sử dụng if trong it, hãy để nội dung phần if đấy lên context nhé.

Cải thiện tốc độ

Tốc độ của rspec phụ thuộc nhiều vào việc tạo các bản ghi pre-condition. Việc tạo bản ghi sẽ chủ yếu thông qua gem factorybot.

FactoryBot cung cấp khung xương để tạo data thông qua các file factory. Để hiểu rõ hơn thì trong phần này mình sẽ xem xét ví dụ với model item và model category có quan hệ: item belongs to category

  • FactoryBot.create: tạo instance và association liên quan, sau đó trả ra instance sau khi đã được lưu vào database. Ví dụ khi FactoryBot.create(:item) thì ngoài item, category cũng sẽ được tạo

Bên cạnh đó, các call back liên quan đến việc create item object cũng sẽ được gọi như before(:create), after(:create), after(:build)

  • FactoryBot.build: build instance và association liên quan, tuy nhiên, sẽ không trigger vào database mà trả ra instance đấy luôn. Ví dụ khi FactoryBot.build(:item) thì sẽ chỉ đơn giản là

Callback được gọi ở đây sẽ chỉ có after(:build)

  • FactoryBot.build_stubbed: tạo object và gán các giá trị thuộc tính vào object đó, và tương tự như build, sẽ không trigger vào database.

Có thể thấy rằng các giá trị id, created_at, updated_at đều khác nil. Tuy nhiên, khi Item.find(item.id) thì sẽ trả ra exception, bởi vì giá trị id này được fake ra, không hề tồn tại. Callback được gọi là after(:build).

Vậy, trong các example, thay vì dùng create, hãy tận dụng buildbuild_stubbed triệt để, vì càng thao tác với database ít thì tốc độ test càng nhanh.

Ví dụ: Thay vì item chỉ thuộc về 1 category như hiện tại thì item sẽ thuộc về 1 list category, lưu bởi trường category_ids với kiểu dữ liệu string. Và để lấy ra list item cùng thuộc về 1 category thì có scope

Để viết test cho scope này, input data mà mình tạo là:

Mình đã dùng build_stubbed để tạo data thay cho create. Và khi đấy

DRY unit test

Tất nhiên rồi, Don’t repeat yourself (DRY) code rồi thì hãy cố gắng DRY unit test nữa nhé. Và để làm được việc này thì hãy sử dụng share_examplesshare_context

  • share_example:
    • được hiểu như là một example, bao gồm pre-condition(có thể có hoặc không), behavior
    • được sử dụng thông qua: include_examples, it_behaves_like
  • share_context:
    • được hiểu như là một ngữ cảnh, chỉ gồm pre-condition
    • được sử dụng thông qua: include_context

Ví dụ: Giả sử có yêu cầu: mỗi user login hệ thống vào ngày 2020/01/01 thì sẽ được tặng 1000 points. Khi đó, unit test sẽ là:

sử dụng helper travel_to

Ở đây, việc sử dụng it_behaves_like để gọi lại shared_examples cũng tương tự như dùng include_examples. Tuy nhiên, trong một số trường hợp khác thì không. Theo document có viết:

include_examples “name” # include the examples in the current context

it_behaves_like “name” # include the examples in a nested context

Để làm rõ hơn điều này, ta sẽ xem xét ví dụ sau:

nếu gọi liên tiếp 2 shared_examples này bởi include_examples với giá trị params truyền vào khác nhau như:

chạy test thì sẽ thấy example đầu tiên bị fail. Đọc log sẽ thấy expected là ‘second value’, còn current là ‘first value’, điều này có nghĩa là biến something đã nhận giá trị truyền vào sau cùng. Map với docs bên trên thì sẽ hiểu: khi sử dụng include_examples thì example sẽ được thêm luôn vào context hiện tại, còn khi sử dụng it_behaves_like thì example sẽ được bọc trong một context nữa, rồi mới thêm vào context hiện tại. Do đó, khi include 2 lần liên tiếp như ví dụ trên, thì biến something, trong cùng 1 context, đã được khai báo 2 lần, dẫn đến việc bị ghi đè, nên giá trị nhận được đó là giá trị khai báo sau, là ‘second value’

Kết

Bài viết mang quan điểm cá nhân của mình, sau một khoảng thời gian đã viết rspec khá nhiều. Mong nhận được sự đóng góp của mọi người!

Chia sẻ bài viết ngay

Nguồn bài viết : Viblo