Product Management
Đăng nhập
ESC

Nhập từ khóa để tìm kiếm

↑↓ Di chuyển
Enter Mở
ESC Đóng

BDD với Cucumber — Gherkin syntax

Mở đầu — vì sao bài này quan trọng

Bạn đã từng gặp cảnh này chưa: Product Owner (PO) viết một tài liệu yêu cầu dài mười trang, dev đọc rồi hiểu một kiểu, QA đọc rồi hiểu một kiểu khác, và đến khi test thì mới phát hiện ra cả ba người đang nói về ba tính năng khác nhau. Đến cuối sprint, buổi demo trở thành buổi tranh cãi: "Ủa, tôi tưởng nút này phải làm thế này chứ?"

Đây là vấn đề kinh điển của ngành phần mềm: khoảng cách giao tiếp giữa người kinh doanh và người kỹ thuật. Test tự động thông thường (Selenium, pytest, JUnit) rất mạnh, nhưng nó viết bằng ngôn ngữ lập trình — PO và BA (Business Analyst) nhìn vào chỉ thấy một mớ assertEqualsdriver.findElement, hoàn toàn không đọc được. Vậy làm sao để họ tin rằng test đang kiểm tra đúng thứ họ cần?

BDD (Behavior-Driven Development — phát triển hướng hành vi) và công cụ Cucumber cùng ngôn ngữ Gherkin ra đời để giải quyết chính khoảng cách đó. Ý tưởng cốt lõi rất đẹp: viết kịch bản test bằng ngôn ngữ gần với tiếng người, ai cũng đọc được — PO, BA, QA, dev, thậm chí khách hàng — nhưng đằng sau nó vẫn là code chạy thật, kiểm thử thật.

Trong bài này, chúng ta tập trung duy nhất vào Cucumber và cú pháp Gherkin: cách viết một kịch bản hành vi rõ ràng, cách kết nối nó với code, và làm sao để một đội Việt Nam nơi PO không biết code vẫn có thể cùng tham gia định nghĩa chất lượng sản phẩm. Đây không phải chuyện "thêm một framework nữa để học" — đây là chuyện thay đổi cách cả đội nói chuyện với nhau về sản phẩm.

Khái niệm cốt lõi

BDD là gì và khác gì so với test thông thường

BDD là một phương pháp mở rộng từ TDD (Test-Driven Development). Thay vì hỏi "hàm này trả về đúng giá trị không?", BDD hỏi "khi người dùng làm hành động X trong tình huống Y, hệ thống có cư xử đúng như kỳ vọng Z không?". Nó mô tả hành vi (behavior) của hệ thống từ góc nhìn người dùng, chứ không phải chi tiết kỹ thuật bên trong.

Điểm mấu chốt: BDD tạo ra một tài liệu sống (living documentation). Kịch bản test chính là đặc tả yêu cầu, và vì nó chạy được nên nó không bao giờ lỗi thời — nếu tính năng thay đổi mà kịch bản không cập nhật, test sẽ fail ngay.

Cucumber — cỗ máy chạy Gherkin

Cucumber là công cụ đọc các file .feature viết bằng Gherkin, rồi ánh xạ từng dòng sang code (gọi là step definitions), và thực thi. Cucumber có mặt trên hầu hết ngôn ngữ: Cucumber-JVM (Java), Cucumber.js (JavaScript), Behave và pytest-bdd (Python), SpecFlow (nay là Reqnroll, cho C#).

Kiến trúc gồm ba tầng:

  • File .feature: viết bằng Gherkin, mô tả hành vi. Đây là phần PO/BA đọc được.
  • Step definitions: code (Java/Python/JS...) khớp với từng câu Gherkin bằng biểu thức chính quy hoặc Cucumber Expression.
  • Automation code: bên trong step definition, bạn gọi Selenium, API client, hoặc bất cứ thứ gì để thao tác hệ thống thật.

Gherkin — ngôn ngữ của hành vi

Gherkin là một ngôn ngữ có cấu trúc rất nhẹ, dùng vài từ khóa để tạo khung. Các từ khóa quan trọng:

  • Feature (Tính năng): mô tả tính năng đang được kiểm thử.
  • Scenario (Tình huống/Kịch bản): một trường hợp cụ thể của hành vi.
  • Given (Cho): bối cảnh ban đầu, tiền điều kiện. Trạng thái hệ thống trước khi hành động xảy ra.
  • When (Khi): hành động người dùng thực hiện — thường là một hành động duy nhất, chính.
  • Then (Thì/Thì): kết quả kỳ vọng, phần được kiểm chứng (assertion).
  • And / But (Và / Nhưng): nối thêm các bước cùng loại cho dễ đọc.
  • Background (Bối cảnh): các bước Given chung, chạy trước mỗi Scenario trong file.
  • Scenario Outline (Khung kịch bản) + Examples (Ví dụ): chạy cùng một kịch bản với nhiều bộ dữ liệu.
Điều tuyệt vời với người Việt: Gherkin hỗ trợ hơn 70 ngôn ngữ, trong đó có tiếng Việt. Chỉ cần khai báo # language: vi ở đầu file, bạn có thể viết Tính năng, Kịch bản, Cho, Khi, Thì, . Điều này cực kỳ giá trị cho các đội có PO/BA không thạo tiếng Anh.

Một ví dụ tiếng Việt hoàn chỉnh:

language: vi

Tính năng: Đăng nhập vào hệ thống VietShop Là một khách hàng đã có tài khoản Tôi muốn đăng nhập Để có thể xem lịch sử đơn hàng của mình

Bối cảnh: Cho tôi đang ở trang đăng nhập

Kịch bản: Đăng nhập thành công với thông tin hợp lệ Khi tôi nhập email "an@vietshop.vn" và mật khẩu "MatKhau@123" Và tôi nhấn nút "Đăng nhập" Thì tôi được chuyển đến trang chủ Và tôi thấy lời chào "Xin chào, An"

Kịch bản: Đăng nhập thất bại với sai mật khẩu Khi tôi nhập email "an@vietshop.vn" và mật khẩu "saibet" Và tôi nhấn nút "Đăng nhập" Thì tôi thấy thông báo lỗi "Email hoặc mật khẩu không đúng"

Cùng file, nhưng nếu viết tiếng Anh sẽ là Feature, Background, Scenario, Given, When, Then, And.

Nguyên tắc "Given-When-Then" chuẩn

Một kịch bản tốt tuân theo cấu trúc rõ ràng:

  • Given thiết lập trạng thái — không có hành động người dùng ở đây.
  • When chứa một hành động chính. Nếu bạn có nhiều When, thường là dấu hiệu bạn đang gộp nhiều kịch bản.
  • Then chỉ kiểm chứng kết quả quan sát được — không thao tác thêm.
Viết Gherkin ở mức "hành vi" chứ không phải "thao tác UI". So sánh hai câu: Khi tôi nhấn vào ô email, gõ chữ, nhấn tab, nhấn ô mật khẩu... (quá chi tiết, dễ vỡ) so với Khi tôi đăng nhập với tài khoản hợp lệ (mô tả hành vi, ổn định). Chi tiết UI nên nằm trong step definition, không nằm trong file .feature.

Tình huống thực tế

Ví dụ 1: Tiki và bài toán "PO không đọc được test"

Giả sử một đội phát triển tính năng giỏ hàng tại một sàn thương mại điện tử lớn như Tiki. Trước đây, QA viết toàn bộ test bằng Selenium + Java thuần. PO tên chị Hương phụ trách mảng khuyến mãi, nhưng mỗi lần muốn biết "test đã cover trường hợp áp mã giảm giá cho đơn dưới 200 nghìn chưa?" thì chị phải nhờ QA giải thích bằng miệng — mất thời gian, và không có gì đảm bảo.

Đội chuyển sang Cucumber. Giờ chị Hương mở file khuyen-mai.feature và đọc trực tiếp:

language: vi

Tính năng: Áp mã giảm giá

Kịch bản: Không áp được mã cho đơn dưới ngưỡng tối thiểu Cho giỏ hàng có tổng giá trị 150000 đồng Khi tôi áp mã "GIAM50K" yêu cầu đơn tối thiểu 200000 đồng Thì tôi thấy thông báo "Đơn hàng chưa đạt giá trị tối thiểu 200.000đ" Và tổng tiền vẫn là 150000 đồng

Kết quả: chị Hương tự rà soát được các kịch bản, phát hiện thiếu trường hợp "đơn đúng bằng 200 nghìn" và yêu cầu bổ sung. Bài học rút ra: giá trị lớn nhất của Gherkin không phải là tự động hóa — Selenium đã làm được điều đó — mà là biến test thành cầu nối giao tiếp, để người hiểu nghiệp vụ nhất cũng tham gia đảm bảo chất lượng.

Ví dụ 2: Một fintech Đông Nam Á và Scenario Outline cho nạp tiền

Một ví điện tử kiểu MoMo cần kiểm thử luồng nạp tiền với nhiều mức và nhiều ngân hàng. Nếu viết riêng từng kịch bản thì file phình to và trùng lặp. Đội dùng Scenario Outline để tham số hóa:

language: vi

Tính năng: Nạp tiền vào ví

Khung kịch bản: Nạp tiền thành công qua các ngân hàng Cho tài khoản ví có số dư 0 đồng Khi tôi nạp <so_tien> đồng qua ngân hàng "<ngan_hang>" Thì giao dịch được ghi nhận "thành công" Và số dư ví là <so_du_moi> đồng

Ví dụ:

so_tienngan_hangso_du_moi
50000Vietcombank50000
100000Techcombank100000
2000000BIDV2000000

Một khung, ba dòng dữ liệu — Cucumber tự động chạy thành ba kịch bản. Khi cần thêm ngân hàng thứ tư, chỉ thêm một dòng trong bảng Ví dụ, không đụng tới code. Bài học rút ra: Scenario Outline là cách Gherkin diễn đạt "cùng một hành vi, nhiều dữ liệu" một cách gọn gàng, giữ file .feature vừa ngắn vừa dễ đọc. (Lưu ý: đây khác với Data-Driven Testing tổng quát ở Bài 10; ở đây trọng tâm là cú pháp Gherkin diễn đạt điều đó.)

Ví dụ 3: Đội outsource và "sự thật duy nhất" giữa khách và team

Một công ty phần mềm gia công ở TP.HCM làm dự án cho khách Singapore. Khách viết yêu cầu bằng tiếng Anh, gửi qua email và Confluence, nhưng yêu cầu liên tục thay đổi và không ai chắc bản nào mới nhất. Cuối mỗi sprint, khách nghiệm thu và luôn có tranh cãi về "đúng spec hay chưa".

Đội quyết định dùng Gherkin làm tài liệu đặc tả chung. Trước mỗi tính năng, họ tổ chức buổi "Three Amigos" (ba vai: PO đại diện nghiệp vụ, dev, QA) để cùng viết kịch bản Gherkin. File .feature được khách duyệt trước khi code — trở thành hợp đồng nghiệm thu. Khi tính năng xong, chạy Cucumber ra kết quả pass/fail đối chiếu thẳng với các kịch bản khách đã ký.

Sau ba tháng, tỷ lệ tranh cãi khi nghiệm thu giảm hẳn, vì "sự thật" giờ nằm trong các file Gherkin mà cả hai bên cùng thống nhất. Bài học rút ra: Gherkin phát huy sức mạnh nhất khi được viết TRƯỚC khi code, trong một buổi thảo luận chung, chứ không phải viết sau như một lớp trang trí cho test đã có.

Hướng dẫn từng bước

Ta sẽ dựng một ví dụ nhỏ với pytest-bdd (Python) — nhẹ và dễ thấy toàn bộ luồng.

Bước 1 — Cài đặt.

pip install pytest-bdd

Bước 2 — Viết file .feature. Tạo features/dang_nhap.feature:

Feature: Đăng nhập
  Scenario: Đăng nhập thành công
    Given người dùng "an@vietshop.vn" đã tồn tại
    When người dùng đăng nhập với mật khẩu "MatKhau@123"
    Then đăng nhập thành công

Bước 3 — Viết step definitions. Tạo test_dang_nhap.py:

from pytest_bdd import scenarios, given, when, then, parsers

scenarios('features/dang_nhap.feature')

class HeThong: users = {"an@vietshop.vn": "MatKhau@123"} dang_nhap_ok = False

@given(parsers.parse('người dùng "{email}" đã tồn tại'), target_fixture="ctx") def user_ton_tai(email): return {"email": email, "he_thong": HeThong()}

@when(parsers.parse('người dùng đăng nhập với mật khẩu "{mat_khau}"')) def dang_nhap(ctx, mat_khau): email = ctx["email"] ctx["he_thong"].dang_nhap_ok = (HeThong.users.get(email) == mat_khau)

@then('đăng nhập thành công') def kiem_tra(ctx): assert ctx["he_thong"].dang_nhap_ok is True

Bước 4 — Chạy.

pytest -v
Cucumber/pytest-bdd sẽ khớp từng câu Gherkin với hàm tương ứng, chạy và báo pass.

Bước 5 — Tái sử dụng bước. Chú ý các câu Given/When được viết chung chung, dùng parsers.parse để bắt tham số. Nhờ vậy, Given người dùng "..." đã tồn tại dùng lại được cho hàng chục kịch bản khác — đây chính là điều làm bộ test dễ bảo trì.

Bước 6 — Kết nối automation thật. Trong dự án thực, bên trong step definition bạn thay logic giả bằng lời gọi Selenium (mở trình duyệt, điền form) hoặc API client. File .feature giữ nguyên — chỉ phần code bên dưới thay đổi. Đó là ranh giới quan trọng: Gherkin mô tả "cái gì", step definition lo "làm thế nào".

Lỗi thường gặp & mẹo

Lỗi 1 — Viết Gherkin quá chi tiết theo thao tác UI. Câu như Khi tôi nhấn nút màu xanh ở góc phải, đợi 2 giây, cuộn xuống... khiến kịch bản vỡ ngay khi giao diện đổi. Mẹo: viết ở mức hành vi (Khi tôi gửi đơn hàng), đẩy chi tiết UI vào step definition.

Lỗi 2 — Biến Gherkin thành script test kỹ thuật. Dùng câu như Given SQL INSERT vào bảng users là phá vỡ mục đích của BDD — PO không hiểu SQL. Hãy giữ ngôn ngữ nghiệp vụ, giấu kỹ thuật xuống dưới.

Lỗi 3 — Nhiều When trong một Scenario. Nếu bạn có When A ... Then ... When B ... Then ..., đó là hai kịch bản bị nhồi làm một. Tách ra, mỗi Scenario một hành vi.

Lỗi 4 — Bỏ qua buổi thảo luận, để một mình QA viết Gherkin sau khi code xong. Khi đó Gherkin chỉ còn là lớp vỏ, mất hết giá trị giao tiếp. Mẹo: áp dụng "Three Amigos", viết kịch bản trước.

Lỗi 5 — Step definition trùng lặp/mâu thuẫn. Nhiều regex khớp cùng một câu gây lỗi "ambiguous step". Mẹo: đặt tên bước nhất quán, gom step dùng chung vào một nơi.

Mẹo tổng quát: dùng Background cho các Given lặp lại; dùng tag (@smoke, @regression) để lọc và chạy nhóm kịch bản; đừng lạm dụng BDD cho unit test thuần kỹ thuật — BDD tỏa sáng ở tầng hành vi nghiệp vụ, còn unit test cứ để JUnit/pytest lo.

Bài tập thực hành

  • Viết feature tiếng Việt. Chọn tính năng "tìm kiếm sản phẩm" của một web bán hàng. Viết một file .feature với # language: vi, gồm ít nhất 2 kịch bản: tìm ra kết quả, và tìm không có kết quả. Dùng đúng Cho / Khi / Thì.
  • Chuyển sang Scenario Outline. Lấy tính năng "tính phí ship theo khoảng cách": dưới 5km 15k, 5-10km 25k, trên 10km 40k. Viết một Khung kịch bản với bảng Ví dụ ba dòng.
  • Nối code. Cài pytest-bdd, cài đặt step definitions cho bài 1, chạy pytest -v cho tới khi xanh. Thử làm fail có chủ đích (sửa assertion) để thấy report báo lỗi.
  • Suy ngẫm giao tiếp. Đưa file .feature của bạn cho một người không biết code (bạn bè, người thân) đọc. Họ có hiểu tính năng đang làm gì không? Nếu không, viết lại cho tự nhiên hơn — đó chính là bài kiểm tra thật sự của một kịch bản BDD tốt.

Tóm tắt

  • BDD giải quyết khoảng cách giao tiếp giữa nghiệp vụ và kỹ thuật, mô tả hệ thống theo hành vi chứ không theo chi tiết kỹ thuật.
  • Cucumber là cỗ máy đọc file .feature, ánh xạ sang step definitions (code) và thực thi; có mặt trên Java, Python, JS, C#...
  • Gherkin dùng các từ khóa Given-When-Then (Cho-Khi-Thì), hỗ trợ tiếng Việt qua # language: vi — cực kỳ giá trị cho đội có PO/BA không thạo tiếng Anh.
  • Cấu trúc chuẩn: Given thiết lập bối cảnh, When một hành động chính, Then kiểm chứng; dùng Background cho bước chung, Scenario Outline cho nhiều bộ dữ liệu.
  • Giá trị lớn nhất đến khi Gherkin được viết trước khi code, trong buổi "Three Amigos", trở thành tài liệu sống và hợp đồng nghiệm thu chung.
  • Viết ở mức hành vi, giấu chi tiết UI/kỹ thuật xuống step definition — đó là ranh giới giữa "cái gì" và "làm thế nào".
Nắm vững Gherkin, bạn không chỉ có thêm một công cụ test — bạn có một ngôn ngữ chung để cả đội nói về chất lượng sản phẩm. Và trong một đội nơi PO, dev, QA cùng hiểu nhau, chất lượng gần như tự nhiên mà tốt lên.

Học xong bài này rồi? Tạo tài khoản miễn phí để lưu lại — lần sau vào là biết ngay đang dở ở đâu, và học hết khóa thì có chứng chỉ. Lưu tiến độ của tôi