Các nhà phát triển Python chọn pytest vì nó giúp kiểm thử API bằng mã Python thuần: test chỉ là một hàm bắt đầu bằng test_, assertion là câu lệnh assert, còn pytest lo phần phát hiện, chạy và báo cáo lỗi. Kết hợp pytest với requests, bạn có một bộ kiểm thử API gọn, dễ đưa vào CI và không cần framework nặng.
Bài viết này hướng dẫn cách xây dựng một bộ kiểm thử API với pytest: thiết lập project, viết request đầu tiên, dùng fixture để chia sẻ setup, chạy cùng một test với nhiều input bằng parametrize, và assert status code, response body, JSON Schema.
Thiết lập dự án
Tạo virtual environment và cài các thư viện cần thiết:
python -m venv .venv
source .venv/bin/activate
pip install pytest requests jsonschema
Một cấu trúc project đơn giả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 test nếu bạn tuân thủ các quy ước sau:
- File test bắt đầu bằng
test_hoặc kết thúc bằng_test.py - Hàm test bắt đầu bằng
test_ - Class test bắt đầu bằng
Testvà không có__init__
Ví dụ:
test_users.py
test_orders.py
Nếu bạn mới bắt đầu với kiểm thử tự động, xem thêm hướng dẫn về kiểm thử tự động là gì.
Với kiểm thử API, requests xử lý HTTP, còn pytest xử lý discovery, assertion output, fixtures, data-driven testing bằng parametrize, và báo cáo kết quả. Nhờ đó, bạn có thể giữ test trong cùng repository với mã ứng dụng, giúp lỗi và test thất bại xuất hiện ngay trong cùng pull request.
Viết kiểm thử API đầu tiên
Một test API thường gồm 3 bước:
- Gửi request
- Parse response nếu cần
- Assert kết quả mong đợi
Ví dụ:
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 test:
pytest -v
Khi assertion thất bại, pytest hiển thị giá trị thực tế và giá trị mong đợi rất rõ ràng. Bạn không cần dùng các method assertion riêng như trong unittest; chỉ cần assert.
Một số thứ nên assert trong response API:
- HTTP status code
- Field bắt buộc trong JSON body
- Kiểu dữ liệu
- Giá trị business quan trọng
- Header
- Thời gian phản hồi nếu cần
Xem thêm về các khẳng định API.
Chia sẻ setup với fixtures
Không nên lặp lại BASE_URL, session HTTP hoặc token xác thực trong từng test. Pytest fixtures giúp tái sử dụng setup.
Tạo conftest.py:
# 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"]
Giải thích nhanh:
-
scope="session": tạorequests.Session()một lần cho toàn bộ test run. -
yield: phần trướcyieldlà setup, phần sau là cleanup. - Fixture trong
conftest.pyđược pytest tự động load, không cần import thủ công.
Dùng fixture trong test:
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à cách hiện đại hơn so với setup_function và teardown_function. Chúng rõ ràng, dễ compose, hỗ trợ scope, và được khuyến nghị trong tài liệu fixtures chính thức của pytest.
Chạy một test với nhiều input
API thường cần được kiểm tra với nhiều trường hợp:
- Input hợp lệ
- Input không tồn tại
- Input sai định dạng
- Boundary cases
Thay vì viết nhiều hàm test gần giống nhau, dùng @pytest.mark.parametrize.
import pytest
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
Đoạn trên tạo 4 test case độc lập từ một hàm test. Khi một case fail, các case còn lại vẫn chạy và được báo cáo riêng.
Khi input lớn hơn, bạn có thể đọc dữ liệu từ file CSV hoặc JSON. Xem thêm hướng dẫn về kiểm thử API dựa trên dữ liệu với CSV và JSON.
Nếu chưa chắc endpoint nên trả về status code nào, tham khảo thêm các mã trạng thái HTTP mà REST API nên sử dụng.
Assert response body và schema
Status code đúng chưa đủ. Một response 200 vẫn có thể sai nếu JSON body thiếu field, sai kiểu dữ liệu hoặc đổi cấu trúc.
Ví dụ assert trực tiếp trên JSON body:
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
Để kiểm tra cấu trúc response chặt hơn, dùng JSON Schema:
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)
Schema validation giúp bắt các lỗi như:
- Thiếu field bắt buộc
- Field bị đổi tên
- Sai kiểu dữ liệu
- Giá trị không thỏa điều kiện tối thiểu
Thư viện jsonschema là lựa chọn phổ biến cho Python. Xem thêm tài liệu xác thực.
Chạy bộ kiểm thử trong CI
Pytest trả về exit code khác 0 khi có test fail, nên rất phù hợp để chạy trong CI.
Lệnh cơ bản:
pytest -v
Xuất báo cáo JUnit XML:
pytest -v --junitxml=results.xml
Bạn có thể đưa lệnh này vào GitHub Actions, GitLab CI, Jenkins hoặc bất kỳ pipeline nào khác. Xem thêm hướng dẫn về kiểm thử API trong các pipeline CI/CD.
Hai thực hành quan trọng khi chạy trong CI:
1. Đọc cấu hình từ biến môi trường
Không hard-code URL môi trường hoặc secret trong test.
import os
BASE_URL = os.environ.get("API_BASE_URL", "https://staging.example.com/v1")
Với token hoặc password, dùng secret manager của CI thay vì commit vào repository.
2. Chạy song song khi test độc lập
Cài plugin:
pip install pytest-xdist
Chạy test song song:
pytest -n auto
Điều kiện quan trọng: test không được phụ thuộc vào thứ tự chạy và không chia sẻ trạng thái gây xung đột. Nếu một test tạo dữ liệu, nó nên tự cleanup hoặc dùng dữ liệu riêng.
Giữ bộ kiểm thử pytest dễ bảo trì
Một bộ 50 test có thể vẫn dễ đọc. Một bộ 500 test cần cấu trúc rõ ràng.
Nhóm test theo domain hoặc endpoint
Ví dụ:
test_users.py
test_orders.py
test_payments.py
Chỉ dùng class khi thật sự cần chia sẻ setup hoặc nhóm logic liên quan. Với pytest, các hàm test độc lập thường dễ đọc hơn.
Dùng marks để chạy subset
Khai báo marks trong pytest.ini:
[pytest]
markers =
smoke: quick smoke tests
slow: slower full regression tests
Dùng trong test:
import pytest
@pytest.mark.smoke
def test_get_user_returns_200(api_session):
response = api_session.get("/users/42")
assert response.status_code == 200
Chạy smoke test:
pytest -m smoke
Chạy toàn bộ trừ test chậm:
pytest -m "not slow"
Tập trung cấu hình và helper
Các phần sau nên nằm trong conftest.py hoặc module cấu hình riêng:
- Base URL
- Session HTTP
- Auth token
- Shared headers
- Schema dùng lại nhiều lần
- Helper tạo dữ liệu test
Nguyên tắc đơn giản: nếu bạn copy-paste lần thứ hai, hãy cân nhắc đưa nó vào fixture hoặc helper.
Xem thêm hướng dẫn về viết script kiểm thử tự động.
Khi nào nên dùng một nền tảng chuyên dụng thay thế
Pytest phù hợp khi nhóm của bạn viết Python và muốn test nằm cạnh mã ứng dụng. Tuy nhiên, nó có thể kém thuận tiện hơn nếu:
- QA hoặc Product cần cùng tham gia viết test
- Bạn muốn thiết kế test, mock API và chạy test ở cùng một nơi
- Bạn không muốn duy trì nhiều fixture, helper và assertion thủ công
- Bạn cần xác thực schema dựa trên OpenAPI một cách trực quan
Apidog giải quyết khoảng trống đó bằng giao diện xây dựng test trực quan, xác thực schema dựa trên OpenAPI, chạy data-driven test từ CSV/JSON, và CLI runner cho CI mà không cần tự viết nhiều mã fixture.
Nhiều nhóm dùng cả hai: pytest cho các kịch bản cần logic Python phức tạp, và Apidog để mở rộng coverage, thiết kế API và mocking. Bạn có thể tải Apidog và so sánh hai cách tiếp cận trên một endpoint thực tế.
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 ít boilerplate hơn. Test là hàm thông thường, assertion dùng assert, fixture linh hoạt hơn setup dựa trên class của unittest, và parametrize hỗ trợ data-driven testing ngay từ đầu. Pytest cũng có hệ sinh thái plugin lớn và vẫn có thể chạy các test kiểu unittest hiện có.
Sự khác biệt giữa fixture và parametrize là gì?
Fixture cung cấp tài nguyên tái sử dụng cho test, ví dụ HTTP session hoặc auth token.
parametrize chạy cùng một test nhiều lần với các input khác nhau.
Chúng có thể dùng cùng nhau:
@pytest.mark.parametrize("user_id", [1, 2, 3])
def test_get_user(api_session, user_id):
response = api_session.get(f"/users/{user_id}")
assert response.status_code == 200
Tôi có nên assert thời gian phản hồi trong pytest không?
Có thể, ví dụ:
assert response.elapsed.total_seconds() < 1.0
Nhưng pytest là công cụ kiểm thử chức năng, không phải công cụ kiểm thử tải. Nếu assert thời gian, hãy đặt ngưỡng đủ rộng để tránh fail ngẫu nhiên do biến động mạng.
Làm thế nào để giữ các kiểm thử API độc lập?
Mỗi test nên có dữ liệu riêng hoặc fixture tạo/dọn dữ liệu cho nó. Tránh phụ thuộc vào thứ tự chạy test. Một bộ test độc lập sẽ dễ debug hơn và có thể chạy song song bằng pytest-xdist.
Pytest có thể xác thực response dựa trên OpenAPI không?
Bản thân pytest không làm việc đó trực tiếp, nhưng bạn có thể dùng jsonschema hoặc plugin hỗ trợ OpenAPI. Nếu schema validation dựa trên OpenAPI là phần quan trọng trong workflow, một nền tảng như Apidog có thể giúp giảm phần thiết lập thủ công.
Top comments (0)