DEV Community

Cover image for Pytest API เฟรมเวิร์คทดสอบอัตโนมัติ: คู่มือฉบับใช้งานจริง
Thanawat Wongchai
Thanawat Wongchai

Posted on • Originally published at apidog.com

Pytest API เฟรมเวิร์คทดสอบอัตโนมัติ: คู่มือฉบับใช้งานจริง

นักพัฒนา Python เลือกใช้ pytest เพราะเริ่มต้นง่าย: การทดสอบคือฟังก์ชันที่ขึ้นต้นด้วย test_, การตรวจสอบใช้ assert ธรรมดา, และ pytest จัดการการค้นหา/รันให้เอง เมื่อใช้ร่วมกับ requests คุณจะได้เฟรมเวิร์กแบบ code-first สำหรับทดสอบ API อัตโนมัติ โดยไม่ต้องมีโครงสร้างซับซ้อน

ลองใช้ Apidog วันนี้

บทความนี้พาคุณสร้างชุดทดสอบ 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
Enter fullscreen mode Exit fullscreen mode

จัดโครงสร้างโปรเจกต์ให้ดูแลง่ายตั้งแต่แรก:

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 จะค้นหา 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
Enter fullscreen mode Exit fullscreen mode

ถ้าคุณเพิ่งเริ่มกับ 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 ขั้นตอน:

  1. ส่ง request
  2. แปลง response
  3. 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"
Enter fullscreen mode Exit fullscreen mode

รันด้วย:

pytest -v
Enter fullscreen mode Exit fullscreen mode

เมื่อ 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"]
Enter fullscreen mode Exit fullscreen mode

จุดสำคัญ:

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

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

โค้ดนี้สร้าง 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
Enter fullscreen mode Exit fullscreen mode

สำหรับ 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)
Enter fullscreen mode Exit fullscreen mode

Schema validation ช่วยจับปัญหาเชิงโครงสร้าง เช่น field หาย, field เปลี่ยนชื่อ หรือ type เปลี่ยน โดยไม่ต้องเขียน assertion ทีละ field ไลบรารี jsonschema เป็นตัวเลือกมาตรฐาน และมีรายละเอียดคีย์เวิร์ดใน เอกสารการตรวจสอบ

การรันชุดทดสอบใน CI

pytest เหมาะกับ CI เพราะคืนค่า exit code ที่ไม่ใช่ศูนย์เมื่อ test fail ทำให้ build fail ได้ทันที

รันพร้อมส่งออก JUnit report:

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

จากนั้นนำคำสั่งนี้ไปใส่ใน 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")
Enter fullscreen mode Exit fullscreen mode

ตัวอย่างการรัน:

API_BASE_URL=https://staging.example.com/v1 pytest -v
Enter fullscreen mode Exit fullscreen mode

รันแบบขนานเมื่อ test เป็นอิสระต่อกัน

ติดตั้ง pytest-xdist:

pip install pytest-xdist
Enter fullscreen mode Exit fullscreen mode

รันแบบใช้ CPU core อัตโนมัติ:

pytest -n auto
Enter fullscreen mode Exit fullscreen mode

การรันแบบขนานจะน่าเชื่อถือก็ต่อเมื่อ test ไม่แชร์ state และไม่ขึ้นกับลำดับการทำงาน ถ้า test ต้องสร้างข้อมูล ให้ใช้ fixture สร้างและ cleanup ข้อมูลของตัวเอง

การดูแลชุดทดสอบ pytest ให้บำรุงรักษาได้

เมื่อจำนวน test เพิ่มจากหลักสิบเป็นหลักร้อย ให้เน้น 3 เรื่องนี้

1. แยก test ตาม domain

ตัวอย่าง:

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

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

รันเฉพาะ smoke:

pytest -m smoke
Enter fullscreen mode Exit fullscreen mode

แนวทางทั่วไป:

  • รัน 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_session fixture

ควร assert response time ใน pytest API test หรือไม่?

ทำได้โดยใช้:

assert response.elapsed.total_seconds() < 1.0
Enter fullscreen mode Exit fullscreen mode

แต่ควรตั้ง 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)