DEV Community

Cover image for Khung Kiểm Thử Tự Động API với Pytest: Hướng Dẫn Thực Hành
Sebastian Petrus
Sebastian Petrus

Posted on • Originally published at apidog.com

Khung Kiểm Thử Tự Động API với Pytest: Hướng Dẫn Thực Hành

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.

Dùng thử Apidog ngay hôm nay

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 Test và không có __init__

Ví dụ:

test_users.py
test_orders.py
Enter fullscreen mode Exit fullscreen mode

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:

  1. Gửi request
  2. Parse response nếu cần
  3. 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"
Enter fullscreen mode Exit fullscreen mode

Chạy test:

pytest -v
Enter fullscreen mode Exit fullscreen mode

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"]
Enter fullscreen mode Exit fullscreen mode

Giải thích nhanh:

  • scope="session": tạo requests.Session() một lần cho toàn bộ test run.
  • yield: phần trước yield là 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"
Enter fullscreen mode Exit fullscreen mode

Fixtures là cách hiện đại hơn so với setup_functionteardown_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
Enter fullscreen mode Exit fullscreen mode

Đ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
Enter fullscreen mode Exit fullscreen mode

Để 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)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Xuất báo cáo JUnit XML:

pytest -v --junitxml=results.xml
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Chạy test song song:

pytest -n auto
Enter fullscreen mode Exit fullscreen mode

Đ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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Chạy smoke test:

pytest -m smoke
Enter fullscreen mode Exit fullscreen mode

Chạy toàn bộ trừ test chậm:

pytest -m "not slow"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)