BDD với Cucumber trong Ruby on Rails

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 đề.

1.png

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

2.png

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

alt

Và khi tạo thành công

alt

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