นักพัฒนา Python เลือกใช้ pytest เพราะเริ่มต้นง่าย: การทดสอบคือฟังก์ชันที่ขึ้นต้นด้วย test_, การตรวจสอบใช้ assert ธรรมดา, และ pytest จัดการการค้นหา/รันให้เอง เมื่อใช้ร่วมกับ requests คุณจะได้เฟรมเวิร์กแบบ code-first สำหรับทดสอบ API อัตโนมัติ โดยไม่ต้องมีโครงสร้างซับซ้อน
บทความนี้พาคุณสร้างชุดทดสอบ API ด้วย pytest แบบใช้งานจริง ตั้งแต่ตั้งค่าโปรเจกต์ เขียน test แรก แชร์ setup ด้วย fixtures รัน test เดียวกับหลาย input ด้วย parametrize ไปจนถึงการตรวจสอบ status code, response body และ JSON Schema ตัวอย่างทั้งหมดใช้รูปแบบ API ที่พบได้บ่อย คุณจึงนำไปปรับกับโปรเจกต์ของตัวเองได้ทันที
การตั้งค่าโปรเจกต์
เริ่มจากสร้าง virtual environment และติดตั้ง dependency ที่จำเป็น:
python -m venv .venv
source .venv/bin/activate
pip install pytest requests jsonschema
จัดโครงสร้างโปรเจกต์ให้ดูแลง่ายตั้งแต่แรก:
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 จะค้นหา test อัตโนมัติเมื่อทำตาม convention เหล่านี้:
- ไฟล์ขึ้นต้นด้วย
test_หรือจบด้วย_test.py - ฟังก์ชันขึ้นต้นด้วย
test_ - คลาส test ขึ้นต้นด้วย
Testและไม่มี__init__
ตัวอย่าง pytest.ini สำหรับลงทะเบียน marks ที่ใช้บ่อย:
[pytest]
markers =
smoke: quick checks for critical API flows
slow: full or long-running API tests
ถ้าคุณเพิ่งเริ่มกับ automation testing อ่านพื้นฐานเพิ่มเติมได้ที่ การทดสอบอัตโนมัติคืออะไร
เหตุผลที่ pytest เหมาะกับ API testing คือ requests ดูแล HTTP ส่วน pytest ดูแลส่วนรอบ ๆ เช่น discovery, readable assertion failure, fixtures, parametrize และ reporting คุณจึงสร้าง API test framework ได้จากไลบรารีเล็ก ๆ ที่ทีม Python คุ้นเคยอยู่แล้ว และสามารถเก็บ test ไว้ใน repository เดียวกับโค้ดแอปเพื่อให้เห็น failure ใน pull request เดียวกัน
การเขียนการทดสอบ API ครั้งแรก
API test พื้นฐานมี 3 ขั้นตอน:
- ส่ง request
- แปลง response
- assert สิ่งที่คาดหวัง
ตัวอย่างสำหรับ endpoint ผู้ใช้:
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"
รันด้วย:
pytest -v
เมื่อ assert ล้มเหลว pytest จะแสดงค่าจริงและค่าที่คาดหวังอย่างละเอียด โดยไม่ต้องใช้ assertion method พิเศษ สำหรับรายการ assertion ที่ควรตรวจใน API response ดูเพิ่มเติมได้ที่ การยืนยัน API
การแชร์การตั้งค่าด้วย fixtures
ถ้าทุก test ต้องเขียน base URL, headers หรือ login ซ้ำ ๆ โค้ดจะดูแลยาก ให้ย้าย setup ไปไว้ใน fixtures
สร้าง 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"]
จุดสำคัญ:
-
scope="session"สร้าง session เพียงครั้งเดียวต่อการรัน test -
yieldใช้แยก setup และ teardown - fixture ใน
conftest.pyใช้ได้ทุก test file โดยไม่ต้อง import
นำ fixture ไปใช้ใน 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 เป็นแนวทางที่ยืดหยุ่นกว่า setup_function และ teardown_function เพราะประกอบกันได้ รองรับ scope และทำให้ dependency ของ test ชัดเจน อ่านเพิ่มเติมได้จาก เอกสาร pytest fixtures
การรัน test เดียวกับ input หลายชุด
API endpoint มักต้องตรวจหลายกรณี เช่น input ถูกต้อง, input ไม่ถูกต้อง และ edge case แทนที่จะเขียนหลายฟังก์ชัน ให้ใช้ @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
โค้ดนี้สร้าง test case 4 รายการจากฟังก์ชันเดียว แต่ละ case ถูกรันและรายงานผลแยกกัน ทำให้เห็นชัดว่า input ไหนล้มเหลว
เมื่อข้อมูล test มีจำนวนมาก ให้โหลดจากไฟล์ CSV หรือ JSON แทนการเขียน inline ดูแนวทางได้ใน การทดสอบ API แบบขับเคลื่อนด้วยข้อมูลด้วย CSV และ JSON
ถ้าไม่แน่ใจว่าแต่ละกรณีควรตอบ status code อะไร อ้างอิงได้จาก รหัสสถานะ HTTP ที่ REST APIs ควรใช้
การยืนยัน response body และ schema
Status code อย่างเดียวไม่พอ เพราะ 200 OK ที่มี body ผิดรูปแบบก็ยังเป็น bug ได้ ควรตรวจ response 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
สำหรับ contract ที่ชัดเจนขึ้น ให้ตรวจ 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 ช่วยจับปัญหาเชิงโครงสร้าง เช่น field หาย, field เปลี่ยนชื่อ หรือ type เปลี่ยน โดยไม่ต้องเขียน assertion ทีละ field ไลบรารี jsonschema เป็นตัวเลือกมาตรฐาน และมีรายละเอียดคีย์เวิร์ดใน เอกสารการตรวจสอบ
การรันชุดทดสอบใน CI
pytest เหมาะกับ CI เพราะคืนค่า exit code ที่ไม่ใช่ศูนย์เมื่อ test fail ทำให้ build fail ได้ทันที
รันพร้อมส่งออก JUnit report:
pytest -v --junitxml=results.xml
จากนั้นนำคำสั่งนี้ไปใส่ใน GitHub Actions หรือ pipeline อื่น ๆ เพื่อให้ API test ตรวจทุก commit คู่มือ การทดสอบ API ใน CI/CD pipelines แสดงการตั้งค่าแบบครบถ้วน รวมถึง secrets และ environment selection
อ่านค่า config จาก environment
อย่าฮาร์ดโค้ด secret หรือ URL ของ environment ลงใน test file ให้ใช้ environment variables:
import os
BASE_URL = os.environ.get("API_BASE_URL", "https://staging.example.com/v1")
ตัวอย่างการรัน:
API_BASE_URL=https://staging.example.com/v1 pytest -v
รันแบบขนานเมื่อ test เป็นอิสระต่อกัน
ติดตั้ง pytest-xdist:
pip install pytest-xdist
รันแบบใช้ CPU core อัตโนมัติ:
pytest -n auto
การรันแบบขนานจะน่าเชื่อถือก็ต่อเมื่อ test ไม่แชร์ state และไม่ขึ้นกับลำดับการทำงาน ถ้า test ต้องสร้างข้อมูล ให้ใช้ fixture สร้างและ cleanup ข้อมูลของตัวเอง
การดูแลชุดทดสอบ pytest ให้บำรุงรักษาได้
เมื่อจำนวน test เพิ่มจากหลักสิบเป็นหลักร้อย ให้เน้น 3 เรื่องนี้
1. แยก test ตาม domain
ตัวอย่าง:
test_users.py
test_orders.py
test_payments.py
ใช้ class เฉพาะเมื่อมี setup ร่วมกันจริง ๆ ไม่ใช่เพื่อจัดรูปแบบเท่านั้น ไฟล์ test_orders.py ที่มีฟังก์ชันชัดเจนมักอ่านง่ายกว่าไฟล์ test ขนาดใหญ่ไฟล์เดียว
2. ใช้ marks เพื่อเลือก subset ของ test
ตัวอย่าง smoke test:
import pytest
@pytest.mark.smoke
def test_get_current_user(api_session):
response = api_session.get("/me")
assert response.status_code == 200
รันเฉพาะ smoke:
pytest -m smoke
แนวทางทั่วไป:
- รัน
smokeทุก commit - รันชุดเต็มทุกคืน
- แยก
slowสำหรับ test ที่ใช้เวลานาน
3. รวม config และ helper ไว้จุดเดียว
สิ่งที่ควรรวมศูนย์:
- base URL
- shared headers
- auth fixture
- schemas
- helper สำหรับสร้าง test data
เมื่อ staging URL เปลี่ยน คุณควรแก้แค่จุดเดียว ไม่ใช่ไล่แก้หลายไฟล์ หลักการเดียวกับการออกแบบ test framework ทั่วไปใน การเขียนสคริปต์ทดสอบอัตโนมัติ: ถ้าเขียนซ้ำมากกว่าหนึ่งครั้ง ให้พิจารณาแยกเป็น fixture หรือ helper
เมื่อใดที่ควรใช้แพลตฟอร์มอื่นแทน
pytest เหมาะมากเมื่อทีมเขียน Python และต้องการให้ test อยู่ใกล้กับ application code แต่จะไม่สะดวกเท่าไรเมื่อ QA หรือ product ต้องมีส่วนร่วม หรือเมื่อคุณต้องการออกแบบ API, mock, test และ run ในที่เดียวโดยไม่ต้องดูแลโค้ด fixture/assertion เอง
Apidog ช่วยเติมช่องว่างนี้ด้วย visual test builder, การตรวจ schema กับ OpenAPI spec, data-driven run จาก CSV/JSON และ CLI runner สำหรับ CI โดยไม่ต้องเขียน fixture และ assertion ด้วยตัวเอง หลายทีมใช้ทั้งสองแนวทางร่วมกัน: pytest สำหรับ scenario ที่มี logic ซับซ้อน และ Apidog สำหรับ coverage กว้าง ๆ รวมถึงการออกแบบและ mock API ที่ pytest ใช้ทดสอบ คุณสามารถ ดาวน์โหลด Apidog แล้วลองเทียบ workflow บน endpoint จริงได้ภายในช่วงบ่าย
คำถามที่พบบ่อย
ทำไมถึงใช้ pytest แทน unittest ที่มาพร้อมกับ Python สำหรับการทดสอบ API?
pytest ใช้ boilerplate น้อยกว่า test เป็นฟังก์ชันธรรมดา assertion ใช้ assert ปกติแต่มี failure output ที่อ่านง่าย และ fixtures จัดการ setup ได้ยืดหยุ่นกว่า class-based setup ของ unittest นอกจากนี้ pytest ยังมี ecosystem plugin ขนาดใหญ่และมี parametrize ในตัวสำหรับ data-driven testing
pytest ยังสามารถรัน test แบบ unittest ที่มีอยู่แล้วได้ จึงลดความเสี่ยงในการย้ายระบบ
Fixture กับ parametrize แตกต่างกันอย่างไร?
Fixture ใช้จัดเตรียม resource ที่ test ต้องใช้ เช่น HTTP session, auth token หรือ test data
parametrize ใช้รัน test body เดียวกันหลายครั้งด้วย input ต่างกัน
สรุปสั้น ๆ:
- fixture = แชร์ setup
- parametrize = เพิ่มจำนวน test case
- ใช้ร่วมกันได้ เช่น test แบบ parametrized ที่ใช้
api_sessionfixture
ควร assert response time ใน pytest API test หรือไม่?
ทำได้โดยใช้:
assert response.elapsed.total_seconds() < 1.0
แต่ควรตั้ง threshold แบบหลวม ๆ เพื่อหลีกเลี่ยง flaky test จาก network jitter ปกติ pytest เหมาะกับ functional testing ไม่ใช่ load testing ถ้าต้องวัด performance จริง ควรใช้เครื่องมือเฉพาะทาง
จะรักษา API test ให้เป็นอิสระต่อกันใน pytest ได้อย่างไร?
ใช้ fixtures เพื่อสร้างข้อมูลเฉพาะของแต่ละ test และ cleanup หลังใช้งาน หลีกเลี่ยงการให้ test หนึ่งพึ่งพาผลลัพธ์จากอีก test หนึ่ง แม้ pytest จะรันตามลำดับไฟล์โดยค่าเริ่มต้น แต่ test suite ที่ดีไม่ควรขึ้นกับลำดับนั้น
test ที่เป็นอิสระจะรันแบบขนานได้ง่าย และ debug ง่ายกว่าเมื่อ fail
pytest สามารถตรวจ response เทียบกับ OpenAPI spec ได้หรือไม่?
pytest เองไม่ได้ทำโดยตรง แต่คุณสามารถตรวจ JSON Schema ด้วย jsonschema และมี plugin ที่ช่วยตรวจ response เทียบกับ OpenAPI document ได้ หาก schema validation เป็น workflow หลักของทีม แพลตฟอร์มอย่าง Apidog ที่ตรวจเทียบกับ OpenAPI spec อัตโนมัติอาจช่วยลดเวลาการตั้งค่า plugin และดูแล test infrastructure ได้มากขึ้น
Top comments (0)