Python 기반 Selenium 및 Unittest를 활용한 POM 패턴 자동화 테스트 프레임워크 구축

프레임워크 아키텍처 및 디렉토리 구조

UI 자동화 테스트의 유지보수성과 재사용성을 높이기 위해서는 체계적인 프레임워크 구축이 필수적입니다. 본 가이드에서는 Page Object Model(POM) 디자인 패턴을 적용하여 Selenium과 Unittest 기반의 자동화 테스트 프레임워크를 설계하는 방법을 다룹니다.

프로젝트의 표준 디렉토리 구조는 다음과 같이 구성합니다.

디렉토리 / 파일 설명 Python 패키지 여부
common공통 유틸리티 클래스 (설정 파일 읽기, 요소 파싱 등)
config전역 설정 및 환경 변수 파일
logs실행 로그 저장 디렉토리아니오
pageSelenium Base Page 클래스
page_elementUI 요소Locator 데이터 (YAML)아니오
page_objectPOM 패턴 기반 페이지 객체 클래스
report테스트 결과 보고서 저장 디렉토리아니오
TestCaseUnittest 테스트 스위트
utils로거, 시간, 이메일 등 보조 도구
run_tests.py테스트 실행 진입점 (Entry Point)아니오

Python 패키지(__init__.py가 필요한 디렉토리)로 지정된 곳은 반드시 빈 __init__.py 파일을 포함해야 합니다.

시간 및 유틸리티 모듈 구현

테스트 실행 시간 측정 및 포맷팅을 위해 utils/time_utils.py 파일을 생성합니다.


import time
import datetime
from functools import wraps

def get_unix_time():
    """현재 유닉스 타임스탬프 반환"""
    return time.time()

def get_formatted_date(fmt="%Y%m%d_%H%M%S"):
    """포맷팅된 현재 날짜 문자열 반환"""
    return datetime.datetime.now().strftime(fmt)

def pause_execution(seconds=1.0):
    """지정된 시간만큼 실행 일시 정지"""
    time.sleep(seconds)

def measure_execution_time(func):
    """함수 실행 시간을 측정하는 데코레이터"""
    @wraps(func)
    def wrapper(*args, **kwargs):
        start = get_unix_time()
        result = func(*args, **kwargs)
        elapsed = get_unix_time() - start
        print(f"[Execution Time] {func.__name__} completed in {elapsed:.3f} seconds.")
        return result
    return wrapper

전역 설정 및 환경 변수 관리

프로젝트의 경로 및 기본 설정을 관리하는 config/settings.py를 작성합니다.


import os
from selenium.webdriver.common.by import By
from utils.time_utils import get_formatted_date

class ProjectSettings:
    BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
    
    INI_FILE_PATH = os.path.join(BASE_DIR, 'config', 'env_config.ini')
    ELEMENTS_DIR = os.path.join(BASE_DIR, 'page_element')
    REPORTS_DIR = os.path.join(BASE_DIR, 'report')
    TEST_SUITES_DIR = os.path.join(BASE_DIR, "TestCase")
    
    LOCATOR_STRATEGY = {
        'css': By.CSS_SELECTOR,
        'xpath': By.XPATH,
        'id': By.ID,
        'name': By.NAME,
        'class': By.CLASS_NAME
    }

    SMTP_CONFIG = {
        'sender': 'test_sender@example.com',
        'auth_code': 'YOUR_SMTP_AUTH_CODE',
        'host': 'smtp.example.com',
        'port': 465
    }

    RECEIVERS = ['admin@example.com']

    @property
    def log_file_path(self):
        log_dir = os.path.join(self.BASE_DIR, 'logs')
        os.makedirs(log_dir, exist_ok=True)
        return os.path.join(log_dir, f"test_{get_formatted_date('%Y%m%d')}.log")

    @property
    def html_report_path(self):
        os.makedirs(self.REPORTS_DIR, exist_ok=True)
        return os.path.join(self.REPORTS_DIR, f"report_{get_formatted_date()}.html")

    @property
    def latest_report_content(self):
        files = sorted(os.listdir(self.REPORTS_DIR), key=lambda x: os.path.getmtime(os.path.join(self.REPORTS_DIR, x)))
        if not files:
            return "<p>No reports found.</p>"
        with open(os.path.join(self.REPORTS_DIR, files[-1]), 'r', encoding='utf-8') as f:
            return f.read()

env = ProjectSettings()

이어서 실제 테스트 대상 URL 등을 저장할 config/env_config.ini 파일을 생성합니다.


[TARGET_ENV]
BASE_URL = https://www.example.com

설정 파일 파서 구현

INI 파일을 읽고 파싱하는 common/config_parser.py 모듈을 작성합니다.


import os
import configparser
from config.settings import env

class IniConfigReader:
    def __init__(self):
        self.file_path = env.INI_FILE_PATH
        if not os.path.exists(self.file_path):
            raise FileNotFoundError(f"Configuration file not found at {self.file_path}")
        
        self.parser = configparser.RawConfigParser()
        self.parser.read(self.file_path, encoding='utf-8')

    def _fetch_value(self, section, key):
        return self.parser.get(section, key)

    @property
    def target_url(self):
        return self._fetch_value('TARGET_ENV', 'BASE_URL')

config_data = IniConfigReader()

로깅 시스템 구축

테스트 실행过程中的 디버깅과 추적을 위한 utils/log_manager.py를 설정합니다.


import logging
from config.settings import env

class TestLogger:
    def __init__(self, logger_name=None):
        self._logger = logging.getLogger(logger_name)
        if not self._logger.handlers:
            self._logger.setLevel(logging.DEBUG)
            
            file_handler = logging.FileHandler(env.log_file_path, encoding='utf-8')
            file_handler.setLevel(logging.INFO)
            
            console_handler = logging.StreamHandler()
            console_handler.setLevel(logging.INFO)
            
            log_format = logging.Formatter('%(asctime)s | %(levelname)-8s | %(name)s:%(lineno)d | %(message)s')
            file_handler.setFormatter(log_format)
            console_handler.setFormatter(log_format)
            
            self._logger.addHandler(file_handler)
            self._logger.addHandler(console_handler)

    @property
    def instance(self):
        return self._logger

POM 패턴과 요소 데이터 관리

Page Object Model(POM)은 UI 변경에 대한 테스트 코드의 영향을 최소화하고 재사용성을 극대화하는 디자인 패턴입니다. 요소Locator는 코드와 분리하여 page_element/search_elements.yaml과 같은 YAML 파일로 관리합니다.


search_input_field: "id==search_query"
search_submit_btn: "css==button.submit-btn"
suggestions_list: "xpath==//ul[@class='suggestions']/li"

YAML 파일을 동적으로 로드하는 common/yaml_element_reader.py를 구현합니다.


import os
import yaml
from config.settings import env

class YamlElementLoader:
    def __init__(self, page_name):
        self.yaml_file = f"{page_name}.yaml"
        self.full_path = os.path.join(env.ELEMENTS_DIR, self.yaml_file)
        if not os.path.exists(self.full_path):
            raise FileNotFoundError(f"Element file {self.yaml_file} does not exist.")
        
        with open(self.full_path, encoding='utf-8') as f:
            self._elements = yaml.safe_load(f)

    def __getitem__(self, element_key):
        raw_locator = self._elements.get(element_key)
        if not raw_locator:
            raise KeyError(f"Element '{element_key}' not found in {self.yaml_file}")
        
        strategy, value = raw_locator.split('==')
        return strategy.strip(), value.strip()

page_elements = YamlElementLoader('search_elements')

Selenium Base Page 캡슐화

Selenium의 기본 메서드를 래핑하여 명시적 대기(Explicit Wait)와 예외 처리를 적용한 page/base_page.py를 작성합니다.


from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.common.exceptions import TimeoutException
from config.settings import env
from utils.time_utils import pause_execution
from utils.log_manager import TestLogger

log = TestLogger(__name__).instance

class SeleniumBasePage:
    def __init__(self, driver):
        self.driver = driver
        self.wait = WebDriverWait(self.driver, 15)

    def navigate_to(self, url):
        self.driver.maximize_window()
        try:
            self.driver.get(url)
            log.info(f"Navigated to {url}")
        except TimeoutException:
            raise TimeoutException(f"Failed to load {url} within the timeout limit.")

    def _locate(self, find_method, locator_tuple):
        strategy, value = locator_tuple
        by_type = env.LOCATOR_STRATEGY.get(strategy)
        if not by_type:
            raise ValueError(f"Invalid locator strategy: {strategy}")
        return find_method(by_type, value)

    def find_single_element(self, locator_tuple):
        return self._locate(
            lambda by, val: self.wait.until(EC.presence_of_element_located((by, val))), 
            locator_tuple
        )

    def find_multiple_elements(self, locator_tuple):
        return self._locate(
            lambda by, val: self.wait.until(EC.presence_of_all_elements_located((by, val))), 
            locator_tuple
        )

    def enter_text(self, locator_tuple, text):
        pause_execution(0.5)
        element = self.find_single_element(locator_tuple)
        element.clear()
        element.send_keys(text)
        log.info(f"Entered text: '{text}'")

    def perform_click(self, locator_tuple):
        self.find_single_element(locator_tuple).click()
        pause_execution()
        log.info(f"Clicked element: {locator_tuple}")

    def extract_text(self, locator_tuple):
        text = self.find_single_element(locator_tuple).text
        log.info(f"Extracted text: '{text}'")
        return text

    @property
    def page_html_source(self):
        return self.driver.page_source

페이지 객체(Page Object) 생성

Base Page를 상속받아 특정 페이지의 비즈니스 로직을 캡슐화한 page_object/search_page_object.py를 작성합니다.


from page.base_page import SeleniumBasePage
from common.yaml_element_reader import page_elements
from utils.time_utils import pause_execution

class SearchPageActions(SeleniumBasePage):
    
    def input_search_keyword(self, keyword):
        self.enter_text(page_elements['search_input_field'], keyword)
        pause_execution()

    def submit_search(self):
        self.perform_click(page_elements['search_submit_btn'])

    @property
    def get_search_suggestions(self):
        elements = self.find_multiple_elements(page_elements['suggestions_list'])
        return [elem.text for elem in elements]

Unittest 테스트 스위트 작성

TestCase/test_search_functionality.py에서 실제 테스트 시나리오를 정의합니다.


import unittest
import re
from selenium import webdriver
from common.config_parser import config_data
from page_object.search_page_object import SearchPageActions
from utils.log_manager import TestLogger

log = TestLogger(__name__).instance

class TestSearchFeature(unittest.TestCase):

    @classmethod
    def setUpClass(cls):
        cls.driver = webdriver.Chrome()
        cls.search_page = SearchPageActions(cls.driver)
        cls.search_page.navigate_to(config_data.target_url)

    @classmethod
    def tearDownClass(cls):
        cls.driver.quit()

    def test_01_verify_search_results(self):
        """검색 결과 페이지에 키워드가 포함되는지 확인"""
        keyword = "automation"
        self.search_page.input_search_keyword(keyword)
        self.search_page.submit_search()
        
        source = self.search_page.page_html_source
        match = re.search(keyword, source, re.IGNORECASE)
        log.info(f"Search result match: {match}")
        self.assertIsNotNone(match)

    def test_02_verify_suggestions_contain_keyword(self):
        """검색 제안 목록에 키워드가 포함되는지 확인"""
        keyword = "selenium"
        self.search_page.input_search_keyword(keyword)
        
        suggestions = self.search_page.get_search_suggestions
        log.info(f"Fetched suggestions: {suggestions}")
        self.assertTrue(all(keyword in s.lower() for s in suggestions))

if __name__ == '__main__':
    unittest.main(verbosity=2)

테스트 실행 및 HTML 보고서 생성

프로젝트 루트에 run_tests.py를 생성하여 테스트를 실행하고 HTML 보고서를 생성합니다. 외부 라이브러리인 HTMLTestRunner를 활용합니다.


import unittest
from config.settings import env
from utils.email_notifier import dispatch_test_report

# HTMLTestRunner는 별도 설치가 필요합니다.
from HTMLTestRunner import HTMLTestRunner 

def execute_suite():
    test_loader = unittest.defaultTestLoader.discover(env.TEST_SUITES_DIR, pattern="test*.py")
    
    try:
        with open(env.html_report_path, 'wb') as report_file:
            runner = HTMLTestRunner(
                stream=report_file,
                title="UI Automation Test Report",
                description="Execution results of the POM-based test suite.",
                verbosity=2
            )
            result = runner.run(test_loader)
    except Exception as e:
        print(f"Test execution failed: {e}")
    else:
        if result.failure_count > 0 or result.error_count > 0:
            dispatch_test_report()

if __name__ == "__main__":
    execute_suite()

테스트 결과 이메일 알림

테스트 실패 시 생성된 HTML 보고서를 이메일로 발송하는 utils/email_notifier.py 모듈을 구현합니다.


import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from email.header import Header
from config.settings import env

def dispatch_test_report():
    smtp_conf = env.SMTP_CONFIG
    sender = smtp_conf['sender']
    password = smtp_conf['auth_code']
    receivers = env.RECEIVERS
    
    message = MIMEMultipart()
    message['From'] = Header("Automation Bot", 'utf-8')
    message['To'] = Header("QA Team", 'utf-8')
    message['Subject'] = Header("Latest UI Test Execution Report", 'utf-8')
    
    html_content = env.latest_report_content
    message.attach(MIMEText(html_content, 'html', 'utf-8'))
    
    try:
        server = smtplib.SMTP_SSL(smtp_conf['host'], smtp_conf['port'])
        server.login(sender, password)
        server.sendmail(sender, receivers, message.as_string())
        server.quit()
        print("Test report email dispatched successfully.")
    except smtplib.SMTPException as err:
        print(f"Failed to send email: {err}")

태그: selenium unittest python page-object-model automation-testing

8월 6일 07:10에 게시됨