목차
- 파이썬 API 테스트 입문 - 파이썬 기초
- 파이썬 API 테스트 입문 - API 테스트 빠른 시작
- 파이썬 API 테스트 입문 - API 보안 검증, 매개변수화 및 검증
- 파이썬 API 테스트 입문 - API 테스트 프레임워크 구현
API 테스트 프레임워크
프레임워크란 무엇인가
현재 주류 API 테스트 솔루션
- 도구 중심 접근법
- Java 중심 접근법
- Python 중심 접근법
- API 플랫폼 접근법
프레임워크 유형
- 기록 및 재생 방식
- 데이터 기반 테스트
- 동작 기반 테스트
프레임워크 계층 및 설계
프레임워크 계층
- 표현 계층: (사용자 인터페이스)
- 비즈니스 로직 계층: (데이터 읽기, 구성 로드 및 요청 조립) + 실행 제어 계층(pytest)
- 데이터 계층: (구성 읽기/데이터 읽기/데이터베이스 연결/기타(log/email)
프레임워크 설계
- tests: 테스트 케이스 디렉토리
- users: (사용자 모듈)
- test_user.py: 테스트 케이스
- base.py: 케이스 공통 메서드
- users: (사용자 모듈)
- resources: 데이터 파일 디렉토리
- user_test_data.xlsx: 테스트 케이스 데이터 파일
- settings: 설정 파일 디렉토리
- default.conf: 기본 설정 파일
- results: pytest 생성 보고서 저장 경로
- logs: log 저장 경로, 일별 log 생성
- utils: 공통 메서드 디렉토리
- config_handler.py: 설정 파일 읽기
- data_reader.py: 데이터 파일 읽기
- database.py: 데이터베이스 연결
- logger.py: 로그 설정
- notification.py: 이메일 발송 설정
프레임워크 구현
settings/default.conf: 표현 계층 - 프로젝트 설정 파일
[execution] log_level=debug results_dir=results logs_dir=logs timeout=10 [server] test = http://127.0.0.1:5000 staging = http://127.0.0.1:6000 production = http://127.0.0.1:7000 [database_test] host = localhost port = 3307 db = api_test user = root password = [database_staging] [database_production] [notification] server = smtp.gmail.com user = test_results@gmail.com password = ****** subject = API Test Report recipient = tester@example.com
resources/user_test_data.xlsx: 표현 계층 - 케이스 데이터 파일
등록 테이블 (시트 이름: registration)
| 테스트케이스 | URL | 메소드 | 데이터타입 | 데이터 | 상태코드 | 메시지 |
|---|---|---|---|---|---|---|
| test_reg_normal | /api/users/register/ | POST | JSON | {"username": "{USERNAME}", "password": "123456"} | 200 | 성공 |
로그인 테이블 (시트 이름: login)
| 테스트케이스 | URL | 메소드 | 데이터타입 | 데이터 | 응답텍스트 |
|---|---|---|---|---|---|
| test_login_normal | /api/users/login/ | POST | FORM | 로그인 성공 |
SQL 테이블 (시트 이름: queries)
| 쿼리명 | SQL문 |
|---|---|
| checkUser | SELECT * FROM users WHERE username= |
| checkUserPassword | SELECT * FROM users WHERE username={USERNAME} AND password= |
utils/config_handler.py: 데이터 계층 - 설정 파일 읽기
"""
1. 설정 파일에서 각 섹션 정보 가져오기
2. 프로젝트의 절대 경로 반환
"""
import os
import configparser
# 상대 패키지 임포트 문제 해결
project_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
class ConfigHandler:
def __init__(self, filename="default.conf"):
self.config = configparser.ConfigParser()
self.config.read(os.path.join(project_path, "settings", filename))
def get_execution_config(self, option):
return self.config.get("execution", option)
def get_server_url(self, environment):
return self.config.get("server", environment)
def get_test_db_config(self, option):
return self.config.get("database_test", option)
def get_notification_config(self, option):
return self.config.get("notification", option)
if __name__ == "__main__":
config = ConfigHandler()
print(config.get_execution_config("log_level"))
print(config.get_server_url("test"))
utils/data_reader.py: 데이터 계층 - 데이터 파일 읽기
"""
1. Excel에서 API 테스트 데이터 읽기
2. SQL 명령어 읽기
"""
import xlrd
import sys
import os
sys.path.append("..")
from utils.config_handler import project_path
class TestDataLoader:
def __init__(self, filename):
data_file_path = os.path.join(project_path, "resources", filename)
self.workbook = xlrd.open_workbook(data_file_path)
def get_test_case(self, sheet_name, case_name):
worksheet = self.workbook.sheet_by_name(sheet_name)
for i in range(1, worksheet.nrows):
if worksheet.cell(i, 0).value == case_name:
return worksheet.row_values(i)
print("테스트 케이스를 찾을 수 없습니다")
return None
def get_query(self, query_name):
worksheet = self.workbook.sheet_by_name("queries")
for i in range(worksheet.nrows):
if worksheet.cell(i, 0).value == query_name:
return worksheet.cell(i, 1).value
print("쿼리를 찾을 수 없습니다")
return None
if __name__ == "__main__":
data = TestDataLoader("user_test_data.xlsx")
print(data.get_test_case("registration", "test_reg_normal"))
print(data.get_query("checkUser"))
utils/database.py: 데이터 계층 - 데이터베이스 연결
"""
1. 설정 파일에서 데이터베이스 구성 읽기
2. 데이터베이스 연결
3. SQL 실행 및 결과 반환
"""
import sys
import pymysql
sys.path.append("..")
from utils.config_handler import ConfigHandler
class DatabaseConnector:
def __init__(self):
config = ConfigHandler()
self.connection = pymysql.connect(
host=config.get_test_db_config("host"),
port=int(config.get_test_db_config("port")),
db=config.get_test_db_config("db"),
user=config.get_test_db_config("user"),
password=config.get_test_db_config("password"),
charset="utf8"
)
self.cursor = self.connection.cursor()
def execute_query(self, sql):
self.cursor.execute(sql)
return self.cursor.fetchall()
def __del__(self):
self.cursor.close()
self.connection.close()
if __name__ == "__main__":
db = DatabaseConnector()
print(db.execute_query("SELECT * FROM users"))
utils/logger.py: 데이터 계층 - 로그 설정
"""
1. log 출력 형식 time - loglevel - file - func - line - msg 설정
2. log 파일 및 화면 출력 지원
3. 다른 모듈에서 호출할 수 있는 logger 반환
"""
import sys
sys.path.append("..")
from utils.config_handler import ConfigHandler, project_path
import time
import logging
import os
class LoggerSetup:
@classmethod
def configure_logging(cls):
config = ConfigHandler()
log_dir = os.path.join(project_path, config.get_execution_config("logs_dir"))
today = time.strftime("%Y%m%d", time.localtime(time.time()))
log_file = os.path.join(log_dir, today + ".log")
# 표준 logger 가져오기 및 loglevel 설정
cls.logger = logging.getLogger()
cls.logger.setLevel(eval("logging." + config.get_execution_config("log_level").upper()))
# 다양한 핸들러 생성
file_handler = logging.FileHandler(log_file, mode="a", encoding='utf-8')
console_handler = logging.StreamHandler()
# 출력 형식 정의
formatter = logging.Formatter("%(asctime)s - %(filename)s[line:%(lineno)d] - %(levelname)s: %(message)s")
file_handler.setFormatter(formatter)
console_handler.setFormatter(formatter)
# 커스텀 핸들러를 logger에 추가
cls.logger.addHandler(file_handler)
cls.logger.addHandler(console_handler)
@classmethod
def get_logger(cls):
cls.configure_logging()
return cls.logger
if __name__ == "__main__":
logger = LoggerSetup.get_logger()
logger.info("테스트 메시지")
logger.debug("디버그 정보")
utils/notification.py: 데이터 계층 - 이메일 서버 연결
"""
1. 설정 파일에서 SMTP 구성 읽기
2. results 폴더에서 report.html 열기 및 이메일 발송
"""
import smtplib
from email.mime.text import MIMEText
import os
import sys
sys.path.append("..")
from utils.config_handler import ConfigHandler, project_path
from utils.logger_setup import LoggerSetup
def send_test_report(report_name):
config = ConfigHandler()
logger = LoggerSetup.get_logger()
report_file = os.path.join(project_path, config.get_execution_config("results_dir"), report_name)
with open(report_file, "rb") as f:
body = f.read()
# 이메일 본문 형식화
message = MIMEText(body, "html", "utf-8")
# 이메일 헤더 구성
message["Subject"] = config.get_notification_config("subject")
message["From"] = config.get_notification_config("user")
message["To"] = config.get_notification_config("recipient")
# SMTP 서버 연결 및 이메일 발송
smtp_server = smtplib.SMTP()
smtp_server.connect(config.get_notification_config("server"))
smtp_server.login(config.get_notification_config("user"), config.get_notification_config("password"))
smtp_server.sendmail(config.get_notification_config("user"), config.get_notification_config("recipient"), message.as_string())
print("이메일이 성공적으로 발송되었습니다")
if __name__ == "__main__":
send_test_report("report.html")
tests/base.py: 비즈니스 로직 계층 - 테스트 케이스 실행을 위한 메서드 캡슐화
"""
1. 데이터 로딩
2. API 요청 전송
3. 테스트 케이스를 위한 메서드 캡슐화
"""
import sys
sys.path.append("..")
from utils.logger_setup import LoggerSetup
from utils.config_handler import ConfigHandler
from utils.database_connector import DatabaseConnector
from utils.data_loader import TestDataLoader
import json
import requests
class TestCaseExecutor:
def __init__(self):
self.logger = LoggerSetup.get_logger()
self.config = ConfigHandler()
def load_test_data(self, data_file):
self.data = TestDataLoader(data_file)
def set_environment(self, env):
self.environment = env
def run_test_case(self, sheet_name, case_name, variables={}):
case_data = self.data.get_test_case(sheet_name, case_name)
url = self.config.get_server_url(self.environment) + case_data[1]
data = case_data[4].format(**variables)
if case_data[3].lower() == "form":
data = json.loads(data)
headers = {}
else:
headers = {"content-type": "application/json"}
if case_data[2].lower() == "get":
response = requests.get(url=url)
else:
response = requests.post(url=url, headers=headers, data=data)
return response.text
def verify_response(self):
pass
def verify_database(self, query_name, variables={}):
query = self.data.get_query(query_name).format(**variables)
db = DatabaseConnector()
return db.execute_query(query)
if __name__ == "__main__":
executor = TestCaseExecutor()
executor.set_environment("test")
executor.load_test_data("user_test_data.xlsx")
result = executor.run_test_case("login", "test_login_normal")
print(result)
tests/users/test_user.py: 표현 계층: 테스트 케이스 스크립트
import sys
import random
import pytest
sys.path.append("../..")
from tests.base import TestCaseExecutor
test_executor = TestCaseExecutor()
def setup_module(module):
test_executor.set_environment('dev')
test_executor.load_test_data('user_test_data.xlsx')
def test_login_normal():
result = test_executor.run_test_case("login", "test_login_normal")
if __name__ == '__main__':
pytest.main(["-q", "test_user.py"])
run_tests.py: 표현 계층: 모든 테스트 케이스 실행 진입점
import os
import time
from utils.config_handler import ConfigHandler
from utils.notification import send_test_report
import pytest
def execute_all_tests():
config = ConfigHandler()
results_dir = config.get_execution_config("results_dir")
timestamp = time.strftime('%Y%m%d%H%M%S', time.localtime(time.time()))
report_name = os.path.join(results_dir, 'test_report_' + timestamp + '.html')
pytest.main(["-q", "tests", "--html=" + report_name])
send_test_report(report_name)
if __name__ == '__main__':
execute_all_tests()