Các nhà phát triển Python chọn pytest vì nó giúp họ tập trung vào việc chính. Một kiểm thử chỉ đơn giản là một hàm có tên bắt đầu bằng test_, một khẳng định (assertion) là một câu lệnh assert thông thường, và trình chạy sẽ làm phần còn lại. Kết hợp nó với thư viện requests, bạn sẽ có một framework hoàn chỉnh, ưu tiên mã nguồn để tự động hóa các kiểm thử API, mà không cần các nghi thức phức tạp.
Hướng dẫn này sẽ chỉ cho bạn cách xây dựng một bộ kiểm thử API pytest thực sự. Bạn sẽ thiết lập dự án, viết kiểm thử request đầu tiên của mình, chia sẻ logic thiết lập với fixtures, chạy cùng một kiểm thử với nhiều đầu vào bằng parametrize, và khẳng định (assert) trạng thái phản hồi, nội dung (body) và JSON Schema. Mỗi ví dụ đều sử dụng một API công cộng thực tế để bạn có thể điều chỉnh mã trực tiếp.
Thiết lập dự án
Cài đặt hai thư viện bạn cần vào một môi trường ảo:
python -m venv .venv
source .venv/bin/activate
pip install pytest requests jsonschema
Một bố cục rõ ràng giúp bộ kiểm thử dễ bảo trì khi nó phát triển:
api-tests/
conftest.py # shared fixtures
test_users.py # tests for the users endpoints
test_orders.py # tests for the orders endpoints
pytest.ini # configuration
Pytest tự động phát hiện các kiểm thử. Các tệp phải bắt đầu bằng test_ hoặc kết thúc bằng _test.py, các hàm phải bắt đầu bằng test_, và các lớp kiểm thử phải bắt đầu bằng Test và không có phương thức __init__. Tuân thủ các quy tắc đó và bạn sẽ không bao giờ phải cấu hình việc phát hiện thủ công. Nếu kiểm thử tự động là một lĩnh vực mới đối với bạn, hướng dẫn cơ bản của chúng tôi về kiểm thử tự động là gì sẽ cung cấp bối cảnh.
Tại sao lại là pytest cho kiểm thử API? Thư viện requests xử lý HTTP, và pytest xử lý mọi thứ xung quanh nó: phát hiện, khẳng định (assertion) với đầu ra lỗi dễ đọc, thiết lập và dọn dẹp thông qua fixtures, chạy dựa trên dữ liệu thông qua parametrize và báo cáo. Bạn sẽ xây dựng một framework tự động hóa API hoàn chỉnh từ hai thư viện nhỏ, được tài liệu hóa tốt, bằng một ngôn ngữ mà nhóm của bạn có thể đã sử dụng cho chính ứng dụng. Sự gần gũi đó rất quan trọng. Các kiểm thử nằm trong cùng một kho lưu trữ với mã nguồn sẽ luôn trung thực, vì một thay đổi gây lỗi và kiểm thử thất bại của nó sẽ xuất hiện trong cùng một pull request.
Viết kiểm thử API đầu tiên của bạn
Một kiểm thử API pytest gửi một request và khẳng định (assert) trên phản hồi. Dưới đây là một kiểm thử cho một endpoint người dùng:
import requests
BASE_URL = "https://api.example.com/v1"
def test_get_user_returns_200():
response = requests.get(f"{BASE_URL}/users/42")
assert response.status_code == 200
def test_get_user_returns_expected_fields():
response = requests.get(f"{BASE_URL}/users/42")
body = response.json()
assert body["id"] == 42
assert "email" in body
assert body["status"] == "active"
Chạy bộ kiểm thử với pytest -v. Mỗi câu lệnh assert thất bại sẽ tạo ra một báo cáo chi tiết hiển thị giá trị thực tế, đây là một trong những tính năng tốt nhất của pytest. Bạn không cần các phương thức khẳng định đặc biệt; framework sẽ viết lại các câu lệnh assert thông thường để cung cấp đầu ra phong phú. Để biết thêm các kiểm tra rộng hơn đáng làm trên một phản hồi, hãy xem hướng dẫn của chúng tôi về các khẳng định API.
Chia sẻ thiết lập với fixtures
Lặp lại URL cơ sở và một phiên HTTP trong mọi kiểm thử là lãng phí. Fixtures giải quyết vấn đề này. Một fixture là một hàm được trang trí với @pytest.fixture tạo ra một giá trị mà các kiểm thử có thể yêu cầu bằng cách đặt tên nó như một tham số.
Đặt các fixture được chia sẻ trong conftest.py để mọi tệp kiểm thử có thể sử dụng chúng mà không cần nhập (import):
# conftest.py
import pytest
import requests
BASE_URL = "https://api.example.com/v1"
@pytest.fixture(scope="session")
def api_session():
session = requests.Session()
session.headers.update({"Accept": "application/json"})
yield session
session.close()
@pytest.fixture
def auth_token(api_session):
response = api_session.post(
f"{BASE_URL}/auth/login",
json={"email": "qa@example.com", "password": "test-pass"},
)
return response.json()["token"]
Đối số scope="session" có nghĩa là phiên sẽ được tạo một lần cho toàn bộ quá trình chạy thay vì cho mỗi kiểm thử. Từ khóa yield phân tách thiết lập khỏi dọn dẹp: mã trước yield chạy trước, mã sau chạy khi fixture ra khỏi phạm vi. Một kiểm thử chỉ đơn giản yêu cầu những gì nó cần:
def test_create_order(api_session, auth_token):
response = api_session.post(
f"{BASE_URL}/orders",
headers={"Authorization": f"Bearer {auth_token}"},
json={"product_id": 7, "quantity": 2},
)
assert response.status_code == 201
assert response.json()["status"] == "pending"
Fixtures là sự thay thế hiện đại của pytest cho kiểu setup_function và teardown_function cũ hơn. Chúng kết hợp một cách sạch sẽ, hỗ trợ các phạm vi và làm cho các phụ thuộc trở nên rõ ràng, đó là lý do tại sao tài liệu fixtures chính thức của pytest khuyến nghị chúng là phương pháp mặc định.
Chạy một kiểm thử với nhiều đầu vào
Các endpoint API thường cần được kiểm tra với nhiều đầu vào: giá trị hợp lệ, giá trị không hợp lệ và các trường hợp biên. Viết một hàm riêng cho mỗi trường hợp là tẻ nhạt. Decorator @pytest.mark.parametrize chạy một nội dung kiểm thử với một danh sách các đầu vào:
import pytest
import requests
BASE_URL = "https://api.example.com/v1"
@pytest.mark.parametrize("user_id,expected_status", [
(42, 200),
(99999, 404),
(0, 404),
(-1, 400),
])
def test_get_user_status_codes(api_session, user_id, expected_status):
response = api_session.get(f"{BASE_URL}/users/{user_id}")
assert response.status_code == expected_status
Điều này tạo ra bốn trường hợp kiểm thử riêng biệt từ một hàm. Mỗi trường hợp chạy và báo cáo độc lập, vì vậy một đầu vào xấu không che giấu những cái khác. Parametrize là câu trả lời tích hợp của pytest cho kiểm thử dựa trên dữ liệu (data-driven testing). Khi tập hợp đầu vào lớn, hãy tải nó từ một tệp; hướng dẫn của chúng tôi về kiểm thử API dựa trên dữ liệu với CSV và JSON bao gồm mẫu đó. Nếu bạn không chắc mã trạng thái nào mỗi đầu vào nên trả về, tài liệu tham khảo về các mã trạng thái HTTP mà REST API nên sử dụng là một tài liệu bổ trợ hữu ích.
Khẳng định (Assert) trên nội dung phản hồi và schema
Mã trạng thái là cần thiết nhưng chưa đủ. Một phản hồi 200 với nội dung bị định dạng sai vẫn là một lỗi. Hãy khẳng định (assert) trực tiếp trên JSON đã được phân tích cú pháp:
def test_order_response_shape(api_session, auth_token):
response = api_session.post(
f"{BASE_URL}/orders",
headers={"Authorization": f"Bearer {auth_token}"},
json={"product_id": 7, "quantity": 2},
)
body = response.json()
assert isinstance(body["id"], int)
assert body["quantity"] == 2
assert body["total"] > 0
assert response.elapsed.total_seconds() < 1.0
Để có các đảm bảo mạnh mẽ hơn, hãy xác thực nội dung với JSON Schema. Điều này bắt lỗi các thay đổi cấu trúc, chẳng hạn như một trường bị đổi tên hoặc thiếu, mà các kiểm tra viết tay có thể bỏ sót:
from jsonschema import validate
order_schema = {
"type": "object",
"required": ["id", "product_id", "quantity", "status", "total"],
"properties": {
"id": {"type": "integer"},
"product_id": {"type": "integer"},
"quantity": {"type": "integer", "minimum": 1},
"status": {"type": "string"},
"total": {"type": "number"},
},
}
def test_order_matches_schema(api_session, auth_token):
response = api_session.post(
f"{BASE_URL}/orders",
headers={"Authorization": f"Bearer {auth_token}"},
json={"product_id": 7, "quantity": 2},
)
validate(instance=response.json(), schema=order_schema)
Xác thực schema hiệu quả hơn các khẳng định từng trường vì một schema bao gồm toàn bộ hình dạng phản hồi. Thư viện jsonschema là lựa chọn tiêu chuẩn, và tài liệu xác thực của nó giải thích các từ khóa được hỗ trợ.
Chạy bộ kiểm thử trong CI
Một bộ kiểm thử pytest phát huy giá trị khi nó chạy tự động. Pytest trả về một mã thoát khác 0 khi thất bại, đây chính xác là những gì một máy chủ CI cần để làm cho một bản build thất bại. Phát ra một báo cáo JUnit để hiển thị nội tuyến:
pytest -v --junitxml=results.xml
Kết nối lệnh đó vào một bước GitHub Actions hoặc bất kỳ pipeline nào khác và các kiểm thử API của bạn sẽ bảo vệ mọi commit. Hướng dẫn của chúng tôi về kiểm thử API trong các pipeline CI/CD hiển thị thiết lập đầy đủ, bao gồm việc chèn bí mật cho token và lựa chọn môi trường.
Hai thói quen CI giúp bộ kiểm thử pytest đáng tin cậy. Thứ nhất, không bao giờ mã hóa cứng các bí mật hoặc URL môi trường trong các tệp kiểm thử. Đọc chúng từ các biến môi trường để cùng một bộ kiểm thử chạy đối với môi trường staging trong CI và cục bộ mà không cần chỉnh sửa:
import os
BASE_URL = os.environ.get("API_BASE_URL", "https://staging.example.com/v1")
Thứ hai, chạy các kiểm thử độc lập song song để giữ phản hồi nhanh. Plugin pytest-xdist phân tán các kiểm thử trên các lõi CPU với pytest -n auto. Các lần chạy song song chỉ hoạt động nếu các kiểm thử của bạn không chia sẻ trạng thái, đây là một lý do nữa khiến lớp dữ liệu kiểm thử quan trọng. Một bộ kiểm thử phụ thuộc vào thứ tự thực hiện sẽ thất bại không thể đoán trước ngay khi nó chạy song song.
Giữ cho bộ kiểm thử pytest dễ bảo trì
Một bộ năm mươi kiểm thử thì dễ dàng. Một bộ năm trăm cần kỷ luật. Ba thực hành giúp một framework API pytest khỏe mạnh khi nó phát triển.
Nhóm các kiểm thử liên quan vào các module và chỉ sử dụng các lớp khi chúng chia sẻ thiết lập, không phải để trang trí. Một tệp test_orders.py với một bộ hàm rõ ràng dễ đọc hơn là một tệp khổng lồ. Sử dụng các đánh dấu (marks), đã đăng ký trong pytest.ini, để gắn thẻ các kiểm thử để bạn có thể chạy các tập hợp con: @pytest.mark.smoke cho một cổng nhanh, @pytest.mark.slow cho toàn bộ quá trình quét. Chạy tập hợp smoke trên mỗi commit và toàn bộ tập hợp hàng đêm.
Tập trung cấu hình. Các URL cơ sở, schema và các fixture được chia sẻ thuộc về conftest.py hoặc một module cấu hình nhỏ, không bao giờ sao chép và dán giữa các tệp. Khi URL môi trường staging thay đổi, bạn chỉ cần chỉnh sửa một dòng. Kỷ luật module tương tự áp dụng cho bất kỳ framework nào, được đề cập trong hướng dẫn của chúng tôi về viết script kiểm thử tự động, cũng áp dụng ở đây: trích xuất bất cứ thứ gì bạn viết hai lần vào một fixture hoặc helper.
Khi nào nên dùng một nền tảng chuyên dụng thay thế
Một framework pytest là tuyệt vời khi nhóm của bạn viết Python và muốn các kiểm thử nằm cạnh mã ứng dụng. Nó ít tiện lợi hơn khi nhân viên QA hoặc sản phẩm cần đóng góp, hoặc khi bạn muốn thiết kế kiểm thử, mocking và thực thi ở một nơi mà không cần duy trì mã "glue code".
Apidog lấp đầy khoảng trống đó. Nó cung cấp khả năng xây dựng kiểm thử trực quan, xác thực schema dựa trên thông số kỹ thuật OpenAPI của bạn, chạy dựa trên dữ liệu từ CSV và JSON, và một trình chạy CLI cho CI, tất cả mà không cần viết mã fixture và assertion thủ công. Nhiều nhóm sử dụng cả hai: pytest cho các kịch bản nặng về logic và Apidog để có phạm vi bao phủ rộng và để thiết kế và mocking các API mà bộ kiểm thử pytest kiểm tra. Bạn có thể tải Apidog và so sánh hai phương pháp trên một endpoint thực tế chỉ trong một buổi chiều.
Các câu hỏi thường gặp
Tại sao nên dùng pytest thay vì unittest tích hợp sẵn của Python cho kiểm thử API?
Pytest cần ít mã rườm rà (boilerplate) hơn. Các kiểm thử là các hàm thông thường, các khẳng định là các câu lệnh assert thông thường với đầu ra lỗi phong phú, và các fixture xử lý việc thiết lập linh hoạt hơn so với các phương thức dựa trên lớp của unittest. Pytest cũng có một hệ sinh thái plugin lớn và parametrize tích hợp sẵn cho các kiểm thử dựa trên dữ liệu. Nó vẫn có thể chạy các kiểm thử kiểu unittest hiện có, vì vậy việc chuyển đổi có rủi ro thấp.
Sự khác biệt giữa fixture và parametrize là gì?
Một fixture cung cấp một tài nguyên có thể tái sử dụng, chẳng hạn như phiên HTTP hoặc token xác thực, cho bất kỳ kiểm thử nào yêu cầu nó. Parametrize chạy cùng một nội dung kiểm thử nhiều lần với các giá trị đầu vào khác nhau. Fixtures chia sẻ thiết lập; parametrize nhân rộng các trường hợp. Chúng kết hợp tốt với nhau: một kiểm thử được parametrize vẫn có thể phụ thuộc vào fixtures.
Tôi có nên khẳng định (assert) về thời gian phản hồi trong các kiểm thử API pytest không?
Bạn có thể, sử dụng response.elapsed.total_seconds(), và một giới hạn trên lỏng lẻo có thể bắt được các lỗi hiệu suất nghiêm trọng. Nhưng pytest là một công cụ kiểm thử chức năng, không phải công cụ kiểm thử tải. Đối với công việc hiệu suất thực sự, hãy sử dụng một công cụ chuyên dụng. Giữ các khẳng định thời gian ở mức rộng rãi để sự biến động mạng thông thường không gây ra các lỗi không nhất quán.
Làm thế nào để giữ các kiểm thử API độc lập trong pytest?
Cung cấp cho mỗi kiểm thử dữ liệu riêng thông qua các fixture tạo và dọn dẹp tài nguyên, và tránh phụ thuộc vào thứ tự thực hiện kiểm thử. Pytest chạy các kiểm thử theo thứ tự tệp theo mặc định, nhưng một bộ kiểm thử được thiết kế tốt không phụ thuộc vào điều đó. Các kiểm thử độc lập có thể chạy song song và thất bại một cách riêng biệt, điều này giúp việc gỡ lỗi dễ dàng hơn nhiều.
Pytest có thể xác thực phản hồi dựa trên thông số kỹ thuật OpenAPI không?
Bản thân Pytest thì không, nhưng bạn có thể xác thực với JSON Schema bằng thư viện jsonschema, và có các plugin kiểm tra phản hồi dựa trên tài liệu OpenAPI. Nếu xác thực schema là trọng tâm trong quy trình làm việc của bạn, một nền tảng như Apidog tự động xác thực với thông số kỹ thuật OpenAPI của bạn có thể giúp bạn tiết kiệm việc thiết lập plugin.
