본문 바로가기

QA

Python Appium Android 테스트 하기 1

모바일 자동화 테스트 구조 설계하기

여담

저는 현재 신입으로 아 Q A들어갔어요. 이제 입사한 지 3주 차네요. 여기서 하는 업무는 자동화 스크립트를 짜는 줄 알았는데 그렇게 말해서 엑셀로 짜고 있네요. 이미 다른 분이 와서 구축을 해놨어요.
다만 코드도 한 파일에 몰려있고 엑셀로 스크립트를 짜는 구조인데 여간 불편한 게 아닙니다.

엑셀 기반 자동화의 장단점

장점

  • 코딩을 모르는 사람도 쉽게 할 수 있다는 점 (최대 장점)

단점

  • 엑셀로 스크립트를 실행하기 때문에 행과 열이 있죠. 이 부분이 하나라도 틀리면 오류가 난다는 점
  • 어디서 에러가 났는지 파악이 어려운 점
  • 하나의 엑셀 파일로 실행하기 때문에 결국 나중에 많은 스크립트를 실행할 때 시간이 더 걸린다는 점
  • sleep을 설정해줘야 하는데, 하나의 행동에 5초라는 시간만 주어도 10개면 50초, 100개면 500초로 무궁무진하게 오래 걸립니다
  • 코드로 치면 더 빠르지만 엑셀 기반은 코딩을 아는 사람이라면 더 느립니다
  • 실행 후 많은 시간을 기다려야 하기 때문에 어떤 특정 부분에서 오류가 났는지, 그리고 고치고 나서 오류가 안 나나 확인하는 시간이 생각보다 오래 걸려요

이게 여담보다는 푸념이네요 ㅋㅋ


프로젝트 폴더 구조

먼저 프로젝트 구조를 어떻게 설계하면 좋을지 보여드릴게요.

project/
├── config/
│   └── caps.json              # 공통 Appium 설정
├── utils/
│   └── allure_helper.py       # Allure 헬퍼 함수
├── tests/
│   ├── test_login.py          # 로그인 테스트
│   └── test_home.py           # 홈 테스트
├── local.ini                  # 개인 환경 설정 (gitignore)
├── .gitignore
├── conftest.py                # pytest 전역 설정
└── config_loader.py           # 환경 설정 로더

PO 패턴 (Page Object Pattern)

일단 중요한 것은 자동화 스크립트를 짜더라도 "어떤 식으로 어떻게 해야 구조적으로 좋고 유지보수와 가독성이 좋을까?"라고 생각한다면 PO 패턴이 좋을 것 같아요.

PO 패턴의 장점

  • 가독성과 유지보수성에 아주 좋아요
  • 우리가 TC를 각각의 페이지별로 작성하니까 (로그인, 회원가입 등) 이 패턴을 사용하면 로그인, 홈 등을 나누어서 코드를 작성하고 테스트할 수 있기 때문에 조금 더 편하게 관리를 할 수 있죠
  • 공통적으로 사용하는 이벤트들 (예: 클릭, 스와이프, 스크롤, 클릭 후 입력 등)을 하나의 최상위 클래스에서 정의하면 다른 곳에서 가져다 쓰기만 하면 돼요
  • 이렇게 관리한다면 우리는 중복된 코드를 줄일 수 있고 공통적으로 사용해야 하는 기능들을 하나의 파일에서 관리하니까 유지보수, 추가도 더 쉽죠

환경 파일 설정하기

처음에 우리가 공통적으로 사용하고 절대 변하지 않는 옵션들이 있어요. 이를 하나의 폴더에 JSON 파일로 생성하여 여기에 우리가 다른 사람들이 clone을 받아도 이것은 무조건 있어야 해 이것들을 여기서 정의해 놓으면 좋습니다.
이렇게 JSON 파일로 설정을 해 놓으면 좋아요.

config/caps.json

{
  "platformName": "Android",
  "automationName": "UiAutomator2",
  "appPackage": "com.tms",
  "appActivity": "com.tms.activity.home.MPHomeActivity",
  "unicodeKeyboard": true,
  "resetKeyboard": true,
  "noReset": true,
  "ignoreUnimportantViews": true,
  "disableIdLocatorAutocompletion": true,
  "autoGrantPermissions": true
}

local.ini (개인 환경 설정)

이제 다음 할 것은 우리가 코드를 GitHub에서 clone을 한다고 가정한다면, 또는 다르게 파일을 받는다면 우리만의 환경이 있을 거예요.

이를 이제 root 파일에 .ini라는 파일로 생성해주세요. 여기에는 우리만의 설정이 들어가야 해요. 즉, 우리의 핸드폰을 연결한다면 UDID, 또는 Android Studio를 쓴다면 에뮬레이터 이름을 정의해주세요.

주의점:

  • [LOCAL]이라고 적어주기
  • 소문자로 하는 것을 추천 (이따가 환경을 설정해주기 위해서 중요해요)
  • 이렇게 다 설정을 하셨다면 .gitignore라는 파일에 여러분이 만든 이 파일을 넣어주세요. 이는 GitHub에 올라가면 안 돼요!
[LOCAL]
android_udid=R3CX6QWZXE

.gitignore

# 개인 환경 설정
local.ini

# Allure 결과물
allure-results/
allure-report/

# Python
__pycache__/
*.pyc
.pytest_cache/

conftest.py 설정하기

이제 코딩을 해봅시다.
여기서 중요한 것은 전 포스팅에서도 설명드린 pytest입니다. 이는 전역적으로 설정하여 우리가 커스터마이징을 할 수 있어요.

pytest_addoption

pytest_addoption은 밑줄이 쳐지겠지만 이것은 스네이크 케이스가 아니야 하면서 무시하셔야 합니다. pytest의 문법이어서 이는 우리가 CLI 즉 cmd 커맨드 창에 명령어를 커스텀한다고 생각하시면 돼요. 실행할 때 pytest --env=device 이렇게 실행하죠.

  • action: 문자열을 저장할 거냐? 라고 물어보는 거고
  • help: pytest --help 명령어를 입력하면 "UDID는 필수 값입니다."라고 커맨드 창에 나와요. 우리는 그걸 보고 아 UDID가 없구나 하면서 에러를 찾을 수 있죠

Fixture

또한 전 포스팅에서 설명드린 대로 스코프는 세션으로, 이는 실행 시 한 번만 생성 후 TC가 다 끝나면 종료해라 라는 것입니다. 즉, 저 driver라는 함수는 계속 살아있죠. 테스트가 끝나기 전까지는.

이제 config.getoption 메서드로 우리가 설정한 env를 가져오고 이를 넘겨주세요. yield로 실행해서 끝나면 드라이버를 종료해라고 해주세요.

Allure 스크린샷 설정

또한 이제 훅을 만들어서 Allure 리포트를 사용한다면, 한 번 실행 후 많은 파일이 생기는데 아, 이것도 꼭 .gitignore에 추가해주세요.

만약 우리가 TC를 실행할 때마다 모든 것의 스크린샷을 찍는다면 중요하지도 않은데 불필요한 동작이겠죠. 그래서 우리는 이걸 설정해줘야 해요. 즉, 모든 TC에서 실패 시 사진을 찍어, 그리고 자동화의 목적은 제가 생각하기에는 사람이 하기 귀찮은 부분 그리고 비즈니스 로직은 자동화를 해줘야 한다고 생각해요. 그래서 비즈니스적으로 중요한 로그인, 회원가입, 결제 등은 사진을 찍기 위해서 설정을 해줘야 해요.

conftest.py 전체 코드

import pytest
from config_loader import create_driver
from utils.allure_helper import attach_screenshot

def pytest_addoption(parser):
    parser.addoption(
        "--env", action="store", default="device", help="UDID는 필수 값입니다."
    )

@pytest.fixture(scope="session")
def driver(request):
    env = request.config.getoption("--env")
    driver = create_driver(env)
    yield driver
    if driver:
        driver.quit()

@pytest.hookimpl(tryfirst=True, hookwrapper=True)
def pytest_runtest_makereport(item, call):
    outcome = yield
    result = outcome.get_result()

    driver = item.funcargs.get("driver", None)

    if driver:
        # 핵심 비즈니스 (성공/실패 모두 캡처)
        if "critical" in item.keywords:
            status = "passed" if result.passed else "failed"
            attach_screenshot(driver, name=f"{item.name}_{status}")

        # 나머지 TC (실패 시만 캡처)
        elif result.failed:
            attach_screenshot(driver, name=f"{item.name}_failed")

utils/allure_helper.py

import allure

def attach_screenshot(driver, name="screenshot"):
    png = driver.get_screenshot_as_png()
    allure.attach(png, name=name, attachment_type=allure.attachment_type.PNG)

def attach_text(name, content):
    allure.attach(content, name=name, attachment_type=allure.attachment_type.TEXT)

def attach_xml(name, content):
    allure.attach(content, name=name, attachment_type=allure.attachment_type.XML)

pytest 실행 명령어

이제 실제로 테스트를 실행하는 방법을 알아볼게요.

기본 실행

# local.ini의 [LOCAL] 섹션 사용 (기본값)
pytest

# 특정 환경 지정
pytest --env=local

# 특정 테스트 파일만 실행
pytest tests/test_login.py

# 특정 테스트 함수만 실행
pytest tests/test_login.py::test_login_success

Allure 리포트와 함께 실행

# Allure 결과 생성
pytest --alluredir=allure-results

# Allure 리포트 보기
allure serve allure-results

추가 여담

모바일 자동화 테스트 분야로 오면서 느끼는 생각은 참 공식 문서랑 레퍼런스(블로그) 등이 없다고 생각해요. 어떤 코드가 좋은 코드인지? 어떻게 해야 유지보수성이 좋고 가독성이 좋지? 이런 게 별로 없다고 생각해요. 물론 내가 못 찾는 것일 수도 ㅋㅋㅋ
공부하기가 쉽지 않다는 생각이 듭니다. 그래서 말씀드리고 싶은 것은 AI랑 같이 공부를 하면서 하는 게 좋을 것 같아요.
다만 AI는 최신 버전보다는 이미 오래전에 학습된 코드를 준다는 점, 이런 게 있기 때문에 잘 보고 판단해서 가져다 써야 합니다.
아직 공부한 지는 별로 안 되었지만 자동화 테스트를 만드는 패턴이 있고 구조가 있다고 생각해요.