Product Management
Đăng nhập
ESC

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

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

API Automation — Python requests + pytest

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

Nếu bạn hỏi mười công ty công nghệ ở Việt Nam đang tuyển SDET rằng "backend team của các bạn code bằng gì?", phần lớn câu trả lời sẽ là Python, Java, hoặc Node. Nhưng nếu hỏi tiếp "team QA automation dùng ngôn ngữ gì để test API?", bạn sẽ thấy Python xuất hiện với tần suất áp đảo — kể cả ở những nơi backend viết bằng Java. Lý do rất thực tế: Python đọc gần như tiếng Anh, thư viện requests gọn gàng đến mức viết một request HTTP chỉ mất một dòng, và pytest là một trong những test framework linh hoạt nhất từng được tạo ra.

Trong bài trước bạn đã học REST Assured cho hệ sinh thái Java. Bài này là "người anh em" của nó trong thế giới Python. Nhưng đừng nghĩ đây chỉ là học lại một cú pháp khác. Cách Python tiếp cận API automation có triết lý riêng: nhẹ, nhanh để dựng, dễ mở rộng, và cực kỳ phù hợp cho những team QA mà không phải ai cũng là lập trình viên chuyên nghiệp. Một bạn manual tester chuyển sang automation thường học requests + pytest nhanh hơn nhiều so với REST Assured, và đây chính là con đường vào nghề SDET của rất nhiều người.

Bài học này tập trung vào một stack cụ thể và phổ biến nhất: requests để gọi HTTP, pytest làm khung test, jsonschema để kiểm tra cấu trúc response, và pytest-html để xuất báo cáo. Học xong, bạn sẽ tự tay dựng được một bộ test API chạy được trong CI, có báo cáo đẹp, và cấu trúc đủ tốt để mở rộng lên hàng trăm test case mà không rối.

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

requests — trái tim của việc gọi API

requests là thư viện HTTP client của Python. Nó biến những thao tác network phức tạp thành các lời gọi hàm đơn giản. Một GET request cơ bản:

import requests

resp = requests.get("https://api.example.vn/v1/products/42") print(resp.status_code) # 200 print(resp.json()) # dict Python đã parse sẵn từ JSON

Đối tượng Response trả về chứa mọi thứ bạn cần để assert: status_code, headers, .json() (tự parse JSON thành dict/list), .text (raw body), .elapsed (thời gian phản hồi). Với POST/PUT, bạn truyền dữ liệu qua tham số json= để requests tự serialize và tự set header Content-Type: application/json:

resp = requests.post(
    "https://api.example.vn/v1/orders",
    json={"product_id": 42, "quantity": 2},
    headers={"Authorization": "Bearer <token>"},
)

Một điểm quan trọng cho automation nghiêm túc: dùng requests.Session() thay vì gọi hàm module trực tiếp. Session giữ lại cookie, tái sử dụng kết nối TCP (nhanh hơn khi chạy nhiều request), và cho phép set header chung một lần cho toàn bộ request — ví dụ token xác thực.

pytest — khung tổ chức và chạy test

pytest không bắt bạn viết class hay kế thừa gì cả. Một test chỉ là một hàm bắt đầu bằng test_, và assertion chỉ là câu lệnh assert thuần Python:

def test_get_product_returns_200():
    resp = requests.get("https://api.example.vn/v1/products/42")
    assert resp.status_code == 200
    assert resp.json()["id"] == 42

Điều làm pytest mạnh không phải cú pháp đơn giản, mà là hai cơ chế: fixtureparametrize.

Fixture là cách pytest quản lý những thứ được chia sẻ và tái sử dụng — như một Session đã đăng nhập, một base URL, hay dữ liệu test cần dọn dẹp sau khi chạy. Bạn khai báo fixture một lần, các test "xin" nó bằng cách nhận làm tham số:

import pytest

@pytest.fixture(scope="session") def api(): session = requests.Session() session.headers.update({"Authorization": "Bearer <token>"}) session.base_url = "https://api.example.vn/v1" yield session session.close() # teardown chạy sau khi test xong

def test_list_products(api): resp = api.get(f"{api.base_url}/products") assert resp.status_code == 200

scope="session" nghĩa là fixture chỉ chạy một lần cho cả phiên test, không lặp lại cho từng hàm — rất quan trọng để không đăng nhập lại hàng trăm lần.

Parametrize cho phép chạy cùng một logic test với nhiều bộ dữ liệu khác nhau, mỗi bộ tính là một test case riêng trong báo cáo:

@pytest.mark.parametrize("product_id,expected_status", [
    (42, 200),
    (999999, 404),
    ("abc", 400),
])
def test_product_status(api, product_id, expected_status):
    resp = api.get(f"{api.base_url}/products/{product_id}")
    assert resp.status_code == expected_status

jsonschema — kiểm tra cấu trúc, không chỉ giá trị

Assert từng field một sẽ mệt mỏi và dễ sót. jsonschema cho phép bạn định nghĩa "hợp đồng" của response — các field bắt buộc, kiểu dữ liệu, ràng buộc — rồi validate cả response chỉ trong một dòng:

from jsonschema import validate

product_schema = { "type": "object", "required": ["id", "name", "price"], "properties": { "id": {"type": "integer"}, "name": {"type": "string"}, "price": {"type": "number", "minimum": 0}, }, }

def test_product_schema(api): resp = api.get(f"{api.base_url}/products/42") validate(instance=resp.json(), schema=product_schema)

Cách này bắt được cả những lỗi âm thầm: backend đổi price từ number sang string, hoặc quên trả field name. Đây là loại bug mà assert giá trị đơn lẻ dễ bỏ qua.

pytest-html — báo cáo cho người không đọc log

pytest-html là plugin xuất kết quả test thành một file HTML tự chứa, có màu, có thống kê pass/fail, thời gian chạy, và cả traceback của test lỗi. Sếp QA hay PM không muốn đọc log terminal — họ muốn một file mở lên là hiểu. Chạy đơn giản:

pytest --html=report.html --self-contained-html

Tình huống thực tế

Tình huống 1: Sàn TMĐT ở TP.HCM và bug "giá âm"

Một công ty thương mại điện tử tầm trung ở TP.HCM (giả định, khoảng 40 dev) có API /v1/products phục vụ app mobile. Team QA ban đầu chỉ test bằng Postman thủ công, mỗi lần release mất nửa ngày click tay. Họ chuyển sang requests + pytest và viết schema validation cho toàn bộ 12 endpoint chính.

Ba tuần sau, một PR backend vô tình để logic khuyến mãi tính ra price: -15000 khi giảm giá vượt quá giá gốc. Assert giá trị thông thường không bắt được vì test chỉ kiểm tra status_code == 200. Nhưng schema có ràng buộc "minimum": 0 cho price, nên bộ test CI fail ngay trước khi merge. Bug đó, nếu lọt lên production, sẽ hiển thị giá âm trên app trong giờ flash sale.

Bài học: schema validation không chỉ kiểm tra "có field không" mà còn kiểm tra ràng buộc giá trị. Nó là lưới an toàn cho những bug logic mà bạn không nghĩ tới lúc viết test.

Tình huống 2: Fintech và bài toán token hết hạn

Một startup fintech ở Hà Nội test API ví điện tử. Token xác thực chỉ sống 15 phút, mà bộ test đầy đủ chạy mất gần 20 phút. Ban đầu họ lấy token trong mỗi test bằng cách hardcode — kết quả là nửa cuối bộ test luôn fail với lỗi 401 vì token đã hết hạn.

Giải pháp là một fixture scope="session" lấy token một lần đầu, nhưng thêm cơ chế kiểm tra và tự refresh khi gần hết hạn. Họ bọc Session trong một class nhỏ, override phương thức để tự động gọi lại /auth/refresh khi nhận 401, rồi retry request. Sau khi sửa, tỉ lệ test fail giả (do hạ tầng, không phải do bug thật) giảm từ khoảng 30% xuống gần 0.

Bài học: xác thực là phần khó nhất của API automation thực tế, không phải phần gọi request. Thiết kế fixture quản lý token đúng cách quan trọng hơn việc viết thêm nhiều test case.

Tình huống 3: Team outsource và báo cáo cho khách Nhật

Một công ty outsource ở Đà Nẵng làm dự án cho khách hàng Nhật Bản. Khách yêu cầu mỗi lần chạy regression phải có báo cáo rõ ràng gửi kèm, không chấp nhận "chúng tôi đã test rồi". Team dùng pytest-html sinh báo cáo tự động trong pipeline, đính kèm cả response body của các test fail nhờ hook pytest_html_results_table_row.

Điều bất ngờ: chính việc phải trình bày báo cáo cho khách khiến team viết test câu chữ rõ ràng hơn (tên test mô tả đúng nghiệp vụ), và tỉ lệ bug lọt xuống production giảm rõ rệt sau hai sprint. Báo cáo đẹp không chỉ để làm hài lòng khách — nó ép chất lượng test đi lên.

Bài học: báo cáo là một phần của sản phẩm test, không phải thứ trang trí thêm. Với dự án outsource hay có khách hàng khó tính, pytest-html là công cụ giao tiếp chứ không chỉ là công cụ kỹ thuật.

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

Bước 1 — Dựng môi trường. Tạo virtual environment để cô lập thư viện, tránh xung đột phiên bản với dự án khác:

python -m venv venv
source venv/bin/activate        # Windows: venv\Scripts\activate
pip install requests pytest jsonschema pytest-html
pip freeze > requirements.txt   # ghi lại để CI cài đúng phiên bản

Bước 2 — Tổ chức thư mục. Cấu trúc rõ ràng ngay từ đầu giúp bộ test không rối khi lớn lên:

api-tests/
├── conftest.py          # fixture dùng chung
├── requirements.txt
├── schemas/
│   └── product.json     # tách schema ra file riêng
└── tests/
    ├── test_products.py
    └── test_orders.py

Bước 3 — Viết conftest.py. Đây là nơi pytest tự động tìm fixture chung. Đặt fixture api (Session đã xác thực) và base_url ở đây để mọi test dùng được mà không cần import:

import pytest
import requests

BASE_URL = "https://api.example.vn/v1"

@pytest.fixture(scope="session") def api(): s = requests.Session() resp = s.post(f"{BASE_URL}/auth/login", json={"username": "qa", "password": "secret"}) token = resp.json()["access_token"] s.headers.update({"Authorization": f"Bearer {token}"}) yield s s.close()

Bước 4 — Viết test đầu tiên kết hợp status, giá trị, và schema:

import json
from jsonschema import validate

def test_get_product(api): resp = api.get(f"{BASE_URL}/products/42") assert resp.status_code == 200

body = resp.json() assert body["id"] == 42

with open("schemas/product.json") as f: schema = json.load(f) validate(instance=body, schema=schema)

Bước 5 — Chạy và xuất báo cáo:

pytest -v --html=report.html --self-contained-html

Cờ -v cho kết quả chi tiết từng test; file report.html mở được trực tiếp trên trình duyệt.

Bước 6 — Mở rộng bằng parametrize để tăng độ phủ mà không nhân đôi code, như ví dụ parametrize ở phần trên. Từ đây bộ test của bạn đã sẵn sàng để cắm vào pipeline CI (chủ đề của các bài sau).

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

Không kiểm tra Content-Type trước khi gọi .json(). Khi API lỗi 500, server có thể trả về trang HTML thay vì JSON. Gọi .json() lúc đó sẽ ném JSONDecodeError khó hiểu, che mất lỗi thật. Mẹo: assert status_code trước, hoặc kiểm tra resp.headers["Content-Type"] trước khi parse.

Quên timeout. requests.get(url) không có timeout mặc định — nếu server treo, test của bạn treo vô hạn và làm nghẽn cả CI. Luôn đặt timeout=, ví dụ api.get(url, timeout=10).

Test phụ thuộc thứ tự chạy. Một test tạo order, test sau đọc order đó. Khi pytest chạy song song hoặc đổi thứ tự, chúng fail. Mỗi test phải tự chuẩn bị dữ liệu của mình (dùng fixture để tạo và dọn dẹp), không dựa vào "rác" của test trước để lại.

Hardcode dữ liệu môi trường. Đừng nhét URL production hay token thật vào code. Dùng biến môi trường: BASE_URL = os.getenv("API_BASE_URL", "https://staging.example.vn"). Điều này cho phép cùng bộ test chạy trên staging và production mà không sửa code.

Assert quá lỏng. assert resp.status_code == 200 là chưa đủ. API có thể trả 200 với body rỗng hoặc sai cấu trúc. Kết hợp status + schema + vài giá trị nghiệp vụ quan trọng mới đủ tin cậy.

Mẹo dùng marker để phân loại test. Dùng @pytest.mark.smoke cho test nhanh, @pytest.mark.regression cho bộ đầy đủ, rồi chạy chọn lọc bằng pytest -m smoke. Rất hữu ích khi bạn muốn chạy nhanh trên mỗi commit và chạy đầy đủ ban đêm.

Mẹo debug bằng -sresp.text. Khi test fail bí ẩn, chạy pytest -s để thấy print, và in resp.status_code cùng resp.text để xem server thực sự trả về gì. Đừng đoán — hãy nhìn body.

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

Dùng một API công khai để luyện (ví dụ https://jsonplaceholder.typicode.com hoặc https://reqres.in):

  • Dựng project. Tạo virtualenv, cài đủ 4 thư viện, tạo cấu trúc thư mục như hướng dẫn và commit requirements.txt.
  • Fixture xác thực. Viết fixture scope="session" trả về một Session. Với reqres.in, gọi endpoint login để lấy token và gắn vào header.
  • CRUD cơ bản. Viết test cho GET (một user), POST (tạo user), và kiểm tra status code lẫn field trả về đúng như dữ liệu bạn gửi.
  • Schema validation. Định nghĩa schema JSON cho user (các field bắt buộc, kiểu dữ liệu), tách ra file riêng trong schemas/, và validate response GET bằng jsonschema.
  • Parametrize. Viết một test dùng parametrize kiểm tra ít nhất 4 user id khác nhau, gồm cả một id không tồn tại (mong đợi 404).
  • Báo cáo. Chạy toàn bộ với --html=report.html --self-contained-html, mở file và kiểm tra rằng test fail (cố ý làm sai một assert) hiển thị traceback rõ ràng.
Mục tiêu: sau bài tập bạn có một repo chạy được pytest, ra báo cáo HTML, và cấu trúc đủ sạch để thêm endpoint mới chỉ bằng cách thêm một file test.

Tóm tắt

requests + pytest là stack API automation phổ biến nhất trong thế giới Python, và là con đường vào nghề SDET của rất nhiều tester Việt Nam nhờ cú pháp gọn và độ dốc học tập thấp. requests lo phần gọi HTTP với Session để giữ xác thực và tối ưu kết nối; pytest tổ chức test bằng hai vũ khí cốt lõi là fixture (chia sẻ, dọn dẹp) và parametrize (nhân độ phủ); jsonschema kiểm tra hợp đồng cấu trúc response để bắt những bug âm thầm mà assert giá trị bỏ sót; và pytest-html biến kết quả thành báo cáo mà cả người không đọc log cũng hiểu.

Ba tình huống thực tế cho thấy điều quan trọng nhất không nằm ở việc gọi được request — mà ở thiết kế fixture xác thực đúng, validate cấu trúc chặt chẽ, và báo cáo rõ ràng. Nắm vững nền tảng này, bạn đã sẵn sàng để đưa bộ test vào CI/CD và mở rộng lên quy mô hàng trăm test case trong các bài tiếp theo.

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