BDD với Cucumber trong Ruby on Rails
BDD is second-generation, outside-in, pull-base, multiple-stakeholder, multiple-scale, high-automation, agile methodology. (Dan North) BDD mô tả một chu kỳ của sự tương tác với kết quả đầu ra được xác định rõ,kết quả trong việc cung cấp các hoạt động, thử nghiệm phần mềm có vấn đề. TDD là ...
BDD is second-generation, outside-in, pull-base, multiple-stakeholder, multiple-scale, high-automation, agile methodology. (Dan North)
BDD mô tả một chu kỳ của sự tương tác với kết quả đầu ra được xác định rõ,kết quả trong việc cung cấp các hoạt động, thử nghiệm phần mềm có vấn đề.
TDD là một mô hình hơn là một quá trình. Nó miêu tả các chu kỳ của việc viết test trước, rồi sau đó là mã code, rồi đến việc refactoring. Nhưng nó không làm bất cứ báo cáo nào về:
- Tôi bắt đầu phát triển từ đâu ?
- Tôi nên test chính xác những gì ?
- Làm thế nào để test được cấu trúc và cách đặt tên ?
Cái tên TDD cũng gây nhầm lẫn. Làm thế nào bạn có thể test được những thứ chưa có ở đây
Dan North đã đưa ra gợi ý rằng: Thay vì viết test, bạn nên nghĩ đến việc qui định các hành vi cụ thể. Hành vi là những cách người dùng muốn ứng dụng của họ có
Ví dụ: Một chức năng đăng ký, tôi muốn hiển thị những gì, tôi muốn khi người dùng nhập sai thì hiển thị những gì. Đó là các hành vi của ứng dụng
Cucumber giúp chúng ta:
- Giảm thiểu hiểu lầm
- Ẩn đi chi tiết cách thực hiện
- Cung cấp kiểm tra hồi qui mạnh mẽ
- Truyền đạt được ý định
Cucumber cho phép chúng ta thông báo, bằng tiếng Anh thuần, ý định những hành vi của ứng dụng chúng ta xây dựng nên cho những người phát triển sau, hơn là tập trung họ xem code để biết mình cần làm gì
Cucumber là một công cụ kiểm thử tự động dựa trên việc thực thi các functions được mô tả dướng dạng plain-text, mục đích là để hỗ trợ cho việc viết BDD của các developers. Điều này có nghĩa rằng kịch bản test unit (scenarios) sẽ được viết trước và thể hiện nghiệp vụ, sau đó source code mới được cài đặt để pass qua tất cả các stories đó.
Trong Cucumber, mỗi page sẽ được định nghĩa bằng một model, các chi tiết UI sẽ được định nghĩa bằng các method tương ứng của model đó
Thông qua các page object, develop sẽ tương tác với server và thực hiện việc test
Ví dụ về story và scenario
Scenario: Typical Meetup Given : I am on the estimate page When : I fill in "Guest count" with "10" And : I fill in "Slice count" with "2" And : I press "Get Estimate" Then : I should see "You will need to order 3 pizzas"
Như ví dụ trên, chúng ta hiện tại đang truy cập vào trang esimate, sau đó chúng ta điền vào trường "Guest count" với giá trị 10, trường "Slice count" với giá trị 2 và submit form bằng nút "Get Estimate"
Và sau khi submit form, chúng ta phải nhận được thông báo "You will need to order 3 pizzas"
Chúng ta có thể viết lại dưới dạng "code" hơn như sau:
Scenario: Typical Meetup Given : I am on "/estimates/new" When : I fill in "input#guests" with "10" And : I fill in "input#slices" with "2" And : I press "input[type='submit']" Then : I should see "You will need to order 3 pizzas"
Sau đây sẽ là một ví dụ hoàn chỉnh về việc order
Feature: Estimating Pizza Requirements In order to avoid wasting either pizza or money As an organizer I want to know how many pizzas I need to order Background: Given there are 10 guests expected Scenario: Typical meetup (Guests eat 2 slices) Given the guests are hungry When I ask how much to order Then I will know I need to buy 3 pizzas Scenario: Late-night meetup (Guests eat 3 slices) Given the guests are starving When I ask how much to order Then I will know I need to buy 4 pizzas Scenario: After-lunch meetup (Guests eat 1 slice) Given the guests are full When I ask how much to order Then I will know I need to buy 2 pizzas
Để thực hiện các hành vi tương tác trên, chúng ta phải viết "Step Definitions" để qui định chúng ta sẽ làm gì với các "hành vi" đó
Trong Step Definitions, chúng ta sẽ sử dụng các method liên quan đến domain để tương tác với server
Given(/^there are (d+) guests expected$/) do |guest_count| Site.new_estimate_page.guests_expected = guest_count end Given(/^the guests are (full|hungry|starving)$/) do |hunger_level| Site.new_estimate_page.hunger_level = hunger_level end When 'I ask how much to order' do Site.new_estimate_page.request_estimate end Then(/^I will know I need to buy (d+ pizzas)$/) do |pie_count| expect(Site.new_estimate_page).to have_text("#{pie_count}") end
Ở đây, Site là một page object đại diện cho trang web của chúng ta, "new_estimate_page" là trang order hiện tại, với mỗi hành vi trong scenario trùng khớp với regx được cho trong Step Definitions, các method tại Site sẽ được thực thi tương ứng
Ta sẽ làm một trang web đơn giản tạo Book và viết Cucumber test cho trang web nay
Đầu tiên, dùng scaffold để tạo ngay một trang tạo Book đơn giản với 2 trường là Name và Author
rails new book_demo -d mysql rails g scaffold Book name:string author:string
Thêm gem vào trong Gemfile
group :test do gem 'cucumber-rails', :require => false # database_cleaner is not required, but highly recommended gem 'database_cleaner' gem 'capybara' end
Cài đặt Cucumber
rails g cucumber:install
Thêm validate cho name và author
validates :author, presence: true validates :name, presence: true
Như vậy khi tạo book mới, nếu không điền author hoặc name, ta sẽ được lỗi như sau
Và khi tạo thành công
Bây giờ ta sẽ biết BDD cho chức năng tạo mới book
Ta tạo file book.feature trong thư mục features
Feature: Create book form Input data to form click submit button Scenario: Create a new book with invalid params Given I am on "/books/new" When I fill in "book[name]" with "Linh" When I press "Create Book" Then I should see "Author can't be blank" Scenario: Create a new book with valid params Given I am on "/books/new" When I fill in "book[name]" with "Linh" When I fill in "book[author]" with "Linh" When I press "Create Book" Then I should see "Book was successfully created."
Bây giờ, chúng ta sẽ định nghĩa các hành vi mô tả trong feature file
Tạo file features/step_definitions/books_steps.rb
Given /^I am on "(.+)"$/ do |page_path| visit page_path end When /^I fill in "(.+)" with "(.+)"$/ do |field, value| fill_in(field, with: value) end When /^I press "([^"]*)"$/ do |button| click_button(button) end Then /^I should see "([^"]*)"$/ do |text| page.has_content? text end
Trong file này, ta đã định nghĩa các hành vi được đưa ra trong feature file
Và cuối cùng khi chạy
rake cucumber
ta được kết quả
Using the default profile... Feature: Create book form Input data to form click submit button Scenario: Create a new book with invalid params # features/book.feature:5 Given I am on "/books/new" # features/step_definitions/books_steps.rb:1 When I fill in "book[name]" with "Linh" # features/step_definitions/books_steps.rb:5 When I press "Create Book" # features/step_definitions/books_steps.rb:9 Then I should see "Author can't be blank" # features/step_definitions/books_steps.rb:13 Scenario: Create a new book with valid params # features/book.feature:11 Given I am on "/books/new" # features/step_definitions/books_steps.rb:1 When I fill in "book[name]" with "Linh" # features/step_definitions/books_steps.rb:5 When I fill in "book[author]" with "Linh" # features/step_definitions/books_steps.rb:5 When I press "Create Book" # features/step_definitions/books_steps.rb:9 Then I should see "Book was successfully created." # features/step_definitions/books_steps.rb:13 2 scenarios (2 passed) 9 steps (9 passed)
Nhưng các bạn có thấy điều lạ khi ở đây ta không cần tạo page object cho trang tạo mới book này
Lý do rất đơn giản là ta đã sử dụng gem "Capybara", nó sẽ tạo biến page mỗi khi ta "visit" đến một page nào đó, và biến "page" ở đây sẽ đại diện cho trang hiện tại chúng ta "visit" đến
Như ở đây, ta đã dùng method "has_content?" để kiểm tra "page" có chứa đoạn text nào đó hay không
Như vậy, bạn có thể test các chức năng tương tác với server bằng Cucumber, chỉ cần chỉ ra luồng dữ liệu, viết các bước và xem kết quả
Code https://github.com/linhnt/book_demo