PythonでAPIのGETテストを書く方法|pytest・requestsでステータス・レスポンス検証

テスト自動化

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

  • PythonとrequestsでGETリクエストのテストを書きたい方
  • ステータスコードやレスポンスのJSONを検証する方法を学びたい方
  • pytestでAPIテストを自動化したいQAエンジニアの方
  • レスポンスのスキーマ検証まで実装したい方

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

  • pytestとrequestsでGETリクエストのAPIテストを書く基本パターン
  • ステータスコード・レスポンスボディ・ヘッダーの検証方法
  • JSONのスキーマ(型・フィールド)を検証する実装方法
  • 正常系・異常系のAPIテストケース設計パターン

👨‍💻 筆者について

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

PythonでAPIテストを書くとき、最初に実装するのがGETリクエストのテストです。pytestとrequestsを使って「ステータスコードは200か?」「レスポンスのJSONに必要なフィールドが含まれているか?」を自動で検証する方法を解説します。

この記事では、基本的なステータスコードの確認からレスポンスボディのスキーマ検証までを実務で使えるコードとともに紹介します。


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

APIテストとは、ブラウザを使わずにHTTPリクエストを直接送信してレスポンスを検証するテストです。GETリクエストはAPIの中で最も基本的な操作で、「データを取得する」役割を担います。

GETテストでは主に以下の3つを検証します。

確認項目内容
ステータスコード期待したHTTPステータスが返るか200, 404, 401など
レスポンスボディJSONの値が正しいかid・name・emailの値
スキーマ検証JSONのフィールド・型が正しいかidはint型、nameはstr型

💡 ポイント:実務ではステータスコードだけでなく、レスポンスの中身まで検証することが重要です。ステータスが200でも、レスポンスの値が間違っていればバグです。


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

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

pip install requests pytest pytest-html

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

GET https://jsonplaceholder.typicode.com/users/1

レスポンスのサンプルはこちらです。

{
    "id": 1,
    "name": "Leanne Graham",
    "username": "Bret",
    "email": "Sincere@april.biz",
    "phone": "1-770-736-8031 x56442",
    "website": "hildegard.org"
}

💡 ポイント:JSONPlaceholderはデータの変更・削除が実際には行われないモックAPIです。テスト学習用として安心して使えます。


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

ステータスコードの検証

最も基本的なテストです。リクエストを送って200が返ることを確認します。

import requests

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

def test_get_user_status_code():
    """TC01: ステータスコードが200であること"""
    response = requests.get(f"{BASE_URL}/users/1")

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

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

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

実務でよく使われる書き方です。raise_for_status() を追加するだけで、4xx / 5xx のエラーが返ってきたときに自動的に例外を発生させてテストを失敗させられます。

def test_get_user_raise_for_status():
    """TC02: 4xx/5xxエラー時に例外が発生すること"""
    response = requests.get(f"{BASE_URL}/users/1")

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

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

💡 ポイント:response.raise_for_status()実務で必須の1行です。assertでステータスコードをチェックする前に入れておくと、予期しないエラーレスポンスを確実に検知できます。

ヘッダーの検証

レスポンスのContent-Typeが正しいかも確認します。JSONを期待しているのにHTMLが返ってきたようなケースを検知できます。

def test_get_user_header():
    """TC03: Content-TypeがJSONであること"""
    response = requests.get(f"{BASE_URL}/users/1")

    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}")

💡 ポイント:ヘッダーチェックはAPIが正しい形式でレスポンスを返しているかの確認です。特にContent-Typeの検証は実務でよく使われます。

レスポンスJSONの検証

ステータスコードだけでなく、レスポンスの値が正しいかも確認します。

def test_get_user_response_body():
    """TC02: レスポンスボディの値が正しいこと"""
    response = requests.get(f"{BASE_URL}/users/1")

    assert response.status_code == 200
    body = response.json()

    assert body["id"] == 1,                     f"id が違います: {body['id']}"
    assert body["name"] == "Leanne Graham",     f"name が違います: {body['name']}"
    assert body["email"] == "Sincere@april.biz",f"email が違います: {body['email']}"

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

必須フィールドの存在確認

必要なフィールドが全て含まれているかをチェックします。

def test_get_user_fields_exist():
    """TC03: 必須フィールドが存在すること"""
    response = requests.get(f"{BASE_URL}/users/1")
    body = response.json()

    required_fields = ["id", "name", "username", "email", "phone", "website"]
    for field in required_fields:
        assert field in body, f"必須フィールド '{field}' が存在しません"

    print(f"\n✅ TC03 PASS | 全必須フィールドの存在を確認")

💡 ポイント:フィールドをリストで管理すると、必須項目が増えてもコードの変更が最小限で済みます。実務でよく使われる書き方です。


03. APIテストのスキーマ(型)検証

レスポンスの値だけでなく、型(int/str/dict)が正しいかどうかもテストします。型が変わるとフロントエンドが壊れることがあるため、実務では重要な検証です。

def test_get_user_schema():
    """TC04: レスポンスのスキーマ(型)が正しいこと"""
    response = requests.get(f"{BASE_URL}/users/1")
    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'])}"
    assert isinstance(body["username"], str),  f"username は str であるべき: {type(body['username'])}"
    assert isinstance(body["email"],    str),  f"email は str であるべき: {type(body['email'])}"
    assert isinstance(body["address"],  dict), f"address は dict であるべき: {type(body['address'])}"
    assert isinstance(body["company"],  dict), f"company は dict であるべき: {type(body['company'])}"

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

💡 ポイント:isinstance() を使うと型チェックが簡単にできます。APIのレスポンス型が変更されたときに即座に検知できるため、実務では必須の検証です。


04. APIテストの異常系パターン

存在しないリソースへのアクセス(404確認)

def test_get_nonexistent_user():
    """TC05: 存在しないユーザーへのアクセスで404が返ること"""
    response = requests.get(f"{BASE_URL}/users/99999")

    assert response.status_code == 404, \
        f"存在しないリソースは404が期待値: {response.status_code}"

    print(f"\n✅ TC05 PASS | 404を正しく確認: {response.status_code}")

一覧取得と件数確認

def test_get_all_users():
    """TC06: 全ユーザーの一覧が取得できること"""
    response = requests.get(f"{BASE_URL}/users")
    body = response.json()

    assert response.status_code == 200
    assert isinstance(body, list), "レスポンスはリスト形式であるべき"
    assert len(body) == 10, f"ユーザー数は10件が期待値: {len(body)}件"

    print(f"\n✅ TC06 PASS | ユーザー一覧取得成功: {len(body)}件")

⚠️ 注意:異常系テストは正常系と同じくらい重要です。存在しないリソースに対して正しく404が返るかを確認することで、APIのエラーハンドリングが正しく実装されているかを検証できます。


05. APIテストのレスポンスタイム検証

パフォーマンス観点から、レスポンスが一定時間内に返ってくるかも確認しましょう。

import time

def test_get_user_response_time():
    """TC07: レスポンスタイムが2000ms以内であること"""
    start = time.time()
    response = requests.get(f"{BASE_URL}/users/1")
    elapsed_ms = (time.time() - start) * 1000

    assert response.status_code == 200
    assert elapsed_ms < 2000, \
        f"レスポンスが遅すぎます: {elapsed_ms:.0f}ms(上限: 2000ms)"

    print(f"\n✅ TC07 PASS | レスポンスタイム: {elapsed_ms:.0f}ms")

💡 ポイント:レスポンスタイムの閾値はシステムの要件に合わせて調整しましょう。CI/CDに組み込むと、パフォーマンス劣化を自動で検知できます。


06. GETテストのコード全文

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

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


def test_get_user_status_code():
    """TC01: ステータスコードが200であること"""
    response = requests.get(f"{BASE_URL}/users/1")
    assert response.status_code == 200, \
        f"期待値: 200, 実際の値: {response.status_code}"
    print(f"\n✅ TC01 PASS | status: {response.status_code}")


def test_get_user_raise_for_status():
    """TC02: 4xx/5xxエラー時に例外が発生すること"""
    response = requests.get(f"{BASE_URL}/users/1")
    response.raise_for_status()  # 4xx/5xxで自動的に例外発生
    assert response.status_code == 200
    print(f"\n✅ TC02 PASS | raise_for_status() 正常通過")


def test_get_user_header():
    """TC03: Content-TypeがJSONであること"""
    response = requests.get(f"{BASE_URL}/users/1")
    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_get_user_response_body():
    """TC04: レスポンスボディの値が正しいこと"""
    response = requests.get(f"{BASE_URL}/users/1")
    response.raise_for_status()
    body = response.json()
    assert body["id"] == 1
    assert body["name"] == "Leanne Graham"
    assert body["email"] == "Sincere@april.biz"
    print(f"\n✅ TC04 PASS | name: {body['name']}")


def test_get_user_fields_exist():
    """TC05: 必須フィールドが存在すること"""
    response = requests.get(f"{BASE_URL}/users/1")
    response.raise_for_status()
    body = response.json()
    for field in ["id", "name", "username", "email", "phone", "website"]:
        assert field in body, f"'{field}' が存在しません"
    print(f"\n✅ TC05 PASS | 全必須フィールド確認済み")


def test_get_user_schema():
    """TC06: スキーマ(型)が正しいこと"""
    response = requests.get(f"{BASE_URL}/users/1")
    response.raise_for_status()
    body = response.json()
    assert isinstance(body["id"],      int)
    assert isinstance(body["name"],    str)
    assert isinstance(body["email"],   str)
    assert isinstance(body["address"], dict)
    assert isinstance(body["company"], dict)
    print(f"\n✅ TC06 PASS | スキーマ検証完了")


def test_get_nonexistent_user():
    """TC07: 存在しないユーザーで404が返ること"""
    response = requests.get(f"{BASE_URL}/users/99999")
    assert response.status_code == 404
    print(f"\n✅ TC07 PASS | 404確認: {response.status_code}")


def test_get_all_users():
    """TC08: 全ユーザー一覧が10件取得できること"""
    response = requests.get(f"{BASE_URL}/users")
    response.raise_for_status()
    body = response.json()
    assert isinstance(body, list)
    assert len(body) == 10
    print(f"\n✅ TC08 PASS | ユーザー数: {len(body)}件")


def test_get_user_response_time():
    """TC09: レスポンスタイムが2000ms以内であること"""
    start = time.time()
    response = requests.get(f"{BASE_URL}/users/1")
    elapsed_ms = (time.time() - start) * 1000
    assert response.status_code == 200
    assert elapsed_ms < 2000, f"{elapsed_ms:.0f}ms(上限: 2000ms)"
    print(f"\n✅ TC09 PASS | レスポンスタイム: {elapsed_ms:.0f}ms")

実行コマンド

pytest test_get_api.py -v -s

実行結果サンプル

test_get_api.py::test_get_user_status_code      PASSED
test_get_api.py::test_get_user_raise_for_status PASSED
test_get_api.py::test_get_user_header           PASSED
test_get_api.py::test_get_user_response_body    PASSED
test_get_api.py::test_get_user_fields_exist     PASSED
test_get_api.py::test_get_user_schema           PASSED
test_get_api.py::test_get_nonexistent_user      PASSED
test_get_api.py::test_get_all_users             PASSED
test_get_api.py::test_get_user_response_time    PASSED

9 passed in 3.45s ✅

07. ハマりポイント

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


① response.json() でKeyErrorが出る

レスポンスのJSONから値を取り出すとき、キー名のスペルミスや大文字・小文字の違いで KeyError が発生しました。

# ❌ キー名のスペルミス
assert body["Name"] == "Leanne Graham"  # KeyError!(Nが大文字)

# ✅ まずprint()でキー名を確認する
print(body.keys())
# dict_keys(['id', 'name', 'username', 'email', ...])

# ✅ get()を使うとKeyErrorを防げる
name = body.get("name", "")  # キーがなければ空文字を返す

💡 ポイント:初めてテストするAPIは必ず print(response.json()) でレスポンスの構造を確認してからテストコードを書きましょう。


② ステータスコードが200なのにテストが失敗する

ステータスコードが200なのにassertが失敗するケースがありました。原因はレスポンスボディが空のJSONや想定外の構造で返ってきていたためです。

# ❌ ステータスだけ確認して安心してしまう
assert response.status_code == 200  # パスしても中身が空の可能性

# ✅ レスポンスボディも合わせて確認する
assert response.status_code == 200
assert response.json() is not None
assert len(response.json()) > 0

💡 ポイント:「200が返ってきた=成功」ではありません。レスポンスの中身まで検証するのがAPIテストの本質です。


③ isinstance()でネストしたJSONの型チェックに失敗する

ネストしたJSONフィールド(address.city など)を検証しようとして、正しくアクセスできずエラーになりました。

# ❌ ネストしたフィールドに直接アクセスしようとして失敗
assert isinstance(body["address.city"], str)  # KeyError!

# ✅ 正しいネストアクセスの書き方
assert isinstance(body["address"]["city"], str)
assert isinstance(body["address"]["zipcode"], str)

💡 ポイント:ネストしたJSONは body["address"]["city"] のように段階的にアクセスします。まず print(body["address"]) で構造を確認してから書くと安心です。


④ レスポンスタイムが環境によってバラつく

ローカルでは1000ms以内なのに、CI環境では2000msを超えてテストが失敗することがありました。

# ❌ 厳しすぎる閾値
assert elapsed_ms < 500  # CIでは失敗することがある

# ✅ 環境を考慮した余裕のある閾値に設定
assert elapsed_ms < 3000  # 3秒以内であれば十分

⚠️ 注意:レスポンスタイムはネットワーク環境やサーバー負荷によって変わります。余裕を持った閾値を設定しておきましょう。


⑤ 一覧取得でデータ件数が変わってテストが壊れる

テスト環境のデータが変わると件数も変わりテストが壊れました。実際のプロジェクトでは固定値より条件検証の方が堅牢です。

# ❌ 件数を固定値で検証すると壊れやすい
assert len(body) == 10  # データが増減すると失敗

# ✅ 件数よりも「1件以上存在すること」を検証する方が堅牢
assert len(body) >= 1, "ユーザーが1件も存在しません"
assert isinstance(body, list), "レスポンスはリスト形式であるべき"

💡 ポイント:JSONPlaceholderのような固定モックAPIは固定値でもOKです。実際のプロジェクトではデータが変わっても壊れない設計を意識しましょう。


08. よくある質問(FAQ)

Q. APIテストのGETテストで最低限確認すべきことは何ですか?
A. 最低限「ステータスコードが200であること」と「必須フィールドが存在すること」の2つです。余裕があれば型チェック・値の検証・レスポンスタイムも追加すると実務レベルのAPIテストになります。

Q. requests.get()とrequests.head()の違いは何ですか?
A. get() はレスポンスボディも取得します。head() はヘッダー情報のみ取得しボディを取得しないため高速です。ステータスコードだけ確認したい場合は head() の方が効率的ですが、サーバーによっては head() を拒否することもあります。

Q. assertとpytest.raisesはどう使い分けますか?
A. 正常系のレスポンス検証には assert を使います。例外(エラー)が発生することを確認したい場合は pytest.raises を使います。APIテストでは基本的に assert で十分です。

Q. GETテストのテストケースはどのくらい書けばいいですか?
A. 最低限「正常系1件・異常系1件」から始めましょう。実務では「ステータス確認・ボディ検証・スキーマ検証・404確認・レスポンスタイム」の5〜7件が目安です。今回のTC01〜TC07がそのまま実務の標準パターンになります。

Q. JSONPlaceholder以外のAPIテスト用サイトはありますか?
A. はい。認証フローのテストには Restful Booker、より本格的なAPIには reqres.inhttpbin.org が使えます。ポートフォリオ用には JSONPlaceholder(CRUD)と Restful Booker(認証)の組み合わせがおすすめです。


09. まとめ

PythonでAPIのGETテストを実装する場合、pytestとrequestsを使ってステータスコード・レスポンスボディ・スキーマの3つを検証することが実務の基本です。シンプルなコードで網羅的なテストが書けます。

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

テストケース内容
TC01ステータスコードが200であること
TC02raise_for_status()で4xx/5xxを検知
TC03Content-TypeがJSONであること
TC04レスポンスボディの値が正しいこと
TC05必須フィールドが存在すること
TC06スキーマ(型)が正しいこと
TC07存在しないリソースで404が返ること
TC08一覧取得で件数が正しいこと
TC09レスポンスタイムが2000ms以内であること

次の記事ではPOSTリクエストのAPIテスト(データ作成・バリデーション確認)を解説します。

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