นักพัฒนา Python เลือกใช้ pytest เพราะมันใช้งานง่ายและไม่ซับซ้อน การทดสอบเป็นเพียงฟังก์ชันที่มีชื่อขึ้นต้นด้วย test_, การยืนยันคือคำสั่ง assert ธรรมดา, และตัวรันก็จะจัดการส่วนที่เหลือเอง เมื่อใช้ร่วมกับไลบรารี requests คุณก็จะมีเฟรมเวิร์กที่สมบูรณ์แบบ เน้นโค้ดเป็นหลักสำหรับการทดสอบ API แบบอัตโนมัติ โดยไม่ต้องมีพิธีการมากมาย
บทช่วยสอนนี้จะแสดงวิธีสร้างชุดทดสอบ API ด้วย pytest ที่ใช้งานได้จริง คุณจะได้ตั้งค่าโปรเจกต์, เขียนการทดสอบคำขอแรก, แชร์ตรรกะการตั้งค่าด้วย fixtures, รันการทดสอบเดียวกันกับอินพุตหลายชุดด้วย parametrize, และยืนยันสถานะการตอบกลับ, เนื้อหา, และ JSON Schema ทุกตัวอย่างใช้ API สาธารณะที่สมจริง คุณจึงสามารถนำโค้ดไปปรับใช้ได้โดยตรง
การตั้งค่าโปรเจกต์
ติดตั้งสองไลบรารีที่คุณต้องการลงในสภาพแวดล้อมเสมือน (virtual environment):
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_ หรือลงท้ายด้วย _test.py, ฟังก์ชันจะต้องขึ้นต้นด้วย test_, และคลาสทดสอบจะต้องขึ้นต้นด้วย Test และไม่มีเมธอด __init__ ทำตามกฎเหล่านี้แล้วคุณก็ไม่จำเป็นต้องกำหนดค่าการค้นหาด้วยตนเอง หากการทดสอบอัตโนมัติเป็นเรื่องใหม่สำหรับคุณ บทนำของเราเกี่ยวกับ การทดสอบอัตโนมัติคืออะไร จะช่วยให้คุณเข้าใจบริบทได้
ทำไมต้องใช้ pytest สำหรับการทดสอบ API โดยเฉพาะ? ไลบรารี requests จัดการกับ HTTP ส่วน pytest จัดการทุกอย่างรอบ ๆ: การค้นพบ, การยืนยันพร้อมเอาต์พุตความล้มเหลวที่อ่านง่าย, การตั้งค่าและยกเลิกการตั้งค่าผ่าน fixtures, การรันแบบขับเคลื่อนด้วยข้อมูลผ่าน parametrize, และการรายงาน คุณสามารถประกอบเฟรมเวิร์กการทดสอบ API อัตโนมัติที่สมบูรณ์แบบได้จากไลบรารีสองตัวเล็ก ๆ ที่มีเอกสารประกอบดี ในภาษาที่ทีมของคุณอาจใช้อยู่แล้วสำหรับแอปพลิเคชันนั้น ๆ ความใกล้ชิดนั้นมีความสำคัญ การทดสอบที่อยู่ใน repository เดียวกันกับโค้ดจะยังคงความถูกต้อง เพราะการเปลี่ยนแปลงที่ทำให้เกิดข้อผิดพลาดและการทดสอบที่ล้มเหลวจะปรากฏขึ้นใน pull request เดียวกัน
การเขียนการทดสอบ API ครั้งแรกของคุณ
การทดสอบ API ด้วย pytest จะส่งคำขอและยืนยันการตอบกลับ นี่คือการทดสอบกับ 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 คุณไม่จำเป็นต้องใช้วิธีการยืนยันพิเศษ เฟรมเวิร์กจะเขียนคำสั่ง assert ธรรมดาใหม่เพื่อให้ได้เอาต์พุตที่สมบูรณ์ สำหรับชุดการตรวจสอบที่กว้างขึ้นที่ควรทำกับการตอบกลับ โปรดดูคู่มือของเราเกี่ยวกับ การยืนยัน API
การแชร์การตั้งค่าด้วย fixtures
การทำซ้ำ URL พื้นฐานและเซสชัน HTTP ในการทดสอบทุกครั้งเป็นการสิ้นเปลือง Fixtures ช่วยแก้ปัญหานี้ Fixture คือฟังก์ชันที่ตกแต่งด้วย @pytest.fixture ซึ่งสร้างค่าที่การทดสอบสามารถเรียกใช้ได้โดยการระบุชื่อเป็นพารามิเตอร์
ใส่ shared fixtures ไว้ใน conftest.py เพื่อให้ไฟล์ทดสอบทุกไฟล์สามารถใช้งานได้โดยไม่ต้อง 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"]
อาร์กิวเมนต์ scope="session" หมายความว่าเซสชันจะถูกสร้างขึ้นเพียงครั้งเดียวสำหรับการรันทั้งหมด ไม่ใช่สำหรับแต่ละการทดสอบ คีย์เวิร์ด yield แยกการตั้งค่าออกจาก teardown: โค้ดก่อน yield จะทำงานก่อน โค้ดหลังจากนั้นจะทำงานเมื่อ fixture สิ้นสุดขอบเขต การทดสอบจะเรียกใช้สิ่งที่ต้องการง่ายๆ:
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 คือสิ่งทดแทนที่ทันสมัยของ pytest สำหรับสไตล์ setup_function และ teardown_function แบบเก่า พวกมันประกอบเข้าด้วยกันได้อย่างสะอาดตา รองรับ scopes และทำให้การพึ่งพาอาศัยกันชัดเจน นั่นคือเหตุผลที่ เอกสารประกอบ pytest fixtures อย่างเป็นทางการแนะนำให้ใช้เป็นแนวทางเริ่มต้น
การรันการทดสอบเดียวกับอินพุตหลายชุด
โดยทั่วไปแล้ว API endpoints จำเป็นต้องถูกตรวจสอบกับอินพุตหลายรูปแบบ: ค่าที่ถูกต้อง, ค่าที่ไม่ถูกต้อง, และกรณีขอบ การเขียนฟังก์ชันแยกกันสำหรับแต่ละกรณีเป็นเรื่องน่าเบื่อ decorator @pytest.mark.parametrize จะรันเนื้อหาการทดสอบเดียวกับรายการอินพุต:
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
สิ่งนี้สร้างกรณีทดสอบสี่กรณีแยกกันจากฟังก์ชันเดียว แต่ละกรณีจะรันและรายงานผลอย่างอิสระ ดังนั้นอินพุตที่ไม่ดีเพียงหนึ่งเดียวจะไม่ซ่อนกรณีอื่น ๆ Parametrize เป็นคำตอบในตัวของ pytest สำหรับการทดสอบแบบขับเคลื่อนด้วยข้อมูล เมื่อชุดอินพุตมีขนาดใหญ่ขึ้น ให้โหลดจากไฟล์แทน; คู่มือของเราเกี่ยวกับการ ทดสอบ API แบบขับเคลื่อนด้วยข้อมูลด้วย CSV และ JSON ครอบคลุมรูปแบบนั้น หากคุณไม่แน่ใจว่าแต่ละอินพุตควรส่งคืนรหัสสถานะใด ข้อมูลอ้างอิงเกี่ยวกับ รหัสสถานะ HTTP ที่ REST APIs ควรใช้ ก็เป็นคู่มือที่เป็นประโยชน์
การยืนยันเนื้อหาและการตอบกลับของ schema
รหัสสถานะจำเป็นแต่ไม่เพียงพอ การตอบกลับ 200 ที่มีเนื้อหาผิดรูปแบบก็ยังถือเป็นบั๊ก ยืนยัน JSON ที่แยกวิเคราะห์โดยตรง:
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
เพื่อการรับประกันที่แข็งแกร่งยิ่งขึ้น ให้ตรวจสอบเนื้อหาเทียบกับ 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 ขยายขนาดได้ดีกว่าการยืนยันแบบ field-by-field เนื่องจาก Schema เดียวครอบคลุมรูปร่างการตอบสนองทั้งหมด ไลบรารี jsonschema เป็นตัวเลือกมาตรฐาน และ เอกสารการตรวจสอบ ของมันจะอธิบายคีย์เวิร์ดที่รองรับ
การรันชุดทดสอบใน CI
ชุดทดสอบ pytest มีประโยชน์อย่างยิ่งเมื่อมันทำงานโดยอัตโนมัติ Pytest จะคืนค่า exit code ที่ไม่ใช่ศูนย์เมื่อล้มเหลว ซึ่งเป็นสิ่งที่เซิร์ฟเวอร์ CI ต้องการเพื่อทำให้ build ล้มเหลว ส่งออกรายงาน JUnit สำหรับการแสดงผลแบบอินไลน์:
pytest -v --junitxml=results.xml
เชื่อมต่อคำสั่งนั้นเข้ากับขั้นตอน GitHub Actions หรือ pipeline อื่น ๆ แล้วการทดสอบ API ของคุณจะตรวจสอบทุก commit คู่มือของเราเกี่ยวกับการ ทดสอบ API ใน CI/CD pipelines แสดงการตั้งค่าทั้งหมด รวมถึงการแทรก secret สำหรับโทเค็นและการเลือกสภาพแวดล้อม
สองพฤติกรรม CI ที่ช่วยให้ชุดทดสอบ pytest น่าเชื่อถือ ประการแรก อย่าฮาร์ดโค้ด secret หรือ URL สภาพแวดล้อมในไฟล์ทดสอบ อ่านจากตัวแปรสภาพแวดล้อมเพื่อให้ชุดทดสอบเดียวกันรันกับ staging ใน CI และภายในเครื่องได้โดยไม่ต้องแก้ไข:
import os
BASE_URL = os.environ.get("API_BASE_URL", "https://staging.example.com/v1")
ประการที่สอง รันการทดสอบอิสระแบบขนานเพื่อให้ได้ผลตอบรับที่รวดเร็ว ปลั๊กอิน pytest-xdist จะกระจายการทดสอบไปยังคอร์ CPU ด้วย pytest -n auto การรันแบบขนานจะใช้ได้ก็ต่อเมื่อการทดสอบของคุณไม่แชร์สถานะ ซึ่งเป็นอีกเหตุผลหนึ่งที่เลเยอร์ข้อมูลการทดสอบมีความสำคัญ ชุดทดสอบที่ขึ้นอยู่กับลำดับการทำงานจะล้มเหลวโดยไม่คาดคิดในทันทีที่รันแบบขนาน
การดูแลชุดทดสอบ pytest ให้สามารถบำรุงรักษาได้
ชุดทดสอบห้าสิบชุดนั้นง่าย ชุดทดสอบห้าร้อยชุดต้องการวินัย สามแนวทางปฏิบัตินี้จะช่วยให้เฟรมเวิร์ก API ของ pytest มีสุขภาพดีเมื่อมันเติบโตขึ้น
จัดกลุ่มการทดสอบที่เกี่ยวข้องกันเข้าเป็นโมดูล และใช้คลาสเฉพาะเมื่อมีการตั้งค่าร่วมกัน ไม่ใช่เพื่อการตกแต่ง ไฟล์ test_orders.py ที่มีชุดฟังก์ชันที่ชัดเจนจะอ่านง่ายกว่าไฟล์ขนาดใหญ่เพียงไฟล์เดียว ใช้ marks ที่ลงทะเบียนใน pytest.ini เพื่อติดแท็กการทดสอบ เพื่อให้คุณสามารถรันบางส่วนได้: @pytest.mark.smoke สำหรับการตรวจสอบเบื้องต้นอย่างรวดเร็ว @pytest.mark.slow สำหรับการทดสอบแบบเต็มรูปแบบ รันชุด smoke test ทุกครั้งที่ commit และรันชุดเต็มทุกคืน
รวมศูนย์การกำหนดค่า URL พื้นฐาน, schemas และ shared fixtures ควรอยู่ใน conftest.py หรือโมดูลการกำหนดค่าขนาดเล็ก ไม่ใช่การคัดลอกวางข้ามไฟล์ เมื่อ URL ของ staging เปลี่ยน คุณควรแก้ไขเพียงบรรทัดเดียว วินัยการออกแบบโมดูลแบบเดียวกันที่ใช้กับเฟรมเวิร์กใดๆ ซึ่งครอบคลุมอยู่ในคู่มือของเราเกี่ยวกับการ เขียนสคริปต์ทดสอบอัตโนมัติ ก็สามารถนำมาใช้ได้ที่นี่: แยกสิ่งที่คุณเขียนซ้ำสองครั้งออกเป็น fixture หรือ helper
เมื่อใดที่ควรใช้แพลตฟอร์มอื่นแทน
เฟรมเวิร์ก pytest นั้นยอดเยี่ยมเมื่อทีมของคุณเขียน Python และต้องการให้การทดสอบอยู่เคียงข้างโค้ดแอปพลิเคชัน มันจะสะดวกน้อยลงเมื่อบุคลากร QA หรือผลิตภัณฑ์จำเป็นต้องมีส่วนร่วม หรือเมื่อคุณต้องการออกแบบการทดสอบ การจำลอง และการรันในที่เดียวโดยไม่ต้องดูแลโค้ดเชื่อมต่อ
Apidog อุดช่องว่างนั้น มันมีเครื่องมือสร้างการทดสอบแบบภาพ, การตรวจสอบ schema กับ OpenAPI spec ของคุณ, การรันแบบขับเคลื่อนด้วยข้อมูลจาก CSV และ JSON, และ CLI runner สำหรับ CI ทั้งหมดนี้โดยไม่ต้องเขียนโค้ด fixture และ assertion ด้วยตนเอง หลายทีมใช้ทั้งสองอย่าง: pytest สำหรับสถานการณ์ที่มีตรรกะซับซ้อน และ Apidog สำหรับการครอบคลุมที่กว้างขวาง และสำหรับการออกแบบและจำลอง API ที่ชุดทดสอบ pytest ใช้ทดสอบ คุณสามารถ ดาวน์โหลด Apidog และเปรียบเทียบแนวทางทั้งสองบน endpoint จริงได้ภายในช่วงบ่าย
คำถามที่พบบ่อย
ทำไมถึงใช้ pytest แทน unittest ที่มาพร้อมกับ Python สำหรับการทดสอบ API?
Pytest ต้องการโค้ดตั้งต้น (boilerplate) น้อยกว่า การทดสอบเป็นเพียงฟังก์ชันธรรมดา การยืนยันเป็นคำสั่ง assert ธรรมดาพร้อมเอาต์พุตความล้มเหลวที่สมบูรณ์ และ fixtures จัดการการตั้งค่าได้อย่างยืดหยุ่นกว่าเมธอดแบบคลาสของ unittest Pytest ยังมีระบบนิเวศของปลั๊กอินขนาดใหญ่และ parametrize ในตัวสำหรับการทดสอบแบบขับเคลื่อนด้วยข้อมูล มันยังสามารถรันการทดสอบสไตล์ unittest ที่มีอยู่ได้ ดังนั้นการย้ายระบบจึงมีความเสี่ยงต่ำ
Fixture กับ parametrize แตกต่างกันอย่างไร?
Fixture จัดหาทรัพยากรที่นำกลับมาใช้ใหม่ได้ เช่น เซสชัน HTTP หรือโทเค็นการยืนยันตัวตน ให้กับการทดสอบใด ๆ ที่เรียกใช้ Parametrize รันเนื้อหาการทดสอบเดียวกันหลายครั้งกับค่าอินพุตที่แตกต่างกัน Fixtures แชร์การตั้งค่า; parametrize เพิ่มจำนวนกรณีทดสอบ ทั้งสองอย่างสามารถทำงานร่วมกันได้ดี: การทดสอบแบบ parametrized ยังคงสามารถพึ่งพา fixtures ได้
ฉันควรยืนยันเวลาการตอบสนองในการทดสอบ API ของ pytest หรือไม่?
คุณสามารถทำได้โดยใช้ response.elapsed.total_seconds() และการกำหนดขีดจำกัดสูงสุดแบบหลวมๆ สามารถตรวจจับข้อผิดพลาดร้ายแรงได้ แต่ pytest เป็นเครื่องมือทดสอบการทำงาน ไม่ใช่เครื่องมือทดสอบโหลด สำหรับงานด้านประสิทธิภาพจริง ให้ใช้เครื่องมือเฉพาะทาง ควรกำหนดการยืนยันเวลาให้เผื่อไว้ เพื่อไม่ให้ความผันผวนของเครือข่ายตามปกติทำให้เกิดความล้มเหลวแบบไม่คงที่
ฉันจะรักษาสภาพการทดสอบ API ให้เป็นอิสระใน pytest ได้อย่างไร?
ให้ข้อมูลเฉพาะสำหรับการทดสอบแต่ละรายการผ่าน fixtures ที่สร้างและทำความสะอาดทรัพยากร และหลีกเลี่ยงการพึ่งพาลำดับการทำงานของการทดสอบ Pytest รันการทดสอบตามลำดับไฟล์โดยค่าเริ่มต้น แต่ชุดทดสอบที่ออกแบบมาอย่างดีจะไม่ขึ้นอยู่กับสิ่งนั้น การทดสอบที่เป็นอิสระสามารถรันแบบขนานและล้มเหลวแยกกันได้ ซึ่งทำให้การดีบักง่ายขึ้นมาก
pytest สามารถตรวจสอบการตอบกลับเทียบกับข้อกำหนด OpenAPI ได้หรือไม่?
Pytest เองไม่สามารถทำได้ แต่คุณสามารถตรวจสอบเทียบกับ JSON Schema ด้วยไลบรารี jsonschema และมีปลั๊กอินที่ตรวจสอบการตอบกลับเทียบกับเอกสาร OpenAPI หากการตรวจสอบ schema เป็นหัวใจสำคัญของเวิร์กโฟลว์ของคุณ แพลตฟอร์มอย่าง Apidog ที่ตรวจสอบกับ OpenAPI spec ของคุณโดยอัตโนมัติอาจช่วยคุณประหยัดเวลาในการตั้งค่าปลั๊กอินได้
