BDD với Cucumber trong Ruby on Rails
Bài đăng này đã không được cập nhật trong 3 năm
BDD là gì
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
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 đó.
Page Object
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
Stories và scenarios
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
Step Definitions
Để 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
Tạo ứng dụng Rails đơn giản chạy cucumber
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ả
All rights reserved