#การทดสอบ API ด้วย pytest

pytest เป็น Testing Framework ยอดนิยมของภาษา Python ที่สามารถนำมาใช้ทดสอบได้ทั้ง Unit Test, Integration Test และ API Test โดยมีจุดเด่นคือ syntax กระชับ ใช้ assert แบบ Python ปกติ รองรับ Fixture และ Parameterized Test และสามารถต่อยอดเข้าสู่ CI/CD ได้ง่าย

สำหรับการทดสอบ REST API เราสามารถใช้ pytest ทำหน้าที่เป็น Test Runner และใช้ไลบรารี requests สำหรับส่ง HTTP Request ไปยัง API

ตัวอย่างในบทความนี้ใช้ JSONPlaceholder ซึ่งเป็น Fake REST API สำหรับการทดลองและการทำ Prototype


#1. API Testing คืออะไร

API Testing คือการตรวจสอบพฤติกรรมของ Application Programming Interface ว่าทำงานตรงตามข้อกำหนดหรือไม่ เช่น

  • HTTP Status Code ถูกต้องหรือไม่
  • Response Body มีข้อมูลตามที่กำหนดหรือไม่
  • JSON Structure ถูกต้องหรือไม่
  • Header ถูกต้องหรือไม่
  • Authentication และ Authorization ทำงานถูกต้องหรือไม่
  • API จัดการข้อมูลผิดรูปแบบได้อย่างเหมาะสมหรือไม่
  • Response Time อยู่ในระดับที่ยอมรับได้หรือไม่

ตัวอย่างเช่น เมื่อเรียก

GET /posts/1

เราอาจคาดหวังว่า API จะตอบกลับด้วย HTTP Status 200 และ JSON ที่มี field เช่น id, title, body และ userId


#2. ทำไมจึงเหมาะกับ pytest

จุดเด่นของ pytest สำหรับงาน API Testing ได้แก่

  1. เขียน Test Case ได้กระชับ
  2. ใช้ assert ของ Python ได้โดยตรง
  3. มี Fixture สำหรับจัดการข้อมูลหรือ Client ที่ใช้ร่วมกัน
  4. รองรับ Parameterized Test
  5. แยก Test Suite ด้วย Marker ได้
  6. ใช้ร่วมกับ Requests, HTTPX และไลบรารี Python อื่นได้ง่าย
  7. ทำงานร่วมกับ GitHub Actions, GitLab CI, Jenkins และระบบ CI/CD อื่นได้ดี

#3. ติดตั้งเครื่องมือ

ตรวจสอบ Python

python --version

สร้าง Virtual Environment

python -m venv .venv

เปิดใช้งานบน macOS/Linux

source .venv/bin/activate

บน Windows PowerShell

.venv\Scripts\Activate.ps1

ติดตั้งแพ็กเกจ

pip install pytest requests

ตรวจสอบ pytest

pytest --version

#4. โครงสร้างโปรเจกต์

ตัวอย่างโครงสร้างที่เหมาะกับ API Test ขนาดเล็กถึงกลาง

api-testing/
├── tests/
│   ├── conftest.py
│   ├── test_posts.py
│   └── test_users.py
├── pytest.ini
└── requirements.txt

ไฟล์ requirements.txt

pytest
requests

ติดตั้ง dependency

pip install -r requirements.txt

#5. API ที่ใช้ทดลอง

กำหนด Base URL เป็น

https://jsonplaceholder.typicode.com

ตัวอย่าง Endpoint

GET    /posts
GET    /posts/1
POST   /posts
PUT    /posts/1
PATCH  /posts/1
DELETE /posts/1

JSONPlaceholder จำลองการสร้าง แก้ไข และลบข้อมูล แต่ไม่ได้บันทึกการเปลี่ยนแปลงจริงลงฐานข้อมูล


#6. ทดสอบ GET Request

สร้างไฟล์

tests/test_posts.py

แล้วเขียน

import requests

BASE_URL = "https://jsonplaceholder.typicode.com"


def test_get_post():
    response = requests.get(
        f"{BASE_URL}/posts/1",
        timeout=10
    )

    assert response.status_code == 200

    data = response.json()

    assert data["id"] == 1
    assert "title" in data
    assert "body" in data
    assert "userId" in data

รัน

pytest

หรือแสดงรายละเอียดเพิ่มเติม

pytest -v

สิ่งที่ Test Case ตรวจสอบคือ

Request
   ↓
GET /posts/1
   ↓
Response
   ↓
Status Code == 200
   ↓
ตรวจสอบ JSON

#7. ตรวจสอบ Response Header

สามารถตรวจสอบ HTTP Header ได้เช่นกัน

def test_response_content_type():
    response = requests.get(
        f"{BASE_URL}/posts/1",
        timeout=10
    )

    assert response.status_code == 200
    assert "application/json" in response.headers["Content-Type"]

#8. ทดสอบรายการข้อมูล

def test_get_posts():
    response = requests.get(
        f"{BASE_URL}/posts",
        timeout=10
    )

    assert response.status_code == 200

    data = response.json()

    assert isinstance(data, list)
    assert len(data) > 0

นอกจากตรวจสอบ Status Code ควรตรวจสอบชนิดและเนื้อหาของข้อมูลด้วย เพราะ HTTP 200 เพียงอย่างเดียวไม่ได้หมายความว่า Response ถูกต้องตาม Business Requirement


#9. ทดสอบ POST Request

ตัวอย่างสร้าง Post ใหม่

def test_create_post():
    payload = {
        "title": "API Testing with pytest",
        "body": "Testing REST API using Python",
        "userId": 1
    }

    response = requests.post(
        f"{BASE_URL}/posts",
        json=payload,
        timeout=10
    )

    assert response.status_code == 201

    data = response.json()

    assert data["title"] == payload["title"]
    assert data["body"] == payload["body"]
    assert data["userId"] == payload["userId"]
    assert "id" in data

เมื่อใช้

json=payload

ไลบรารี Requests จะ serialize dictionary เป็น JSON สำหรับ Request ให้อัตโนมัติ


#10. ใช้ Fixture ลด Code ซ้ำ

ถ้ามีหลาย Test Case ที่ใช้ Base URL หรือ HTTP Session เดียวกัน สามารถใช้ Fixture

สร้าง

tests/conftest.py
import pytest
import requests


@pytest.fixture(scope="session")
def base_url():
    return "https://jsonplaceholder.typicode.com"


@pytest.fixture(scope="session")
def api_client():
    with requests.Session() as session:
        yield session

จากนั้น Test Case สามารถรับ Fixture ผ่าน Parameter

def test_get_post(api_client, base_url):
    response = api_client.get(
        f"{base_url}/posts/1",
        timeout=10
    )

    assert response.status_code == 200
    assert response.json()["id"] == 1

ข้อดีคือ Configuration และ Resource ที่ใช้ร่วมกันถูกรวมไว้ในจุดเดียว


#11. Parameterized Testing

ถ้าต้องทดสอบ Endpoint เดียวกันกับหลายค่า ไม่ควรเขียน Test Function ซ้ำ

ใช้ pytest.mark.parametrize

import pytest


@pytest.mark.parametrize("post_id", [1, 2, 3, 4, 5])
def test_get_multiple_posts(api_client, base_url, post_id):
    response = api_client.get(
        f"{base_url}/posts/{post_id}",
        timeout=10
    )

    assert response.status_code == 200
    assert response.json()["id"] == post_id

pytest จะสร้าง Test Case จากข้อมูลแต่ละชุดให้อัตโนมัติ

test_get_multiple_posts[1]
test_get_multiple_posts[2]
test_get_multiple_posts[3]
test_get_multiple_posts[4]
test_get_multiple_posts[5]

#12. ทดสอบ Query Parameter

ตัวอย่างค้นหา Post ของ User หมายเลข 1

def test_filter_posts_by_user(api_client, base_url):
    params = {
        "userId": 1
    }

    response = api_client.get(
        f"{base_url}/posts",
        params=params,
        timeout=10
    )

    assert response.status_code == 200

    posts = response.json()

    assert len(posts) > 0
    assert all(post["userId"] == 1 for post in posts)

รูปแบบ Request ที่เกิดขึ้นคือ

GET /posts?userId=1

#13. Negative Testing

API Testing ไม่ควรทดสอบเฉพาะ Happy Path

ควรตรวจสอบ Invalid Input, Resource ที่ไม่มีอยู่ หรือกรณี Error ต่าง ๆ ด้วย

ตัวอย่างเรียก Resource ที่ไม่มีอยู่

def test_post_not_found(api_client, base_url):
    response = api_client.get(
        f"{base_url}/posts/999999",
        timeout=10
    )

    assert response.status_code == 404

Negative Test ของระบบจริงอาจประกอบด้วย

  • Missing required field
  • Invalid data type
  • Invalid token
  • Expired token
  • Unauthorized user
  • Forbidden operation
  • Resource not found
  • Duplicate data
  • Invalid query parameter

#14. ทดสอบ Authentication

API จริงมักใช้ Bearer Token

ตัวอย่าง

def test_profile():
    token = "YOUR_ACCESS_TOKEN"

    headers = {
        "Authorization": f"Bearer {token}"
    }

    response = requests.get(
        "https://api.example.com/profile",
        headers=headers,
        timeout=10
    )

    assert response.status_code == 200

ไม่ควรเขียน Token จริงลง Source Code

ควรอ่านจาก Environment Variable เช่น

import os

token = os.getenv("API_TOKEN")

แล้วกำหนดค่า

macOS/Linux:

export API_TOKEN="your-secret-token"

Windows PowerShell:

$env:API_TOKEN="your-secret-token"

#15. ใช้ Marker แบ่ง Test Suite

สร้าง pytest.ini

[pytest]
markers =
    smoke: critical API smoke tests
    regression: regression API tests

กำหนด Marker

import pytest


@pytest.mark.smoke
def test_health_check(api_client, base_url):
    response = api_client.get(
        f"{base_url}/posts/1",
        timeout=10
    )

    assert response.status_code == 200

รันเฉพาะ Smoke Test

pytest -m smoke -v

รันเฉพาะ Regression

pytest -m regression -v

#16. ตรวจสอบ Response Time

สามารถวัดเวลาตอบสนองแบบง่ายได้

def test_response_time(api_client, base_url):
    response = api_client.get(
        f"{base_url}/posts/1",
        timeout=10
    )

    assert response.status_code == 200
    assert response.elapsed.total_seconds() < 2

อย่างไรก็ตามการตรวจ Response Time ใน Functional Test ไม่ใช่สิ่งทดแทน Performance Testing

หากต้องการ Load Test หรือ Stress Test ควรใช้เครื่องมือ เช่น

  • k6
  • JMeter
  • Locust

#17. ตรวจสอบ JSON Schema

เมื่อ API มี Contract ชัดเจน เราสามารถตรวจสอบ Structure ของ JSON ได้

ติดตั้ง

pip install jsonschema

ตัวอย่าง Schema

from jsonschema import validate


POST_SCHEMA = {
    "type": "object",
    "properties": {
        "userId": {"type": "integer"},
        "id": {"type": "integer"},
        "title": {"type": "string"},
        "body": {"type": "string"}
    },
    "required": [
        "userId",
        "id",
        "title",
        "body"
    ]
}


def test_post_schema(api_client, base_url):
    response = api_client.get(
        f"{base_url}/posts/1",
        timeout=10
    )

    assert response.status_code == 200

    validate(
        instance=response.json(),
        schema=POST_SCHEMA
    )

การตรวจ Schema ช่วยตรวจจับ Breaking Change เช่น field หายไป หรือชนิดข้อมูลเปลี่ยน


#18. ตัวอย่าง Test Suite แบบรวม

tests/test_posts.py

import pytest


def test_get_post(api_client, base_url):
    response = api_client.get(
        f"{base_url}/posts/1",
        timeout=10
    )

    assert response.status_code == 200

    data = response.json()

    assert data["id"] == 1
    assert isinstance(data["title"], str)


def test_create_post(api_client, base_url):
    payload = {
        "title": "pytest API",
        "body": "API automation testing",
        "userId": 1
    }

    response = api_client.post(
        f"{base_url}/posts",
        json=payload,
        timeout=10
    )

    assert response.status_code == 201

    data = response.json()

    assert data["title"] == payload["title"]
    assert data["body"] == payload["body"]
    assert data["userId"] == payload["userId"]


@pytest.mark.parametrize("post_id", [1, 2, 3])
def test_multiple_posts(
    api_client,
    base_url,
    post_id
):
    response = api_client.get(
        f"{base_url}/posts/{post_id}",
        timeout=10
    )

    assert response.status_code == 200
    assert response.json()["id"] == post_id


def test_post_not_found(api_client, base_url):
    response = api_client.get(
        f"{base_url}/posts/999999",
        timeout=10
    )

    assert response.status_code == 404

#19. คำสั่ง pytest ที่ควรรู้

รัน Test ทั้งหมด

pytest

แสดงรายละเอียด

pytest -v

ลด Output

pytest -q

รันเฉพาะไฟล์

pytest tests/test_posts.py

รันเฉพาะ Test Function

pytest tests/test_posts.py::test_get_post

เลือก Test จากชื่อ

pytest -k "post"

หยุดเมื่อพบ Failure แรก

pytest -x

หยุดเมื่อ Failure ครบ 2 ครั้ง

pytest --maxfail=2

#20. แนวทางออกแบบ API Test ที่ดี

ไม่ควรตรวจเพียง

assert response.status_code == 200

แต่ควรตรวจหลายระดับ

1. Status Code
2. Response Headers
3. Response Body
4. Data Type
5. Required Fields
6. Business Rules
7. Error Handling
8. Authentication / Authorization
9. Schema / Contract
10. Response Time ตามเกณฑ์ที่เหมาะสม

ตัวอย่าง

def test_user(api_client, base_url):
    response = api_client.get(
        f"{base_url}/users/1",
        timeout=10
    )

    assert response.status_code == 200
    assert "application/json" in response.headers["Content-Type"]

    data = response.json()

    assert data["id"] == 1
    assert isinstance(data["name"], str)
    assert isinstance(data["email"], str)
    assert "@" in data["email"]

#21. แนวทางสำหรับโปรเจกต์จริง

เมื่อ Test Suite ใหญ่ขึ้น ควรแยก Layer เช่น

api-testing/
├── clients/
│   ├── posts_client.py
│   └── users_client.py
├── tests/
│   ├── test_posts.py
│   └── test_users.py
├── schemas/
│   ├── post_schema.py
│   └── user_schema.py
├── config/
│   └── settings.py
├── conftest.py
├── pytest.ini
└── requirements.txt

แนวคิดคือไม่ให้ Test Case ต้องรู้รายละเอียดการสร้าง HTTP Request มากเกินไป

ตัวอย่าง API Client

class PostsClient:

    def __init__(self, session, base_url):
        self.session = session
        self.base_url = base_url

    def get_post(self, post_id):
        return self.session.get(
            f"{self.base_url}/posts/{post_id}",
            timeout=10
        )

    def create_post(self, payload):
        return self.session.post(
            f"{self.base_url}/posts",
            json=payload,
            timeout=10
        )

Test จะอ่านง่ายขึ้น

def test_get_post(posts_client):
    response = posts_client.get_post(1)

    assert response.status_code == 200
    assert response.json()["id"] == 1

#22. Workflow ของ API Testing ด้วย pytest

        ┌──────────────────┐
        │   Test Case      │
        └────────┬─────────┘
                 │
                 ▼
        ┌──────────────────┐
        │ pytest Fixture   │
        └────────┬─────────┘
                 │
                 ▼
        ┌──────────────────┐
        │ requests.Session │
        └────────┬─────────┘
                 │
                 ▼
        ┌──────────────────┐
        │    REST API      │
        └────────┬─────────┘
                 │
                 ▼
        ┌──────────────────┐
        │ HTTP Response    │
        └────────┬─────────┘
                 │
                 ▼
        ┌──────────────────┐
        │ Assertions       │
        │ Status / JSON    │
        │ Header / Schema  │
        └──────────────────┘

#23. สรุป

pytest + requests เป็นชุดเครื่องมือที่เหมาะสำหรับเริ่มต้นสร้าง Automated API Testing ด้วย Python เพราะเรียนรู้ได้เร็วและขยาย Test Suite ได้ดี

แนวทางที่แนะนำคือเริ่มจาก

pytest
  ↓
GET / POST
  ↓
Assertions
  ↓
Fixtures
  ↓
Parametrization
  ↓
Negative Testing
  ↓
Authentication
  ↓
Schema Validation
  ↓
CI/CD

เมื่อระบบมีขนาดใหญ่ขึ้น ควรแยก API Client, Test Data, Configuration และ Schema ออกจาก Test Case เพื่อให้ Test Suite อ่านง่าย ดูแลรักษาง่าย และนำกลับมาใช้ซ้ำได้


#แหล่งอ้างอิง