Python으로 API DELETE 테스트 작성법|pytest×requests로 삭제 검증

테스트 자동화

📌 이 글은 이런 분께 추천합니다

  • Python과 requests로 DELETE 요청 테스트를 작성하고 싶은 분
  • API 테스트에서 데이터 삭제 검증 방법을 배우고 싶은 QA 엔지니어
  • CRUD 조작의 API 테스트를 완성하고 싶은 분
  • 삭제 후 404 확인까지 포함한 견고한 DELETE 테스트를 구현하고 싶은 분

이 글을 읽으면 알 수 있는 것

  • pytest와 requests로 DELETE 요청 API 테스트를 작성하는 기본 패턴
  • 삭제 성공 시 상태 코드(200・204)의 차이와 사용 구분
  • 삭제 후 404가 반환되는 것을 확인하는 구현 방법
  • raise_for_status()를 사용한 실무 수준의 테스트 코드 작성법

👨‍💻 필자 소개

QA 엔지니어로서 실무에서 Python・pytest・requests를 사용한 API 테스트 자동화를 담당하고 있습니다. 본 글에서 사용하는 코드는 모두 GitHub에 공개되어 있으며, 실제로 동작 확인이 완료된 코드를 그대로 해설하고 있습니다. GitHub에서 코드 보기 →

Python으로 API 테스트를 작성할 때 CRUD 조작의 마지막이 DELETE 요청 테스트입니다. pytest와 requests를 사용하면 API의 삭제 처리가 올바르게 동작하는지를 자동 테스트로 검증할 수 있습니다.

이 글에서는 DELETE 테스트의 기본부터 삭제 후 404 확인까지를 실무에서 사용할 수 있는 코드와 함께 소개합니다.


00. API 테스트와 DELETE 요청의 기본

DELETE 요청은 「리소스를 삭제하는」 조작입니다. GET이나 POST에 비해 심플하지만 삭제 후 리소스의 상태 확인이 중요합니다.

확인 항목내용
상태 코드삭제 성공 시 200 또는 204가 반환되는가200 OK / 204 No Content
삭제 후 확인삭제 후 GET하면 404가 반환되는가404 Not Found
존재하지 않는 리소스존재하지 않는 ID를 삭제하려 했을 때 적절한 오류가 반환되는가404 Not Found
응답 바디204의 경우 바디가 비어있는가{} 또는 빈 값

💡 포인트:DELETE 테스트는 「삭제할 수 있었다」뿐만 아니라 「삭제 후 접근할 수 없게 되었다」까지 확인함으로써 진정한 의미의 테스트가 됩니다.


01. pytest + requests의 API 테스트 환경

아직 환경 구축이 안 된 분은 먼저 설치해주세요.

pip install requests pytest pytest-html

이번 테스트 대상은 무료 목(mock) API JSONPlaceholder 입니다.

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

💡 포인트:JSONPlaceholder는 목 API이므로 DELETE를 실행해도 실제 데이터는 삭제되지 않습니다. 그 때문에 삭제 후 GET해도 200이 반환되는 경우가 있습니다. 실제 API에서는 삭제 후 GET에서 404가 반환되는 것을 기대하여 테스트를 작성하는 경우가 많습니다.


02. Python으로 API DELETE 테스트를 작성하는 방법

기본적인 DELETE 테스트(200 확인)

DELETE 요청을 보내고 200이 반환되는 것을 확인합니다.

import requests

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

def test_delete_user_status_code():
    """TC01: DELETE로 삭제 시 상태 코드 200이 반환되어야 한다"""
    response = requests.delete(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를 확실히 검지

def test_delete_user_raise_for_status():
    """TC02: 4xx/5xx 오류 시 예외가 발생해야 한다"""
    response = requests.delete(f"{BASE_URL}/users/1")

    response.raise_for_status()

    assert response.status_code == 200
    print(f"\n✅ TC02 PASS | raise_for_status() 정상 통과")

💡 포인트:raise_for_status() 를 사용하면 4xx/5xx를 예외로 검지할 수 있습니다. 하지만 API 테스트에서는 의도적으로 4xx/5xx를 확인하는 케이스도 있으므로, status_code의 assert와 사용 구분하는 것이 중요합니다.

응답 바디 확인

DELETE의 응답 바디가 비어있거나 빈 JSON이 반환되는 것을 확인합니다.

def test_delete_user_response_body():
    """TC03: DELETE의 응답 바디가 빈 값 또는 빈 JSON이어야 한다"""
    response = requests.delete(f"{BASE_URL}/users/1")

    response.raise_for_status()

    # 204 No Content일 때 json()을 호출하면 JSONDecodeError가 되므로 분기
    if response.status_code == 204:
        body = None
    else:
        body = response.json()

    assert body in [None, {}], \
        f"삭제 후 응답 바디는 빈 값이 기댓값: {body}"

    print(f"\n✅ TC03 PASS | 응답 바디: {body}")

💡 포인트:DELETE의 응답 바디는 API 설계에 따라 다릅니다. 빈 JSON `{}`・빈 문자열・204 No Content(바디 없음)의 패턴이 있습니다. 반드시 상태 코드를 확인하고 나서 json()을 호출하세요.


03. API 테스트 DELETE 후 상태 확인

삭제 테스트에서 가장 중요한 것이 「삭제 후 리소스에 접근할 수 없게 되어 있는가」의 확인입니다.

삭제 후 GET하면 404가 반환되는 것을 확인

def test_delete_then_get_returns_404():
    """TC04: 삭제 후 GET하면 404가 반환되어야 한다"""
    # Step1: 리소스를 삭제
    delete_response = requests.delete(f"{BASE_URL}/users/1")
    assert delete_response.status_code == 200, "삭제가 실패했습니다"

    # Step2: 삭제 후 GET해본다
    get_response = requests.get(f"{BASE_URL}/users/1")

    # JSONPlaceholder는 목이므로 200이 반환되는 경우도 있다
    # 실제 API에서는 404가 반환되는 것을 검증한다
    assert get_response.status_code in [200, 404], \
        f"삭제 후 GET은 404가 기댓값: {get_response.status_code}"

    print(f"\n✅ TC04 PASS | 삭제 후 GET 상태: {get_response.status_code}")

💡 포인트:삭제 후 404가 반환되는 것의 확인은 2스텝 테스트(DELETE→GET)로 실현합니다. 이것이 DELETE 테스트에서 가장 중요한 검증입니다.


04. API 테스트 DELETE 이상계 패턴

존재하지 않는 리소스 삭제(404 확인)

def test_delete_nonexistent_user():
    """TC05: 존재하지 않는 리소스 삭제 시 404가 반환되어야 한다"""
    response = requests.delete(f"{BASE_URL}/users/99999")

    # JSONPlaceholder는 목이므로 200이 반환되는 경우도 있다
    # 실제 API에서는 404가 기대됨
    assert response.status_code in [200, 404], \
        f"예상 밖의 상태 코드: {response.status_code}"

    print(f"\n✅ TC05 PASS | 존재하지 않는 리소스 삭제: {response.status_code}")

⚠️ 주의:JSONPlaceholder는 목 API이므로 존재하지 않는 ID에도 200을 반환하는 경우가 있습니다. 실제 API에서는 404가 반환되는 것을 검증해주세요.

200과 204 둘 다에 대응한 테스트

def test_delete_user_accepts_200_or_204():
    """TC06: 삭제 성공 시 200 또는 204가 반환되어야 한다"""
    response = requests.delete(f"{BASE_URL}/users/1")

    # REST API 관습: 삭제 성공은 200 또는 204
    assert response.status_code in [200, 204], \
        f"삭제 성공 시 200 또는 204가 기댓값: {response.status_code}"

    print(f"\n✅ TC06 PASS | 상태 코드: {response.status_code}")

💡 포인트:삭제 성공 시 상태 코드는 API 설계에 따라 200 또는 204가 반환됩니다. 둘 다에 대응할 수 있는 작성법으로 해두면 범용성이 높아집니다.


05. API 테스트 DELETE 헤더 검증

def test_delete_user_header():
    """TC07: Content-Type이 JSON이어야 한다(204 이외의 경우)"""
    response = requests.delete(f"{BASE_URL}/users/1")

    response.raise_for_status()

    # 204 No Content는 Content-Type 헤더가 존재하지 않는 경우가 있다
    if response.status_code != 204:
        content_type = response.headers.get("Content-Type", "")
        assert "application/json" in content_type, \
            f"예상 밖의 Content-Type: {content_type}"
        print(f"\n✅ TC07 PASS | Content-Type: {content_type}")
    else:
        print(f"\n✅ TC07 PASS | 204 No Content이므로 Content-Type 체크 스킵")

06. DELETE 테스트 전체 코드

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

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


def test_delete_user_status_code():
    """TC01: DELETE로 삭제 시 상태 코드 200이 반환되어야 한다"""
    response = requests.delete(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_delete_user_raise_for_status():
    """TC02: 4xx/5xx 오류 시 예외가 발생해야 한다"""
    response = requests.delete(f"{BASE_URL}/users/1")
    response.raise_for_status()
    assert response.status_code == 200
    print(f"\n✅ TC02 PASS | raise_for_status() 정상 통과")


def test_delete_user_response_body():
    """TC03: DELETE의 응답 바디가 빈 값 또는 빈 JSON이어야 한다"""
    response = requests.delete(f"{BASE_URL}/users/1")
    response.raise_for_status()
    if response.status_code == 204:
        body = None
    else:
        body = response.json()
    assert body in [None, {}]
    print(f"\n✅ TC03 PASS | body: {body}")


def test_delete_then_get_returns_404():
    """TC04: 삭제 후 GET하면 404가 반환되어야 한다"""
    delete_response = requests.delete(f"{BASE_URL}/users/1")
    assert delete_response.status_code == 200
    get_response = requests.get(f"{BASE_URL}/users/1")
    assert get_response.status_code in [200, 404]
    print(f"\n✅ TC04 PASS | 삭제 후 GET 상태: {get_response.status_code}")


def test_delete_nonexistent_user():
    """TC05: 존재하지 않는 리소스 삭제 시 404가 반환되어야 한다"""
    response = requests.delete(f"{BASE_URL}/users/99999")
    assert response.status_code in [200, 404]
    print(f"\n✅ TC05 PASS | status: {response.status_code}")


def test_delete_user_accepts_200_or_204():
    """TC06: 삭제 성공 시 200 또는 204가 반환되어야 한다"""
    response = requests.delete(f"{BASE_URL}/users/1")
    assert response.status_code in [200, 204]
    print(f"\n✅ TC06 PASS | status: {response.status_code}")


def test_delete_user_header():
    """TC07: Content-Type이 JSON이어야 한다(204 이외의 경우)"""
    response = requests.delete(f"{BASE_URL}/users/1")
    response.raise_for_status()
    if response.status_code != 204:
        content_type = response.headers.get("Content-Type", "")
        assert "application/json" in content_type
        print(f"\n✅ TC07 PASS | Content-Type: {content_type}")
    else:
        print(f"\n✅ TC07 PASS | 204이므로 스킵")

실행 커맨드

pytest test_delete_api.py -v -s

실행 결과 샘플

test_delete_api.py::test_delete_user_status_code        PASSED
test_delete_api.py::test_delete_user_raise_for_status   PASSED
test_delete_api.py::test_delete_user_response_body      PASSED
test_delete_api.py::test_delete_then_get_returns_404    PASSED
test_delete_api.py::test_delete_nonexistent_user        PASSED
test_delete_api.py::test_delete_user_accepts_200_or_204 PASSED
test_delete_api.py::test_delete_user_header             PASSED

7 passed in 3.56s ✅

07. 자주 겪는 문제 & 해결법

구현 중 실제로 겪었던 문제들을 정리했습니다. 같은 곳에서 막히는 분들께 도움이 되면 좋겠습니다.


① 삭제 성공이 200인지 204인지 헷갈린다

DELETE 성공 응답이 200인지 204인지 헷갈렸습니다. API 설계에 따라 다릅니다.

# ❌ 고정으로 200만을 기대해버린다
assert response.status_code == 200

# ✅ 200과 204 둘 다를 허용한다
assert response.status_code in [200, 204]
# 200 OK         → 응답 바디 있음(삭제한 리소스 정보 등)
# 204 No Content → 응답 바디 없음(심플한 삭제 확인)

💡 포인트:테스트를 작성하기 전에 API 문서에서 기대하는 상태 코드를 확인하세요. JSONPlaceholder는 200을 반환합니다.


② 204일 때 response.json()에서 오류가 발생한다

204 No Content의 응답에서 response.json() 을 호출했더니 바디가 비어있어 오류가 발생했습니다.

# ❌ 204일 때 json()을 호출하면 오류가 발생한다
body = response.json()  # JSONDecodeError!

# ✅ 상태 코드로 분기한다
if response.status_code == 204:
    print("응답 바디 없음(204 No Content)")
else:
    body = response.json()
    print(f"응답 바디: {body}")

⚠️ 주의:204 No Content는 응답 바디가 존재하지 않습니다. 상태 코드를 확인하고 나서 json()을 호출하도록 하세요.


③ 목 API에서 삭제 후 404를 확인할 수 없다

JSONPlaceholder에서 DELETE 후 GET해도 200이 반환되어버려 삭제 후 404 확인이 불가능했습니다.

# JSONPlaceholder는 목이므로
# DELETE 후 GET해도 200이 반환되어버린다
requests.delete(f"{BASE_URL}/users/1")
response = requests.get(f"{BASE_URL}/users/1")
print(response.status_code)  # → 200(실제 API에서는 404가 기대됨)

💡 포인트:삭제 후 404 확인은 실제로 데이터가 삭제되는 API로 수행해야 합니다. Restful Booker나 직접 만든 API로 검증하세요.


④ DELETE 요청에 요청 바디를 포함해버린다

DELETE 요청에 요청 바디를 포함하려 했지만, 대부분의 REST API에서 DELETE는 URL로 삭제 대상을 지정하므로 일반적으로 바디는 불필요합니다.

# ❌ DELETE에 바디를 포함하는 것은 대부분의 REST API에서 일반적이지 않다
response = requests.delete(url, json={"id": 1})

# ✅ DELETE는 URL만으로 지정하는 것이 기본
response = requests.delete(f"{BASE_URL}/users/1")

💡 포인트:DELETE는 통상 URL 파라미터로 삭제 대상을 지정하므로 대부분의 REST API에서는 요청 바디는 불필요합니다. 다만 Elasticsearch 등 일부 API에서는 삭제 조건을 바디로 보내는 설계도 있습니다. API 문서로 확인하세요.


⑤ 이미 삭제된 리소스를 재삭제했을 때의 동작 확인을 잊어버린다

한 번 삭제한 리소스를 다시 삭제하려고 했을 때 어떤 응답이 반환되는지 확인하는 것을 잊었습니다.

# 삭제된 리소스를 재삭제한 경우
response1 = requests.delete(f"{BASE_URL}/users/1")  # 1회째: 성공(200)
response2 = requests.delete(f"{BASE_URL}/users/1")  # 2회째: 이미 존재하지 않음

assert response1.status_code == 200
# 실제 API에서는 2회째는 404가 기대됨
assert response2.status_code in [200, 404]

💡 포인트:재삭제 테스트는 실제 API에서의 검증이 필요합니다. 목 API에서는 확인할 수 없습니다.


08. DELETE API 테스트의 베스트 프랙티스

DELETE API 테스트에서는 다음 3가지를 확인하는 것이 중요합니다.

확인 항목내용중요도
① 상태 코드 확인삭제 성공 시 200 또는 204가 반환될 것⭐⭐⭐ 필수
② 삭제 후 404 확인삭제 후 GET하면 404가 반환될 것⭐⭐⭐ 중요
③ 이상계 확인존재하지 않는 리소스 삭제 시 적절한 오류가 반환될 것⭐⭐ 권장

이 3가지를 확인함으로써 DELETE API의 품질을 높은 수준으로 보증할 수 있습니다. 특히 삭제 후 GET하면 404가 반환되는 것의 확인은 단순히 「삭제할 수 있었다」뿐만 아니라 「삭제 후 접근할 수 없게 되었다」까지 검증하므로 실무에서는 반드시 실시하세요.

💡 포인트:GET・POST・PUT/PATCH・DELETE의 4종류를 구현함으로써 CRUD 전 조작을 커버한 완전한 API 테스트 스위트가 완성됩니다. 이것은 포트폴리오로서도 매우 강력한 어필이 됩니다.


09. 자주 묻는 질문(FAQ)

Q. DELETE 테스트에서 최소한 확인해야 할 것은 무엇인가요?
A. 최소한 「상태 코드가 200 또는 204인 것」1가지입니다. 여유가 있다면 「삭제 후 GET하면 404가 반환되는 것」도 추가하면 실무 수준의 테스트가 됩니다.

Q. DELETE 상태 코드는 200과 204 어느 것이 올바른가요?
A. 둘 다 올바릅니다. API 설계에 따라 다릅니다. 삭제 후 리소스 정보를 반환하는 경우는 200, 아무것도 반환하지 않는 경우는 204가 사용되는 경우가 많습니다. 테스트를 작성하기 전에 API 문서로 확인하세요.

Q. DELETE 요청에 요청 바디가 필요한가요?
A. 일반적으로 불필요합니다. DELETE는 URL 경로 파라미터로 삭제 대상을 지정합니다(예:/users/1). 다만 일부 API에서는 삭제 이유 등을 바디로 보내는 경우도 있습니다. API 문서로 확인하세요.

Q. 삭제 후 404 확인은 어느 API로 시험해볼 수 있나요?
A. JSONPlaceholder는 목 API이므로 삭제 후 404 확인이 불가능합니다. reqres.in 이나 직접 만든 API, 또는 Restful Booker의 예약 삭제 엔드포인트로 시험해볼 수 있습니다.

Q. CRUD 테스트를 전부 작성하면 몇 건이 되나요?
A. 이번 시리즈에서 구현한 테스트 케이스는 GET(9건)・POST(9건)・PUT/PATCH(9건)・DELETE(7건)의 합계 34건입니다. 이 정도면 「CRUD가 전부 가능하다」는 포트폴리오로서 충분한 어필이 됩니다.


10. 정리

Python으로 API DELETE 테스트를 구현하는 경우, pytest와 requests를 사용해서 상태 코드와 삭제 후 상태 확인이 실무의 기본입니다. DELETE는 심플한 테스트이지만 삭제 후 404가 반환되는 것까지 확인함으로써 완전한 CRUD 테스트가 됩니다.

이 글에서는 pytest와 requests를 사용한 API DELETE 테스트 구현 방법을 해설했습니다.

테스트 케이스내용
TC01DELETE로 삭제 시 상태 코드 200이 반환되어야 한다
TC02raise_for_status()로 4xx/5xx를 검지
TC03DELETE의 응답 바디가 빈 값 또는 빈 JSON이어야 한다
TC04삭제 후 GET하면 404가 반환되어야 한다
TC05존재하지 않는 리소스 삭제 시 404가 반환되어야 한다
TC06삭제 성공 시 200 또는 204가 반환되어야 한다
TC07Content-Type이 JSON이어야 한다(204 이외의 경우)

GET・POST・PUT/PATCH・DELETE 전부 구현하여 CRUD 전 조작의 API 테스트가 완성되었습니다. 다음 글에서는 시리즈 마무리로 실무에서의 API 테스트 설계 방법을 해설합니다.

제목과 URL을 복사했습니다