BDD (Behavior-Driven Development) là một phương pháp phát triển phần mềm linh hoạt, tập trung vào việc消除 sự歧义 giữa các bên liên quan về yêu cầu nghiệp vụ và xác thực các kịch bản người dùng cuối. Thay vì viết mã kiểm thử thuần túy, BDD sử dụng ngôn ngữ tự nhiên có cấu trúc để mô tả hành vi của hệ thống.
Mô Tả Nghiệp Vụ Và Kịch Bản Người Dùng
Trong BDD, các yêu cầu được viết dưới dạng các file có đuôi .feature sử dụng cú pháp Gherkin. Cấu trúc cơ bản bao gồm Feature (tính năng), Scenario (kịch bản), và các bước Given (điều kiện đầu), When (hành động), Then (kết quả mong đợi).
Dưới đây là ví dụ về một file mô tả tính năng quản lý khóa học:
# File: lms_features.feature
Feature: Quản lý khóa học trên nền tảng LMS
Mô tả: Cho phép quản trị viên tạo khóa học mới và sinh viên xem chi tiết
Scenario: Quản trị viên tạo khóa học mới
Given Quản trị viên:{admin_name}, Mật khẩu:{admin_pass}
And Chủ đề:{subject}, Tên khóa học:{course_name}, Mô tả:{overview}
When Truy cập trang quản trị LMS
And Chọn module Courses và nhấn nút tạo mới
And Điền thông tin tác giả, chủ đề, tiêu đề và mô tả rồi lưu
Then Hệ thống hiển thị liên kết chứa tên:{course_name}
And Xóa khóa học vừa tạo để dọn dẹp dữ liệu
Một file .feature chỉ chứa một Feature duy nhất nhưng có thể bao gồm nhiều Scenario. Quy trình Given-When-Then tương ứng với chuẩn bị dữ liệu, thực thi thao tác và xác thực kết quả. Các bước And sẽ kế thừa ngữ cảnh của bước trước đó (ví dụ: And sau Given cũng được xem là điều kiện đầu).
Ánh Xạ Và Triển Khai Các Bước Kiểm Thử
Để các kịch bản trong file .feature có thể thực thi được, chúng cần được ánh xạ tới các hàm code Python cụ thể. Thư viện pytest-bdd cung cấp các decorator để liên kết văn bản trong kịch bản với hàm xử lý tương ứng.
# File: steps.py
from pytest_bdd import given, when, then, parsers
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import Select
from selenium.webdriver.support import expected_conditions as EC
@given(parsers.parse("Quản trị viên:{admin_name}, Mật khẩu:{admin_pass}"))
def obtain_credentials(admin_name, admin_pass):
"""Lưu trữ thông tin đăng nhập để sử dụng cho các bước sau"""
return {"admin_name": admin_name, "admin_pass": admin_pass}
@given(parsers.parse("Chủ đề:{subject}, Tên khóa học:{course_name}, Mô tả:{overview}"))
def prepare_course_data(subject, course_name, overview):
"""Chuẩn bị dữ liệu khóa học"""
return {"subject": subject, "course_name": course_name, "overview": overview}
@when("Truy cập trang quản trị LMS")
def authenticate_admin(selenium, obtain_credentials):
"""Thực hiện đăng nhập vào hệ thống"""
selenium.get("http://lms-example.com/admin/")
selenium.find_element(By.ID, "id_username").send_keys(obtain_credentials['admin_name'])
selenium.find_element(By.ID, "id_password").send_keys(obtain_credentials['admin_pass'])
selenium.find_element(By.CLASS_NAME, "submit-row").click()
@when(parsers.parse("Chọn module {module} và nhấn nút tạo mới"))
def navigate_to_create_page(selenium, module):
"""Điều hướng đến trang tạo mới"""
selenium.find_element(By.LINK_TEXT, module).click()
selenium.find_element(By.CLASS_NAME, "addlink").click()
@when("Điền thông tin tác giả, chủ đề, tiêu đề và mô tả rồi lưu")
def submit_course_form(selenium, obtain_credentials, prepare_course_data):
"""Điền form và lưu khóa học"""
Select(selenium.find_element(By.ID, "id_owner")).select_by_visible_text(obtain_credentials['admin_name'])
Select(selenium.find_element(By.ID, "id_subject")).select_by_visible_text(prepare_course_data['subject'])
selenium.find_element(By.ID, "id_title").send_keys(prepare_course_data['course_name'])
selenium.find_element(By.ID, "id_overview").send_keys(prepare_course_data['overview'])
selenium.find_element(By.CLASS_NAME, "default").click()
@then(parsers.parse("Hệ thống hiển thị liên kết chứa tên:{course_name}"))
def verify_course_exists(prepare_course_data):
"""Xác thực khóa học đã được tạo"""
assert EC.presence_of_element_located((By.LINK_TEXT, prepare_course_data['course_name']))
@then("Xóa khóa học vừa tạo để dọn dẹp dữ liệu")
def cleanup_course(selenium, prepare_course_data):
"""Xóa dữ liệu kiểm thử"""
selenium.find_element(By.LINK_TEXT, prepare_course_data['course_name']).click()
selenium.find_element(By.CLASS_NAME, "deletelink").click()
selenium.find_element(By.CSS_SELECTOR, "input[type='submit']").click()
Tham số selenium trong các hàm thường được cung cấp bởi fixture từ pytest-selenium, đại diện cho trình duyệt đang chạy. Module parsers giúp trích xuất biến động từ câu lệnh trong file feature. Các hàm step definition hoạt động tương tự như fixture trong pytest, cho phép truyền dữ liệu giữa các bước.
Thực Thi Kịch Bản Kiểm Thử
Sau khi định nghĩa các bước, cần tạo file test để kích hoạt kịch bản. File này sẽ liên kết file .feature với bộ chạy kiểm thử pytest.
# File: test_lms.py
from pytest_bdd import scenario
from steps import *
@scenario("lms_features.feature", "Quản trị viên tạo khóa học mới")
def test_course_creation():
pass
Để chạy kiểm thử, sử dụng lệnh pytest kèm theo tham số chỉ định trình duyệt. Ví dụ, để chạy trên Chrome:
pytest test_lms.py --driver Chrome
Hạn Chế Của Cách Tiếp Cận Cơ Bản
Mặc dù BDD giúp chuẩn hóa ngôn ngữ giao tiếp giữa tester và developer, việc triển khai naive (mỗi câu trong feature file đều cần một hàm python tương ứng) có thể dẫn đến chi phí bảo trì cao. Nếu giao diện người dùng thay đổi frequently, các step definition cần được cập nhật liên tục. Ngoài ra, nếu không có sự tham gia sâu của developer vào việc viết scenario, lợi ích chính của BDD là giảm thiểu sự hiểu lầm về nghiệp vụ có thể không được phát huy tối đa, biến quy trình này thành một lớp wrapper phức tạp cho automation script thông thường.