API ํ…Œ์ŠคํŠธ๋ž€๏ผŸPython๏ผˆpytest๏ผ‹requests๏ผ‰์œผ๋กœ ์‹œ์ž‘ํ•˜๋Š” ์ž๋™ํ™” ์ž…๋ฌธ

๐Ÿ“Œ ์ด ๊ธ€์€ ์ด๋Ÿฐ ๋ถ„๊ป˜ ์ถ”์ฒœํ•ฉ๋‹ˆ๋‹ค

  • API ํ…Œ์ŠคํŠธ๊ฐ€ ๋ฌด์—‡์ธ์ง€ ์•Œ๊ณ  ์‹ถ์€ QA ์—”์ง€๋‹ˆ์–ดใƒป๊ฐœ๋ฐœ์ž
  • UI ํ…Œ์ŠคํŠธ์™€ API ํ…Œ์ŠคํŠธ์˜ ์ฐจ์ด๋ฅผ ์ดํ•ดํ•˜๊ณ  ์‹ถ์€ ๋ถ„
  • Python์œผ๋กœ ์ž๋™ํ™” ํ…Œ์ŠคํŠธ๋ฅผ ์‹œ์ž‘ํ•˜๊ณ  ์‹ถ์€ ์—”์ง€๋‹ˆ์–ด
  • pytest์™€ requests์˜ ์‚ฌ์šฉ๋ฒ•์„ ๊ธฐ์ดˆ๋ถ€ํ„ฐ ๋ฐฐ์šฐ๊ณ  ์‹ถ์€ ๋ถ„

โœ… ์ด ๊ธ€์„ ์ฝ์œผ๋ฉด ์•Œ ์ˆ˜ ์žˆ๋Š” ๊ฒƒ

  • API ํ…Œ์ŠคํŠธ์™€ UI ํ…Œ์ŠคํŠธ์˜ ์ฐจ์ด์™€ ์‚ฌ์šฉ ๊ตฌ๋ถ„
  • requests ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋กœ HTTP ์š”์ฒญ์„ ๋ณด๋‚ด๋Š” ๋ฐฉ๋ฒ•
  • pytest๋กœ API ํ…Œ์ŠคํŠธ๋ฅผ ์ž๋™ ์‹คํ–‰ํ•˜๋Š” ๊ธฐ๋ณธ ํŒจํ„ด
  • HTML ๋ฆฌํฌํŠธ๋ฅผ ์ž๋™ ์ƒ์„ฑํ•ด์„œ ํฌํŠธํด๋ฆฌ์˜ค์— ํ™œ์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•

๐Ÿ‘จโ€๐Ÿ’ป ํ•„์ž ์†Œ๊ฐœ

QA ์—”์ง€๋‹ˆ์–ด๋กœ์„œ ์‹ค๋ฌด์—์„œ PlaywrightใƒปPythonใƒปpytest๋ฅผ ์‚ฌ์šฉํ•œ API ํ…Œ์ŠคํŠธ ์ž๋™ํ™”๋ฅผ ๋‹ด๋‹นํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ๋ณธ ๊ธ€์—์„œ ์‚ฌ์šฉํ•˜๋Š” ์ฝ”๋“œ๋Š” ๋ชจ๋‘ GitHub์— ๊ณต๊ฐœ๋˜์–ด ์žˆ์œผ๋ฉฐ, ์‹ค์ œ๋กœ ๋™์ž‘ ํ™•์ธ์ด ์™„๋ฃŒ๋œ ์ฝ”๋“œ๋ฅผ ๊ทธ๋Œ€๋กœ ํ•ด์„คํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

ใ€ŒAPI ํ…Œ์ŠคํŠธ๋Š” ์–ด๋ ต๊ฒ ์ง€โ€ฆใ€๋ผ๊ณ  ์ƒ๊ฐํ•˜๊ณ  ์žˆ์ง€๋Š” ์•Š์œผ์‹ ๊ฐ€์š”? ์‚ฌ์‹ค requests์™€ pytest ๋‘ ๊ฐ€์ง€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์กฐํ•ฉํ•˜๋Š” ๊ฒƒ๋งŒ์œผ๋กœ ์‹ค๋ฌด ์ˆ˜์ค€์˜ API ํ…Œ์ŠคํŠธ๋ฅผ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์ด ๊ธ€์—์„œ๋Š” API ํ…Œ์ŠคํŠธ์˜ ๊ธฐ๋ณธ ๊ฐœ๋…๋ถ€ํ„ฐ ํ™˜๊ฒฝ ๊ตฌ์ถ•ใƒป์‹ค์ œ ์ฝ”๋“œ๊นŒ์ง€ ๊ผผ๊ผผํ•˜๊ฒŒ ํ•ด์„คํ•ฉ๋‹ˆ๋‹ค. UI ํ…Œ์ŠคํŠธ ๊ฒฝํ—˜๋ฐ–์— ์—†๋Š” ๋ถ„๋„ ๊ธ€์„ ๋‹ค ์ฝ์„ ๋ฌด๋ ต์—๋Š” ใ€ŒAPI ํ…Œ์ŠคํŠธ๋„ ๋‚ด๊ฐ€ ์ง์ ‘ ์“ธ ์ˆ˜ ์žˆ๋‹คใ€๋Š” ์ž์‹ ๊ฐ์ด ์ƒ๊ธธ ๊ฒƒ์ž…๋‹ˆ๋‹ค.


00. API ํ…Œ์ŠคํŠธ๋ž€๏ผŸ

API ํ…Œ์ŠคํŠธ๋ž€, ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ใ€Œ๋‚ด๋ถ€ ๊ตฌ์กฐ๏ผˆAPI๏ผ‰ใ€๊ฐ€ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๋™์ž‘ํ•˜๋Š”์ง€ ํ™•์ธํ•˜๋Š” ํ…Œ์ŠคํŠธ์ž…๋‹ˆ๋‹ค.

UI ํ…Œ์ŠคํŠธ๊ฐ€ ๋ธŒ๋ผ์šฐ์ €๋ฅผ ์กฐ์ž‘ํ•ด์„œ ํ™”๋ฉด ์ „์ฒด๋ฅผ ํ™•์ธํ•˜๋Š” ๊ฒƒ๊ณผ ๋‹ฌ๋ฆฌ, API ํ…Œ์ŠคํŠธ๋Š” ๋ธŒ๋ผ์šฐ์ €๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ์•Š๊ณ  HTTP ์š”์ฒญ์„ ์ง์ ‘ ์ „์†กํ•˜์—ฌ ์‘๋‹ต์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค.

UI ํ…Œ์ŠคํŠธ API ํ…Œ์ŠคํŠธ
ํ™•์ธ ๋Œ€์ƒ ํ™”๋ฉดใƒป์™ธ๊ด€ใƒป์กฐ์ž‘๊ฐ ๋ฐ์ดํ„ฐใƒป๋กœ์งใƒป๋ณด์•ˆ
๋ธŒ๋ผ์šฐ์ € ํ•„์š” ๋ถˆํ•„์š”
์‹คํ–‰ ์†๋„ ๋А๋ฆผ ๐Ÿข ๋งค์šฐ ๋น ๋ฆ„ โšก
๊นจ์ง€๊ธฐ ์‰ฌ์›€ UI ๋ณ€๊ฒฝ์œผ๋กœ ์‰ฝ๊ฒŒ ๊นจ์ง ๋กœ์ง์ด ๋ฐ”๋€Œ์ง€ ์•Š๋Š” ํ•œ ์•ˆ์ •์ 

๐Ÿ’ก ํฌ์ธํŠธ๏ผšUI ํ…Œ์ŠคํŠธ์™€ API ํ…Œ์ŠคํŠธ๋Š” ใ€Œ์–ด๋А ์ชฝ์ด ๋” ์ข‹๋‹คใ€๊ฐ€ ์•„๋‹ˆ๋ผ, ๊ฐ๊ฐ ์—ญํ• ์ด ๋‹ค๋ฆ…๋‹ˆ๋‹ค. ๋‘˜ ๋‹ค ํ•  ์ˆ˜ ์žˆ์œผ๋ฉด QA ์—”์ง€๋‹ˆ์–ด๋กœ์„œ์˜ ๊ฐ€์น˜๊ฐ€ ํฌ๊ฒŒ ๋†’์•„์ง‘๋‹ˆ๋‹ค.


01. API ํ…Œ์ŠคํŠธ๊ฐ€ ์™œ ์ค‘์š”ํ•œ๊ฐ€๏ผŸ

ํ˜„๋Œ€์˜ ์›น ์•ฑ์€ ๊ฑฐ์˜ ๋ชจ๋‘ ํ”„๋ก ํŠธ์—”๋“œ๏ผˆํ™”๋ฉด๏ผ‰์™€ ๋ฐฑ์—”๋“œ๏ผˆAPI๏ผ‰๊ฐ€ ๋ถ„๋ฆฌ๋œ ๊ตฌ์กฐ๋กœ ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

API๊ฐ€ ๋ง๊ฐ€์ง€๋ฉด ํ™”๋ฉด์ด ์•„๋ฌด๋ฆฌ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ํ‘œ์‹œ๋˜์–ด ์žˆ์–ด๋„ ๋ฐ์ดํ„ฐ๊ฐ€ ์ €์žฅ๋˜์ง€ ์•Š๊ฑฐ๋‚˜ใƒป๋กœ๊ทธ์ธ์ด ์•ˆ ๋˜๊ฑฐ๋‚˜ใƒป๊ฒฐ์ œ๊ฐ€ ์‹คํŒจํ•˜๋Š” ๋“ฑ ์น˜๋ช…์ ์ธ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค.

๋˜ํ•œ API ํ…Œ์ŠคํŠธ๋Š” UI ํ…Œ์ŠคํŠธ๋ณด๋‹ค 10ใ€œ100๋ฐฐ ๋น ๋ฅด๊ฒŒ ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ๊ธฐ ๋•Œ๋ฌธ์—, CI/CD ํŒŒ์ดํ”„๋ผ์ธ์— ํ†ตํ•ฉํ•˜์—ฌ ๋งค๋ฒˆ ๋ฐฐํฌ ์ „์— ์ž๋™์œผ๋กœ ํ™•์ธํ•˜๋Š” ๊ฒƒ์ด ์‹ค๋ฌด์˜ ์ •์„์ž…๋‹ˆ๋‹ค.

# API ํ…Œ์ŠคํŠธ ์ด๋ฏธ์ง€
# ๋ธŒ๋ผ์šฐ์ € ์—†์ด๏ผ์ฝ”๋“œ๋งŒ์œผ๋กœ ์„œ๋ฒ„์™€ ํ†ต์‹ ํ•œ๋‹ค

import requests

response = requests.get("https://jsonplaceholder.typicode.com/users/1")
print(response.status_code)  # 200
print(response.json())       # {"id": 1, "name": "Leanne Graham", ...}

๐Ÿ’ก ํฌ์ธํŠธ๏ผš๋‹จ 3์ค„๋กœ API์— ์š”์ฒญ์„ ๋ณด๋‚ด๊ณ  ์‘๋‹ต์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๊ฒƒ์ด API ํ…Œ์ŠคํŠธ์˜ ๊ธฐ๋ณธ ํ˜•ํƒœ์ž…๋‹ˆ๋‹ค.


02. ํ™˜๊ฒฝ ๊ตฌ์ถ•

ํ•„์š”ํ•œ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์„ค์น˜

์•„๋ž˜ 3๊ฐ€์ง€๋ฅผ ์„ค์น˜ํ•˜๊ธฐ๋งŒ ํ•˜๋ฉด ๋™์ž‘ํ•ฉ๋‹ˆ๋‹ค.

pip install requests pytest pytest-html
๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์—ญํ• 
requests API์— HTTP ์š”์ฒญ์„ ๋ณด๋‚ธ๋‹ค
pytest ํ…Œ์ŠคํŠธ๋ฅผ ์‹คํ–‰ใƒป๊ด€๋ฆฌํ•œ๋‹ค
pytest-html HTML ๋ฆฌํฌํŠธ๋ฅผ ์ž๋™ ์ƒ์„ฑํ•œ๋‹ค

pytest.ini ์„ค์ •

ํ”„๋กœ์ ํŠธ ๋ฃจํŠธ์— pytest.ini ๋ฅผ ๋‘๋Š” ๊ฒƒ์œผ๋กœ, ํ…Œ์ŠคํŠธ ์‹คํ–‰ ๋•Œ๋งˆ๋‹ค HTML ๋ฆฌํฌํŠธ๊ฐ€ ์ž๋™ ์ƒ์„ฑ๋ฉ๋‹ˆ๋‹ค.

# pytest.ini
[pytest]
addopts = --html=report.html --self-contained-html

๐Ÿ’ก Tip๏ผšํ•œ ๋ฒˆ ์„ค์ •ํ•ด๋‘๋ฉด ์ดํ›„๋กœ๋Š” pytest test_api.py -v ๋งŒ์œผ๋กœ ์ž๋™์œผ๋กœ HTML ๋ฆฌํฌํŠธ๊ฐ€ ์ƒ์„ฑ๋ฉ๋‹ˆ๋‹ค. ํฌํŠธํด๋ฆฌ์˜ค ์ฆ๊ฑฐ๋กœ๋„ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.


03. requests ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ๊ธฐ๋ณธ

HTTP ๋ฉ”์„œ๋“œ ์‚ฌ์šฉ๋ฒ•

HTTP ๋ฉ”์„œ๋“œ์— ๋Œ€์‘ํ•˜๋Š” ๋ฉ”์„œ๋“œ๊ฐ€ ๊ฐ๊ฐ ์ค€๋น„๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

import requests

# GET: ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜จ๋‹ค
response = requests.get("https://jsonplaceholder.typicode.com/users/1")

# POST: ๋ฐ์ดํ„ฐ๋ฅผ ์ƒ์„ฑํ•œ๋‹ค
response = requests.post(
    "https://jsonplaceholder.typicode.com/users",
    json={"name": "Taro", "email": "taro@example.com"}
)

# PUT: ๋ฐ์ดํ„ฐ๋ฅผ ์—…๋ฐ์ดํŠธํ•œ๋‹ค
response = requests.put(
    "https://jsonplaceholder.typicode.com/users/1",
    json={"name": "Updated Taro"}
)

# DELETE: ๋ฐ์ดํ„ฐ๋ฅผ ์‚ญ์ œํ•œ๋‹ค
response = requests.delete("https://jsonplaceholder.typicode.com/users/1")

์‘๋‹ต ํ™•์ธ ๋ฐฉ๋ฒ•

response = requests.get("https://jsonplaceholder.typicode.com/users/1")

print(response.status_code)  # ์ƒํƒœ ์ฝ”๋“œ: 200
print(response.json())       # ์‘๋‹ต ๋ฐ”๋””๏ผˆJSON ํ˜•์‹๏ผ‰
print(response.headers)      # ์‘๋‹ต ํ—ค๋”

๐Ÿ’ก ํฌ์ธํŠธ๏ผšresponse.json() ์„ ์‚ฌ์šฉํ•˜๋ฉด ์‘๋‹ต์˜ JSON์„ Python ๋”•์…”๋„ˆ๋ฆฌ ํ˜•ํƒœ๋กœ ๋‹ค๋ฃฐ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๊ทธ๋Œ€๋กœ ์–ด์„œ์…˜์— ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์–ด์„œ ํŽธ๋ฆฌํ•ฉ๋‹ˆ๋‹ค.


04. pytest ๊ธฐ๋ณธ

ํ…Œ์ŠคํŠธ ์ž‘์„ฑ๋ฒ•

ํ•จ์ˆ˜๋ช…์„ test_ ๋กœ ์‹œ์ž‘ํ•˜๊ธฐ๋งŒ ํ•˜๋ฉด pytest๊ฐ€ ์ž๋™์œผ๋กœ ํ…Œ์ŠคํŠธ๋กœ ์ธ์‹ํ•ฉ๋‹ˆ๋‹ค.

import requests

def test_get_user():
    """์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์žˆ๋Š”์ง€ ํ™•์ธ"""
    response = requests.get("https://jsonplaceholder.typicode.com/users/1")

    assert response.status_code == 200, f"๊ธฐ๋Œ“๊ฐ’: 200, ์‹ค์ œ: {response.status_code}"
    assert response.json()["id"] == 1
    assert "name" in response.json()

์‹คํ–‰ ์ปค๋งจ๋“œ

# ๋ชจ๋“  ํ…Œ์ŠคํŠธ๋ฅผ ์‹คํ–‰
pytest

# ์ƒ์„ธ ํ‘œ์‹œ๋กœ ์‹คํ–‰
pytest -v

# ํŠน์ • ํŒŒ์ผ๋งŒ ์‹คํ–‰
pytest test_api.py -v

์‹คํ–‰ ๊ฒฐ๊ณผ ์ƒ˜ํ”Œ

test_api.py::test_get_user       PASSED  [ 33%]
test_api.py::test_create_user    PASSED  [ 66%]
test_api.py::test_delete_user    PASSED  [100%]

3 passed in 1.23s โœ…

๐Ÿ’ก ํฌ์ธํŠธ๏ผšassert ๊ฐ€ ์‹คํŒจํ–ˆ์„ ๋•Œ๋Š”, ์–ด๋–ค ๊ฐ’์ด ๊ธฐ๋Œ“๊ฐ’๊ณผ ๋‹ฌ๋ž๋Š”์ง€๋ฅผ ์ž๋™์œผ๋กœ ํ‘œ์‹œํ•ด์ค๋‹ˆ๋‹ค. ๋””๋ฒ„๊น…์ด ๋งค์šฐ ํŽธํ•ด์ง‘๋‹ˆ๋‹ค.


05. pytest + requests ์กฐํ•ฉํ•˜๊ธฐ

์ด ๋‘ ๊ฐ€์ง€๋ฅผ ์กฐํ•ฉํ•˜๋ฉด ์‹ค๋ฌด ์ˆ˜์ค€์˜ API ํ…Œ์ŠคํŠธ๋ฅผ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. JSONPlaceholder๋ผ๋Š” ๋ฌด๋ฃŒ ๋ชฉ๏ผˆmock๏ผ‰ API๋ฅผ ์‚ฌ์šฉํ•ด์„œ ์‹ค์ œ๋กœ ๋™์ž‘์‹œ์ผœ ๋ด…์‹œ๋‹ค.

import requests
import pytest

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

def test_get_all_users():
    """๋ชจ๋“  ์‚ฌ์šฉ์ž๋ฅผ ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์žˆ๋Š”์ง€ ํ™•์ธ"""
    response = requests.get(f"{BASE_URL}/users")

    assert response.status_code == 200
    assert len(response.json()) == 10  # 10๋ช…์˜ ์‚ฌ์šฉ์ž๊ฐ€ ๋ฐ˜ํ™˜๋  ๊ฒƒ

def test_create_user():
    """์‚ฌ์šฉ์ž๋ฅผ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ๋Š”์ง€ ํ™•์ธ"""
    new_user = {"name": "Yoshitsugu", "email": "test@example.com"}
    response = requests.post(f"{BASE_URL}/users", json=new_user)

    assert response.status_code == 201
    assert response.json()["name"] == "Yoshitsugu"

def test_delete_user():
    """์‚ฌ์šฉ์ž๋ฅผ ์‚ญ์ œํ•  ์ˆ˜ ์žˆ๋Š”์ง€ ํ™•์ธ"""
    response = requests.delete(f"{BASE_URL}/users/1")

    assert response.status_code == 200

โš ๏ธ ์ฃผ์˜๏ผšJSONPlaceholder๋Š” ๋ชฉ๏ผˆmock๏ผ‰ API์ด๋ฏ€๋กœ ์‹ค์ œ๋กœ๋Š” ๋ฐ์ดํ„ฐ๊ฐ€ ์ €์žฅใƒป์‚ญ์ œ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ํ•™์Šต์šฉ์œผ๋กœ ์•ˆ์‹ฌํ•˜๊ณ  ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.


06. ์ž์ฃผ ๊ฒช๋Š” ๋ฌธ์ œ & ํ•ด๊ฒฐ๋ฒ•

๊ตฌํ˜„ ์ค‘ ์‹ค์ œ๋กœ ๊ฒช์—ˆ๋˜ ๋ฌธ์ œ๋“ค์„ ์ •๋ฆฌํ–ˆ์Šต๋‹ˆ๋‹ค. ๊ฐ™์€ ๊ณณ์—์„œ ๋ง‰ํžˆ๋Š” ๋ถ„๋“ค๊ป˜ ๋„์›€์ด ๋˜๋ฉด ์ข‹๊ฒ ์Šต๋‹ˆ๋‹ค.


โ‘  requests์™€ pytest-requests๋ฅผ ํ˜ผ๋™ํ•ด๋ฒ„๋ฆฐ๋‹ค

์ฒ˜์Œ์— ใ€Œpytest๋กœ HTTP ์š”์ฒญ์„ ๋ณด๋‚ด๋Š” ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌใ€๋ฅผ ๊ฒ€์ƒ‰ํ–ˆ๋”๋‹ˆ pytest-requests ๋ผ๋Š” ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์ฐพ์•„ ์„ค์น˜ํ•ด๋ฒ„๋ ธ์Šต๋‹ˆ๋‹ค. ์‹ค์ œ๋กœ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์€ ๋ณ„๊ฐœ์˜ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋กœ, requests๏ผˆHTTP ํด๋ผ์ด์–ธํŠธ๏ผ‰์™€ pytest๏ผˆํ…Œ์ŠคํŠธ ํ”„๋ ˆ์ž„์›Œํฌ๏ผ‰๋ฅผ ์กฐํ•ฉํ•ด์„œ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.

# โŒ pytest-requests๋Š” ์ด๋ฒˆ์— ์‚ฌ์šฉํ•˜์ง€ ์•Š์Œ
pip install pytest-requests

# โœ… ์˜ฌ๋ฐ”๋ฅธ ์„ค์น˜
pip install requests pytest pytest-html

๐Ÿ’ก ํฌ์ธํŠธ๏ผšrequests ๋Š” HTTP ์š”์ฒญ ์ „์šฉ, pytest ๋Š” ํ…Œ์ŠคํŠธ ์‹คํ–‰ ์ „์šฉ์ž…๋‹ˆ๋‹ค. ์—ญํ• ์ด ๋‹ค๋ฅด๋ฏ€๋กœ ๋‘˜ ๋‹ค ์„ค์น˜ํ•˜์—ฌ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.


โ‘ก response.json()์—์„œ KeyError๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค

์‘๋‹ต์˜ JSON์—์„œ ๊ฐ’์„ ๊บผ๋‚ด๋ ค๊ณ  ํ–ˆ์„ ๋•Œ ์กด์žฌํ•˜์ง€ ์•Š๋Š” ํ‚ค๋ฅผ ์ง€์ •ํ•ด๋ฒ„๋ ค KeyError ๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค. ๋จผ์ € ์‘๋‹ต ๋‚ด์šฉ์„ ํ™•์ธํ•˜๊ณ  ๋‚˜์„œ ์ ‘๊ทผํ•˜๋Š” ์Šต๊ด€์ด ์ค‘์š”ํ•ฉ๋‹ˆ๋‹ค.

# โŒ ํ‚ค๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š์œผ๋ฉด KeyError ๋ฐœ์ƒ
user_id = response.json()["userId"]  # KeyError!

# โœ… ๋จผ์ € print()๋กœ ๋‚ด์šฉ์„ ํ™•์ธํ•œ๋‹ค
print(response.json())
# {'id': 1, 'name': 'Leanne Graham', ...}

# โœ… get()์„ ์‚ฌ์šฉํ•˜๋ฉด KeyError๋ฅผ ๋ฐฉ์ง€ํ•  ์ˆ˜ ์žˆ๋‹ค
user_id = response.json().get("id")  # ์˜ค๋ฅ˜ ๋Œ€์‹  None์„ ๋ฐ˜ํ™˜

๐Ÿ’ก ํฌ์ธํŠธ๏ผš์ฒ˜์Œ ์ ‘ํ•˜๋Š” API๋Š” ๋ฐ˜๋“œ์‹œ print(response.json()) ๋กœ ์‘๋‹ต ๊ตฌ์กฐ๋ฅผ ํ™•์ธํ•˜๊ณ  ๋‚˜์„œ ์–ด์„œ์…˜์„ ์ž‘์„ฑํ•ฉ์‹œ๋‹ค.


โ‘ข assert๊ฐ€ ์‹คํŒจํ•ด๋„ ์›์ธ์„ ์•Œ ์ˆ˜ ์—†๋‹ค

์ฒ˜์Œ์—๋Š” assert response.status_code == 200 ๋งŒ ์ž‘์„ฑํ–ˆ๊ธฐ ๋•Œ๋ฌธ์— ์‹คํŒจํ–ˆ์„ ๋•Œ ใ€Œ๋ฌด์—‡์ด ๋ฐ˜ํ™˜๋๋Š”์ง€ใ€๋ฅผ ์•Œ ์ˆ˜ ์—†์–ด ์›์ธ ์กฐ์‚ฌ์— ์‹œ๊ฐ„์ด ๊ฑธ๋ ธ์Šต๋‹ˆ๋‹ค.

# โŒ ์‹คํŒจ ์‹œ ์ •๋ณด๊ฐ€ ์ ๋‹ค
assert response.status_code == 200

# โœ… ์—๋Ÿฌ ๋ฉ”์‹œ์ง€๋ฅผ ์ถ”๊ฐ€ํ•ด์„œ ์›์ธ์„ ๋ฐ”๋กœ ํŒŒ์•…
assert response.status_code == 200, f"๊ธฐ๋Œ“๊ฐ’: 200, ์‹ค์ œ ๊ฐ’: {response.status_code}"

๐Ÿ’ก ํฌ์ธํŠธ๏ผšassert ์กฐ๊ฑด, "๋ฉ”์‹œ์ง€" ํ˜•ํƒœ๋กœ ๋ฉ”์‹œ์ง€๋ฅผ ์ถ”๊ฐ€ํ•˜๋ฉด ์‹คํŒจ ์‹œ์— ๊ธฐ๋Œ“๊ฐ’๊ณผ ์‹ค์ œ ๊ฐ’์ด ๋ฌด์—‡์ด์—ˆ๋Š”์ง€๋ฅผ ๋ฐ”๋กœ ์•Œ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.


โ‘ฃ ํ…Œ์ŠคํŠธ ํŒŒ์ผ๋ช…ใƒปํ•จ์ˆ˜๋ช… ๊ทœ์น™์„ ๋ชฐ๋ž๋‹ค

pytest๊ฐ€ ํ…Œ์ŠคํŠธ๋ฅผ ์ž๋™์œผ๋กœ ์ธ์‹ํ•˜์ง€ ๋ชปํ•˜๋Š” ๊ฒฝ์šฐ๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค. ์›์ธ์€ ํŒŒ์ผ๋ช…์ด๋‚˜ ํ•จ์ˆ˜๋ช…์ด pytest์˜ ๋ช…๋ช… ๊ทœ์น™์„ ๋”ฐ๋ฅด์ง€ ์•Š์•˜๊ธฐ ๋•Œ๋ฌธ์ด์—ˆ์Šต๋‹ˆ๋‹ค.

# โŒ pytest๊ฐ€ ์ธ์‹ํ•˜์ง€ ๋ชปํ•จ
# ํŒŒ์ผ๋ช…: api_test.py๏ผˆtest_๋กœ ์‹œ์ž‘ํ•˜์ง€ ์•Š์Œ๏ผ‰
def check_user():  # test_๋กœ ์‹œ์ž‘ํ•˜์ง€ ์•Š์Œ
    assert True

# โœ… pytest๊ฐ€ ์ž๋™์œผ๋กœ ์ธ์‹
# ํŒŒ์ผ๋ช…: test_api.py๏ผˆtest_๋กœ ์‹œ์ž‘๏ผ‰
def test_check_user():  # test_๋กœ ์‹œ์ž‘
    assert True

โš ๏ธ ์ฃผ์˜๏ผšํŒŒ์ผ๋ช…์€ test_ ๋กœ ์‹œ์ž‘ํ•˜๊ฑฐ๋‚˜ _test ๋กœ ๋๋‚˜์•ผ ํ•˜๊ณ , ํ•จ์ˆ˜๋ช…์€ test_ ๋กœ ์‹œ์ž‘ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์ด ๋‘ ๊ฐ€์ง€๊ฐ€ pytest์˜ ๊ธฐ๋ณธ ๊ทœ์น™์ž…๋‹ˆ๋‹ค.


โ‘ค BASE_URL ๋์— ์Šฌ๋ž˜์‹œ๊ฐ€ ์žˆ์–ด URL์ด ์ด์ค‘์œผ๋กœ ๋œ๋‹ค

BASE_URL ๋์— / ๋ฅผ ๋ถ™์ธ ์ƒํƒœ์—์„œ ๊ฒฝ๋กœ๋ฅผ ๊ฒฐํ•ฉํ–ˆ๋”๋‹ˆ URL์ด //users ์ฒ˜๋Ÿผ ์ด์ค‘ ์Šฌ๋ž˜์‹œ๊ฐ€ ๋˜์–ด๋ฒ„๋ ค ์š”์ฒญ์ด ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.

# โŒ ์ด์ค‘ ์Šฌ๋ž˜์‹œ๊ฐ€ ๋˜์–ด๋ฒ„๋ฆผ
BASE_URL = "https://jsonplaceholder.typicode.com/"
response = requests.get(f"{BASE_URL}/users")
# โ†’ https://jsonplaceholder.typicode.com//users๏ผˆNG๏ผ‰

# โœ… BASE_URL ๋์˜ ์Šฌ๋ž˜์‹œ๋ฅผ ์ œ๊ฑฐ
BASE_URL = "https://jsonplaceholder.typicode.com"
response = requests.get(f"{BASE_URL}/users")
# โ†’ https://jsonplaceholder.typicode.com/users๏ผˆOK๏ผ‰

๐Ÿ’ก ํฌ์ธํŠธ๏ผšBASE_URL ๋์—๋Š” / ๋ฅผ ๋ถ™์ด์ง€ ์•Š๊ณ , ๊ฐ ๊ฒฝ๋กœ์˜ ์•ž์— / ๋ฅผ ๋ถ™์ด๋Š” ๋ฐฉ์‹์œผ๋กœ ํ†ต์ผํ•˜๋ฉด ํ˜ผ๋ž€์ด ์—†์Šต๋‹ˆ๋‹ค.


07. ์ž์ฃผ ๋ฌป๋Š” ์งˆ๋ฌธ๏ผˆFAQ๏ผ‰

Q. pytest๋กœ API ํ…Œ์ŠคํŠธ๋ฅผ ํ•˜๋Š” ์žฅ์ ์€ ๋ฌด์—‡์ธ๊ฐ€์š”๏ผŸ
A. Python์œผ๋กœ ์ง๊ด€์ ์œผ๋กœ ํ…Œ์ŠคํŠธ๋ฅผ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ๊ณ  CI/CD์™€์˜ ํ†ตํ•ฉ์ด ์‰ฌ์šด ๊ฒƒ์ด ๊ฐ€์žฅ ํฐ ์žฅ์ ์ž…๋‹ˆ๋‹ค. ๋˜ํ•œ pytest-html ๋กœ HTML ๋ฆฌํฌํŠธ๋ฅผ ์ž๋™ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ์–ด ํ…Œ์ŠคํŠธ ์ฆ๊ฑฐ ์ž‘์„ฑ ๊ณต์ˆ˜๋ฅผ ๋Œ€ํญ ์ค„์ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

Q. requests์™€ httpx ์–ด๋А ์ชฝ์ด ์ข‹์€๊ฐ€์š”๏ผŸ
A. ์šฉ๋„์— ๋”ฐ๋ผ ๊ตฌ๋ถ„ํ•˜๋Š” ๊ฒƒ์ด ์ตœ์„ ์ž…๋‹ˆ๋‹ค. ๊ฐ„๋‹จํ•œ ๋™๊ธฐ ์ฒ˜๋ฆฌ๋ผ๋ฉด requests, ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ๏ผˆasync/await๏ผ‰๊ฐ€ ํ•„์š”ํ•˜๋‹ค๋ฉด httpx ๋ฅผ ์„ ํƒํ•˜์„ธ์š”. API ํ…Œ์ŠคํŠธ ์ž…๋ฌธ์—๋Š” requests ๊ฐ€ ๋” ๊ฐ„๋‹จํ•˜์—ฌ ์ถ”์ฒœํ•ฉ๋‹ˆ๋‹ค.

Q. UI ํ…Œ์ŠคํŠธ์™€ API ํ…Œ์ŠคํŠธ ์ค‘ ์–ด๋А ๊ฒƒ๋ถ€ํ„ฐ ์‹œ์ž‘ํ•ด์•ผ ํ•˜๋‚˜์š”๏ผŸ
A. API ํ…Œ์ŠคํŠธ๋ถ€ํ„ฐ ์‹œ์ž‘ํ•˜๋Š” ๊ฒƒ์„ ์ถ”์ฒœํ•ฉ๋‹ˆ๋‹ค. ์‹คํ–‰ ์†๋„๊ฐ€ ๋น ๋ฅด๊ณ  ํ™˜๊ฒฝ ์„ค์ •๋„ ๊ฐ„๋‹จํ•˜๊ธฐ ๋•Œ๋ฌธ์— ํ…Œ์ŠคํŠธ ์ž๋™ํ™”์˜ ๊ธฐ์ดˆ๋ฅผ ๋ฐฐ์šฐ๊ธฐ์— ์ตœ์ ์ž…๋‹ˆ๋‹ค. UI ํ…Œ์ŠคํŠธ์— ์ต์ˆ™ํ•œ ๋ถ„๋„ API ํ…Œ์ŠคํŠธ๋ฅผ ์ถ”๊ฐ€ํ•จ์œผ๋กœ์จ ใ€Œ๋‘˜ ๋‹ค ํ•  ์ˆ˜ ์žˆ๋Š” ์—”์ง€๋‹ˆ์–ดใ€๋กœ์„œ ์–ดํ•„ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

Q. ํ…Œ์ŠคํŠธ ๋Œ€์ƒ API๊ฐ€ ์—†๋Š” ๊ฒฝ์šฐ๋Š” ์–ด๋–ป๊ฒŒ ํ•˜๋ฉด ๋˜๋‚˜์š”๏ผŸ
A. ์—ฐ์Šต์šฉ ๊ณต๊ฐœ API๋ฅผ ์‚ฌ์šฉํ•ฉ์‹œ๋‹ค. JSONPlaceholder๏ผˆ์‚ฌ์šฉ์žใƒป๊ฒŒ์‹œ๋ฌผ CRUD๏ผ‰๋‚˜ Restful Booker๏ผˆ์ธ์ฆ ํ”Œ๋กœ์šฐ๏ผ‰๊ฐ€ ํฌํŠธํด๋ฆฌ์˜ค์šฉ API ํ…Œ์ŠคํŠธ์— ์ตœ์ ์ž…๋‹ˆ๋‹ค. ๋‘˜ ๋‹ค ๋ฌด๋ฃŒ๋กœ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

Q. pytest์˜ fixture๋ž€ ๋ฌด์—‡์ธ๊ฐ€์š”๏ผŸ
A. ํ…Œ์ŠคํŠธ์˜ ์‚ฌ์ „ใƒป์‚ฌํ›„ ์ฒ˜๋ฆฌ๋ฅผ ๊ณตํ†ตํ™”ํ•˜๋Š” ๊ตฌ์กฐ์ž…๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด ์ธ์ฆ ํ† ํฐ ์ทจ๋“์„ fixture์— ์ž‘์„ฑํ•ด๋‘๋ฉด ์—ฌ๋Ÿฌ ํ…Œ์ŠคํŠธ ์ผ€์ด์Šค์—์„œ ์žฌ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ฝ”๋“œ์˜ ์ค‘๋ณต์„ ์ค„์ด๊ณ  ์œ ์ง€๋ณด์ˆ˜ํ•˜๊ธฐ ์‰ฌ์šด ๊ตฌ์กฐ๋กœ ๋งŒ๋“ค๊ธฐ ์œ„ํ•œ ์ค‘์š”ํ•œ ๊ธฐ๋Šฅ์ž…๋‹ˆ๋‹ค.


08. ์ •๋ฆฌ

์ด ๊ธ€์—์„œ๋Š” API ํ…Œ์ŠคํŠธ์˜ ๊ธฐ๋ณธ ๊ฐœ๋…๊ณผ pytest์™€ requests์˜ ์‚ฌ์šฉ๋ฒ•์„ ํ•ด์„คํ–ˆ์Šต๋‹ˆ๋‹ค.

ํฌ์ธํŠธ ๋‚ด์šฉ
API ํ…Œ์ŠคํŠธ๋ž€ ๋ธŒ๋ผ์šฐ์ € ์—†์ด HTTP ์š”์ฒญ์„ ๋ณด๋‚ด๊ณ  ์‘๋‹ต์„ ๊ฒ€์ฆํ•˜๋Š” ํ…Œ์ŠคํŠธ
requests์˜ ์—ญํ•  API์— HTTP ์š”์ฒญ ์ „์†กใƒป์‘๋‹ต ์ทจ๋“
pytest์˜ ์—ญํ•  ํ…Œ์ŠคํŠธ ์‹คํ–‰ใƒป๊ด€๋ฆฌใƒปHTML ๋ฆฌํฌํŠธ ์ƒ์„ฑ
์กฐํ•ฉ์˜ ๊ฐ•์  ๊ฐ„๋‹จํ•œ ์ฝ”๋“œ๋กœ ์‹ค๋ฌด ์ˆ˜์ค€์˜ API ํ…Œ์ŠคํŠธ๋ฅผ ์‹คํ˜„ํ•  ์ˆ˜ ์žˆ๋‹ค

UI ํ…Œ์ŠคํŠธ ๊ฒฝํ—˜๋ฐ–์— ์—†๋Š” ๋ถ„๋„, requests์™€ pytest ๋‘ ๊ฐ€์ง€๋งŒ ์ตํžˆ๋ฉด API ํ…Œ์ŠคํŠธ๋ฅผ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋ฉ๋‹ˆ๋‹ค. GitHub์— ์ฝ”๋“œ๋ฅผ ๊ณต๊ฐœํ•จ์œผ๋กœ์จ ใ€ŒUI๋„ API๋„ ๋‘˜ ๋‹ค ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค๏ผใ€๋ผ๋Š” ์–ดํ•„์ด ๋ฉ๋‹ˆ๋‹ค. ๋‹ค์Œ ๊ธ€์—์„œ๋Š” ์‹ค์ œ API ํ…Œ์ŠคํŠธ ์ผ€์ด์Šค๋ฅผ ์ž์„ธํžˆ ํ•ด์„คํ•ฉ๋‹ˆ๋‹ค.

ใ‚ฟใ‚คใƒˆใƒซใจURLใ‚’ใ‚ณใƒ”ใƒผใ—ใพใ—ใŸ