Python API POSTテスト|pytest + requestsでデータ作成を検証

テスト自動化

📌 この記事はこんな方におすすめ

  • PythonとrequestsでPOSTリクエストのテストを書きたい方
  • APIテストでデータ作成の検証方法を学びたいQAエンジニアの方
  • 正常系・バリデーション・異常系のPOSTテストを実装したい方
  • pytestでAPIテストを自動化してポートフォリオに活用したい方

この記事を読むとわかること

  • pytestとrequestsでPOSTリクエストのAPIテストを書く基本パターン
  • データ作成後のレスポンス検証(ステータス・ボディ・ヘッダー)
  • バリデーションエラーの異常系テストの実装方法
  • raise_for_status()を使った実務レベルのテストコードの書き方

👨‍💻 筆者について

QAエンジニアとして実務でPython・pytest・requestsを使ったAPIテスト自動化を担当。本記事で使用するコードはすべてGitHubで公開しており、実際に動作確認済みのコードをそのまま解説しています。GitHubでコードを見る →

PythonでAPIテストを書くとき、GETの次に実装するのがPOSTリクエストのテストです。pytestとrequestsを使って「データが正しく作成されるか?」「バリデーションエラーが正しく返るか?」を自動で検証する方法を解説します。

この記事では、POSTリクエストの基本から異常系・バリデーション検証までを実務で使えるコードとともに紹介します。


00. APIテストとPOSTリクエストの基本

POSTリクエストは「データを新規作成する」操作です。GETと違い、リクエストボディにデータを含めてサーバーに送信します。

POSTテストでは主に以下を検証します。

確認項目内容
ステータスコード作成成功時は201が返るか201 Created
レスポンスボディ送信したデータが正しく返るかnameやemailの値
IDの付与新規作成されたリソースにIDが付与されているか“id”: 11
バリデーション不正なデータを送ったとき適切にエラーが返るか400 Bad Request

💡 ポイント:POSTテストはGETより検証項目が多いです。正常系だけでなくバリデーションエラーの異常系も必ずテストすることで、APIの品質を担保できます。


01. pytest + requestsのAPIテスト環境

まだ環境構築が済んでいない方は先にインストールしてください。

pip install requests pytest pytest-html

今回のテスト対象は無料のモックAPI JSONPlaceholder です。

POST https://jsonplaceholder.typicode.com/users

送信するリクエストボディのサンプルです。

new_user = {
    "name": "Yoshitsugu Tester",
    "username": "yoshitsugu728",
    "email": "yoshitsugu@example.com",
    "phone": "090-1234-5678",
    "website": "qa-auto-lab.com"
}

💡 ポイント:JSONPlaceholderはモックAPIのため、実際にはデータが保存されません。ただしレスポンスは本番APIと同じ形式で返ってくるため、テスト学習に最適です。


02. PythonでAPIのPOSTテストを書く方法

基本的なPOSTテスト(201確認)

POSTリクエストを送り、201 Createdが返ることを確認します。

import requests

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

def test_post_user_status_code():
    """TC01: データ作成時にステータスコード201が返ること"""
    new_user = {
        "name": "Yoshitsugu Tester",
        "username": "yoshitsugu728",
        "email": "yoshitsugu@example.com"
    }
    response = requests.post(f"{BASE_URL}/users", json=new_user)

    assert response.status_code == 201, \
        f"期待値: 201, 実際の値: {response.status_code}"

    print(f"\n✅ TC01 PASS | ステータスコード: {response.status_code}")

raise_for_status()で4xx/5xxを確実に検知する

def test_post_user_raise_for_status():
    """TC02: 4xx/5xxエラー時に例外が発生すること"""
    new_user = {"name": "Test User", "email": "test@example.com"}
    response = requests.post(f"{BASE_URL}/users", json=new_user)

    # 4xx/5xxのとき自動的にHTTPErrorが発生する
    response.raise_for_status()

    assert response.status_code == 201
    print(f"\n✅ TC02 PASS | raise_for_status() 正常通過")

💡 ポイント:response.raise_for_status()実務で必須の1行です。POSTテストでも必ず追加しておきましょう。

ヘッダーの検証

def test_post_user_header():
    """TC03: Content-TypeがJSONであること"""
    new_user = {"name": "Test User", "email": "test@example.com"}
    response = requests.post(f"{BASE_URL}/users", json=new_user)

    response.raise_for_status()

    content_type = response.headers.get("Content-Type", "")
    assert "application/json" in content_type, \
        f"Content-Typeが想定外: {content_type}"

    print(f"\n✅ TC03 PASS | Content-Type: {content_type}")

💡 ポイント:Content-Typeは application/json; charset=utf-8 の形式で返ってくるため、== の完全一致ではなく in の部分一致で検証します。

レスポンスボディの検証

送信したデータがレスポンスに正しく含まれているか確認します。

def test_post_user_response_body():
    """TC04: 送信したデータがレスポンスに正しく含まれること"""
    new_user = {
        "name": "Yoshitsugu Tester",
        "username": "yoshitsugu728",
        "email": "yoshitsugu@example.com"
    }
    response = requests.post(f"{BASE_URL}/users", json=new_user)

    response.raise_for_status()
    body = response.json()

    assert body["name"] == new_user["name"],     f"name が違います: {body['name']}"
    assert body["email"] == new_user["email"],   f"email が違います: {body['email']}"

    print(f"\n✅ TC04 PASS | 作成ユーザー: {body['name']}")

IDが付与されているか確認

新規作成されたリソースにサーバーからIDが付与されているかを確認します。

def test_post_user_id_assigned():
    """TC05: 作成されたリソースにIDが付与されること"""
    new_user = {"name": "Test User", "email": "test@example.com"}
    response = requests.post(f"{BASE_URL}/users", json=new_user)

    response.raise_for_status()
    body = response.json()

    assert "id" in body, "レスポンスにidが含まれていません"
    assert isinstance(body["id"], int), f"idはint型が期待値: {type(body['id'])}"
    assert body["id"] > 0, f"idは正の整数が期待値: {body['id']}"

    print(f"\n✅ TC05 PASS | 付与されたID: {body['id']}")

💡 ポイント:IDの付与確認は実務でよく見落とされるテストです。サーバーがIDを自動採番しているか、型は正しいかを合わせて確認しましょう。


03. APIテストのPOSTバリデーション検証

実務では正常系だけでなく、不正なデータを送ったときに適切にエラーが返るかも必ず検証します。

必須フィールドなしで送信(バリデーションエラー確認)

def test_post_user_missing_required_fields():
    """TC06: 必須フィールドなしで送信したとき適切なエラーが返ること"""
    incomplete_user = {
        "username": "yoshitsugu728"
        # name と email を意図的に省略
    }
    response = requests.post(f"{BASE_URL}/users", json=incomplete_user)

    # JSONPlaceholderはモックのため201を返す
    # 実際のAPIでは400 Bad Requestが期待される
    assert response.status_code in [201, 400], \
        f"期待値: 201 または 400, 実際の値: {response.status_code}"

    print(f"\n✅ TC06 PASS | 必須フィールドなし時のステータス: {response.status_code}")

空文字列を送信

def test_post_user_empty_string():
    """TC07: 空文字列を送信したとき適切なエラーが返ること"""
    invalid_user = {
        "name": "",      # 空文字
        "email": ""      # 空文字
    }
    response = requests.post(f"{BASE_URL}/users", json=invalid_user)

    assert response.status_code in [201, 400, 422], \
        f"期待外のステータスコード: {response.status_code}"

    print(f"\n✅ TC07 PASS | 空文字送信時のステータス: {response.status_code}")

不正なメール形式を送信

def test_post_user_invalid_email():
    """TC08: 不正なメール形式を送信したとき適切なエラーが返ること"""
    invalid_user = {
        "name": "Test User",
        "email": "not-an-email"  # メール形式でない
    }
    response = requests.post(f"{BASE_URL}/users", json=invalid_user)

    assert response.status_code in [201, 400, 422], \
        f"期待外のステータスコード: {response.status_code}"

    print(f"\n✅ TC08 PASS | 不正メール送信時のステータス: {response.status_code}")

⚠️ 注意:JSONPlaceholderはモックAPIのため、バリデーションエラーに対しても201を返します。実際のAPIでは400や422が返ることを検証してください。


04. APIテストのPOSTスキーマ検証

作成されたリソースのレスポンスが正しい型かどうかも確認します。

def test_post_user_schema():
    """TC09: レスポンスのスキーマ(型)が正しいこと"""
    new_user = {
        "name": "Yoshitsugu Tester",
        "email": "yoshitsugu@example.com"
    }
    response = requests.post(f"{BASE_URL}/users", json=new_user)

    response.raise_for_status()
    body = response.json()

    assert isinstance(body["id"],   int), f"idはint型が期待値: {type(body['id'])}"
    assert isinstance(body["name"], str), f"nameはstr型が期待値: {type(body['name'])}"

    print(f"\n✅ TC09 PASS | スキーマ検証完了")

05. POSTテストのコード全文

"""
POST API Test
Target: JSONPlaceholder (https://jsonplaceholder.typicode.com)
Framework: Python + requests + pytest
"""
import requests

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


def test_post_user_status_code():
    """TC01: データ作成時にステータスコード201が返ること"""
    new_user = {"name": "Yoshitsugu Tester", "email": "yoshitsugu@example.com"}
    response = requests.post(f"{BASE_URL}/users", json=new_user)
    assert response.status_code == 201, \
        f"期待値: 201, 実際の値: {response.status_code}"
    print(f"\n✅ TC01 PASS | status: {response.status_code}")


def test_post_user_raise_for_status():
    """TC02: 4xx/5xxエラー時に例外が発生すること"""
    new_user = {"name": "Test User", "email": "test@example.com"}
    response = requests.post(f"{BASE_URL}/users", json=new_user)
    response.raise_for_status()
    assert response.status_code == 201
    print(f"\n✅ TC02 PASS | raise_for_status() 正常通過")


def test_post_user_header():
    """TC03: Content-TypeがJSONであること"""
    new_user = {"name": "Test User", "email": "test@example.com"}
    response = requests.post(f"{BASE_URL}/users", json=new_user)
    response.raise_for_status()
    content_type = response.headers.get("Content-Type", "")
    assert "application/json" in content_type, \
        f"Content-Typeが想定外: {content_type}"
    print(f"\n✅ TC03 PASS | Content-Type: {content_type}")


def test_post_user_response_body():
    """TC04: 送信したデータがレスポンスに正しく含まれること"""
    new_user = {"name": "Yoshitsugu Tester", "email": "yoshitsugu@example.com"}
    response = requests.post(f"{BASE_URL}/users", json=new_user)
    response.raise_for_status()
    body = response.json()
    assert body["name"] == new_user["name"]
    assert body["email"] == new_user["email"]
    print(f"\n✅ TC04 PASS | name: {body['name']}")


def test_post_user_id_assigned():
    """TC05: 作成されたリソースにIDが付与されること"""
    new_user = {"name": "Test User", "email": "test@example.com"}
    response = requests.post(f"{BASE_URL}/users", json=new_user)
    response.raise_for_status()
    body = response.json()
    assert "id" in body
    assert isinstance(body["id"], int)
    assert body["id"] > 0
    print(f"\n✅ TC05 PASS | 付与されたID: {body['id']}")


def test_post_user_missing_required_fields():
    """TC06: 必須フィールドなしで送信したとき適切なエラーが返ること"""
    incomplete_user = {"username": "yoshitsugu728"}
    response = requests.post(f"{BASE_URL}/users", json=incomplete_user)
    assert response.status_code in [201, 400]
    print(f"\n✅ TC06 PASS | status: {response.status_code}")


def test_post_user_empty_string():
    """TC07: 空文字列を送信したとき適切なエラーが返ること"""
    invalid_user = {"name": "", "email": ""}
    response = requests.post(f"{BASE_URL}/users", json=invalid_user)
    assert response.status_code in [201, 400, 422]
    print(f"\n✅ TC07 PASS | status: {response.status_code}")


def test_post_user_invalid_email():
    """TC08: 不正なメール形式を送信したとき適切なエラーが返ること"""
    invalid_user = {"name": "Test User", "email": "not-an-email"}
    response = requests.post(f"{BASE_URL}/users", json=invalid_user)
    assert response.status_code in [201, 400, 422]
    print(f"\n✅ TC08 PASS | status: {response.status_code}")


def test_post_user_schema():
    """TC09: レスポンスのスキーマ(型)が正しいこと"""
    new_user = {"name": "Yoshitsugu Tester", "email": "yoshitsugu@example.com"}
    response = requests.post(f"{BASE_URL}/users", json=new_user)
    response.raise_for_status()
    body = response.json()
    assert isinstance(body["id"],   int)
    assert isinstance(body["name"], str)
    print(f"\n✅ TC09 PASS | スキーマ検証完了")

実行コマンド

pytest test_post_api.py -v -s

実行結果サンプル

test_post_api.py::test_post_user_status_code           PASSED
test_post_api.py::test_post_user_raise_for_status      PASSED
test_post_api.py::test_post_user_header                PASSED
test_post_api.py::test_post_user_response_body         PASSED
test_post_api.py::test_post_user_id_assigned           PASSED
test_post_api.py::test_post_user_missing_required_fields PASSED
test_post_api.py::test_post_user_empty_string          PASSED
test_post_api.py::test_post_user_invalid_email         PASSED
test_post_api.py::test_post_user_schema                PASSED

9 passed in 4.12s ✅

06. ハマりポイント

実装中に実際につまずいた箇所をまとめました。同じところでハマる方の参考になれば嬉しいです。


① json=とdata=の違いで詰まる

requestsでPOSTリクエストを送るとき、json=data= の違いを知らずにハマりました。data= を使うとContent-Typeが application/x-www-form-urlencoded になってしまいJSON APIで失敗します。

# ❌ data=を使うとJSON形式で送信されない
response = requests.post(url, data={"name": "Taro"})

# ✅ json=を使うと自動的にContent-Type: application/jsonが設定される
response = requests.post(url, json={"name": "Taro"})

💡 ポイント:json= を使うとリクエストボディの変換とContent-Typeの設定が自動で行われるため、JSON APIには必ず json= を使いましょう。


② POSTのステータスコードが200か201か迷う

POSTの成功レスポンスが200なのか201なのかで迷いました。APIの設計によって異なりますが、REST APIの慣習では新規作成は201が一般的です。

# ❌ 固定で200を期待してしまう
assert response.status_code == 200

# ✅ APIの仕様に合わせて確認する
# REST APIの慣習:作成成功は201
assert response.status_code == 201

# 仕様が曖昧な場合は両方を許容する書き方も可
assert response.status_code in [200, 201]

💡 ポイント:テストを書く前にAPIドキュメントで期待するステータスコードを確認しましょう。


③ レスポンスボディに送信したデータが含まれない

POSTのレスポンスには送信したデータがそのまま返ってくると思っていましたが、サーバー側で加工されたデータが返ってくるケースがありました。

# ❌ 送信データと完全一致を期待してしまう
assert body == new_user  # サーバー側でidが追加されるため失敗

# ✅ 必要なフィールドだけを個別に検証する
assert body["name"] == new_user["name"]
assert body["email"] == new_user["email"]
assert "id" in body  # サーバーが付与したIDも確認

💡 ポイント:POSTのレスポンスにはサーバーが付与したIDや自動生成フィールドが追加されます。完全一致ではなく必要なフィールドを個別に検証する方が堅牢です。


④ バリデーションエラーのステータスコードが400か422か迷う

バリデーションエラー時のステータスコードが400なのか422なのかで迷うことがありました。

# ❌ 400だけを期待してしまう
assert response.status_code == 400

# ✅ APIの仕様に応じて両方を許容する
assert response.status_code in [400, 422]
# 400 Bad Request    → 一般的なリクエストエラー
# 422 Unprocessable  → バリデーションエラー(FastAPI等で多用)

💡 ポイント:バリデーションエラーのステータスコードはフレームワークによって異なります。Expressは400、FastAPIは422を返すことが多いです。APIドキュメントで確認しましょう。


⑤ モックAPIでバリデーションが検証できない

JSONPlaceholderでバリデーションテストを書いたところ、不正なデータを送っても201が返ってきてしまい、テストの意味がなくなりました。

# JSONPlaceholderはモックのため
# 不正なデータを送っても201が返ってしまう
response = requests.post(url, json={"email": "not-an-email"})
print(response.status_code)  # → 201(本来は400が期待される)

⚠️ 注意:バリデーションテストを正確に行うには実際のバリデーション機能を持つAPIが必要です。Restful Booker や reqres.in などを使うか、自前のAPIで検証しましょう。


07. よくある質問(FAQ)

Q. POSTテストで最低限確認すべきことは何ですか?
A. 最低限「ステータスコードが201であること」「送信したデータがレスポンスに含まれること」「IDが付与されていること」の3つです。余裕があればバリデーション・スキーマ検証・ヘッダー確認も追加すると実務レベルのテストになります。

Q. requests.post()でJSONを送る方法は?
A. requests.post(url, json=データ) を使います。json= を使うとContent-Typeが自動的に application/json に設定されます。data= を使うとフォームデータとして送信されてしまうため、JSON APIには必ず json= を使いましょう。

Q. GETテストとPOSTテストの違いは何ですか?
A. GETは「データの取得」、POSTは「データの作成」です。POSTテストはリクエストボディを送信する点、成功時のステータスコードが201である点、バリデーションエラーの検証が必要な点がGETと異なります。

Q. バリデーションテストはどのAPIで試せますか?
A. JSONPlaceholderはモックAPIのためバリデーション機能がありません。本格的なバリデーションテストには reqres.in や自前のAPIを使いましょう。ポートフォリオ用には Restful Booker の認証エンドポイントも使えます。

Q. POSTとPUTの違いは何ですか?
A. POSTは「新規作成」、PUTは「既存データの全体更新」です。POSTは毎回新しいリソースが作られるのに対し、PUTは同じリクエストを何度送っても同じ結果になります(冪等性)。次の記事でPUT・PATCHテストを解説します。


08. まとめ

PythonでAPIのPOSTテストを実装する場合、pytestとrequestsを使ってステータスコード・レスポンスボディ・IDの付与・バリデーションの4つを検証することが実務の基本です。

この記事では、pytestとrequestsを使ったAPIのPOSTテスト実装方法を解説しました。

テストケース内容
TC01ステータスコードが201であること
TC02raise_for_status()で4xx/5xxを検知
TC03Content-TypeがJSONであること
TC04送信データがレスポンスに正しく含まれること
TC05作成されたリソースにIDが付与されること
TC06必須フィールドなしで適切なエラーが返ること
TC07空文字列送信で適切なエラーが返ること
TC08不正なメール形式で適切なエラーが返ること
TC09レスポンスのスキーマ(型)が正しいこと

次の記事ではPUT・PATCHリクエストのAPIテスト(既存データの更新・部分更新)を解説します。

タイトルとURLをコピーしました