brunch

대규모 모바일 테스트 자동화를
위한 개발 표준 소개

Asleep Mobile Automation Standard Guide

by 이지원
제목을-입력해주세요_-001 (11).png

자동화 테스트를 도입하는 많은 조직이 겪는 공통적인 과정이 있습니다. 초기에는 몇 개의 스크립트로 잘 돌아가는 듯하다가 앱의 규모가 커지고 기능이 추가될수록 유지보수가 어려워지고 간헐적 실패(Flaky Test)가 클라이언트 사이드인지 서버 사이드인지 파악하기 어려울 정도의 디버깅이 난해한 환경에 놓이곤 하는데요. 이번 포스팅에서는 에이슬립 모바일 프로덕트의 대규모 자동화를 준비하며 유지보수성과 확장성을 극대화하기 위해 고민했던 아키텍처 설계 과정과 코드 레벨의 해결책을 공유합니다.


https://brunch.co.kr/@jiwonleeqa/376

과거 2000개가 넘는 모바일 테스트케이스를 검증해야 했습니다. 당시에는 Appium이나 Selenium을 추상화한 프레임워크 없이 Appium Python-Client와 Pytest를 직접 사용해 설계된 상태였습니다. 초기에는 간단한 구조로 빠르게 시작할 수 있겠지만, 대규모 병렬화를 구현해야 할 시점에서 복잡성이 크게 증가했습니다. 특히 FastAPI로 자체 프록시 서버를 구현해 테스트 요청을 분산하고, 각 디바이스별로 Appium 서버를 관리하는 과정에서 많은 개발 및 유지보수 부담이 발생했습니다. FastAPI 프록시는 테스트 요청을 라우팅 하고 세션 충돌을 방지하는 역할을 했지만, 오류 처리와 확장성 관리에서 한계를 드러냈습니다. 이 과정에서 저는 이미 더 나은 방법이 존재하는데 이렇게까지 해야 할 필요가 있을까?라는 의문을 품게 되었습니다. 기술적으로 더 효율적인 대안이 있다고 판단해 새로운 아키텍처를 제안했지만, 기존 방식에 이미 많은 리소스가 투자된 상황에서 이를 전환하기에는 팀의 추가적인 협조와 설득이 필요했습니다. 결국 제가 제안했던 새로운 아키텍처는 실행되지 않았습니다. 당시에는 아쉬움이 컸지만 그 경험을 통해 배우고 성장할 수 있는 계기가 되었습니다.

위 게시글에서는 모바일 대규모 병렬 환경 운영에 필요한 Appium Server 아키텍처 대한 내용에 중점을 뒀다면 이번 게시물에서는 코드 레벨에서의 대규모 모바일 테스트 자동화를 위한 개발 표준 소개합니다. 제가 왜 이렇게 결정했는지에 대한 기술적 의사결정 과정과 코드 레벨의 디테일을 중심으로 작성했습니다. Asleep Mobile Automation Standard Guide로 활용하고 있는 만큼 추후 프로덕트와 회사의 성장으로 인해 새로운 QA분을 추가로 모시게 될 기회가 생길 경우 에이슬립에서는 이러한 방식으로 품질 보증에 필요한 시스템을 개발하고 있으니 참고하시면 좋을 것 같습니다.


Policy 1 로케이터 관리(JSON vs POM)

6년 가까이 자동화 개발을 해오면서 수천 개의 UI 요소(Locator)를 어떻게 관리할 것인가에 대해서 도메인과 서비스 특성별로 다양한 관점과 방식을 도입하곤 했었는데요. 해외 자동화 개발 분야에서 활용되는 대부분의 개발 패턴들을 경험해 봤고 복잡성만 오히려 높인다는 생각과 더불어 개발 경험이 많지 않았던 초기에는 전통적인 POM 구조가 지니고 있는 장점에 대해 크게 와닿지 않았기에 중앙집중식 형태로 관리하곤 했었습니다.


로직과 데이터를 완전히 분리하자는 생각으로 모든 로케이터를 하나의 거대한 json 파일에 넣는 식으로 개발을 했었는데요. 하지만 대규모 협업 환경에서는 단점이 있었습니다.

Merge Conflict: 여러 QA 엔지니어가 동시에 작업할 때 충돌이 빈번함.

동적 요소 처리 불가: text("${name}")처럼 변수를 받아야 하는 로케이터를 JSON으로 처리하려면 별도의 파싱 로직이 필요해짐.

DX(개발자 경험) 저하: IDE의 자동완성이나 타입 추론의 도움을 받기 힘듦.


이후 다시 전통적인 POM 구조로 개발을 했다가 일정 코드 수준에 도달하면 가독성이 나빠지는 경험을 겪고 나서 최종적으로 전통적인 POM의 장점인 캡슐화를 살리면서 데이터 관리의 명확함을 위해 데이터(Selector)와 로직(Behavior)의 완전한 분리를 진행하게 되었습니다. 즉 클래스 내부를 두 개의 영역으로 쪼개는 방식인데요.

Zone 1 (Data): "무엇을(Target)" 찾을 것인가? -> Selector 정의부

Zone 2 (Logic): "어떻게(Action)" 동작할 것인가? -> Method 구현부


하지만 데이터 영역을 코드로 구현하는 과정에서 몇 가지 이슈가 있었습니다. 처음에는 데이터 영역을 가장 직관적인 static 필드로 정의하려 했습니다. 인스턴스 생성과 무관하게 고정된 데이터로 관리하고 싶었기 때문인데요.


하지만 이 방식은 OS 분기 로직이 데이터 선언부에 노출된다는 단점이 있었습니다. 부모 클래스에 getSelector()라는 헬퍼 메서드를 만들어 해당 로직을 추상화하고 싶었습니다. 하지만 static 영역은 클래스가 로드되는 시점에 평가되므로 인스턴스 컨텍스트인 this에 접근할 수 없어 부모 메서드 호출이 불가했습니다.

ㅇㄴㅁㅇㅁ.png

따라서 분리 철학은 유지하되 구현 방식을 static에서 문법적으로는 속성처럼 보이지만 실제로는 함수처럼 동작하여 인스턴스 컨텍스트(this)를 확보할 수 있는 Getter로 변경하게 되었습니다.


Policy 2 One Code, Multi-OS

안드로이드와 iOS는 UI 구조가 다릅니다. 그렇다고 LoginPageAndroid.js, LoginPageIOS.js를 따로 만드는 건 중복 코드 생성과 파일 관리를 어렵게 만듭니다. 구조는 다르지만 비즈니스 로직은 동일하다는 점에서 Selector Abstraction을 통한 단일 코드베이스로 실행 가능한 형태의 구조를 선호하게 되었습니다. 즉 로그인 버튼을 누르는 행위(Action)는 같고 그 버튼을 찾는 방법(Selector)만 다를 뿐인 거죠.

나나.png

Page.js에 OS 분기 헬퍼를 심어 하위 페이지들이 OS를 신경 쓰지 않도록 추상화하는 방법으로 간단히 구현할 수 있습니다. 테스트 스크립트(spec.js)에서는 if (isAndroid) 같은 분기문 없이 단 하나의 코드로 두 OS를 모두 테스트할 수 있습니다.


Policy 3 클라이언트 사이드의 Flaky Test 원인 제거

모바일 자동화에서 가장 흔한 실패 원인인 클라이언트 사이드에서 발생하는 Flaky Test의 원인은 화면 밖의 요소인데요. 특히 WDIO Expecct 매처의 기본 동작은 화면 밖의 요소를 조작하려 하면 에러를 뱉습니다. 테스트 코드에서 이러한 문제를 신경 쓰지 않게 하기 위해 모든 주요 액션(click, input, readText)은 실행 전 반드시 요소를 화면에 위치할 때까지 찾도록 scrollTo 메서드를 구현하여 이를 강제하도록 처리했습니다.

ㅁㄴㅇㅁㄴㅇ.png

특히 온프레미스가 아닌 클라우드 환경에서 유휴 기기 조회 API를 통해 디바이스를 무작위로 실행하다 보면 클라우드 실기기 특성에 따라 텍스트 필드와 Interaction 과정에서 키보드 창이 노출되어 테스트가 실패하곤 하는데요. 이러한 현상도 공통 메서드에서 관리하도록 하여 불필요한 중복 코드를 생성하지 않도록 하고 오직 비즈니스 로직에만 집중 가능하도록 했습니다.


Policy 4 데이터 관리 5-Layer Strategy(Input/Output)

dk아아.png
ㅁㄴ임ㄴㅇㅁ넝.png

테스트 데이터를 어떻게 하면 직관적이고 효과적으로 관리할 수 있을까를 고민하다 보니 생겨난 구조인데요. E2E 테스트 시나리오는 결국 정형화된 패턴이 존재하게 됩니다. 이에 따라 데이터를 입력하는 재료(Input)와 검증하는 정답지(Output)로 명확히 나누어 5단계 레이어로 설계했습니다.


Policy 5 테스트 케이스 구조화

기획서가 없거나 자주 바뀌는 환경에서 테스트 케이스를 목적(Purpose)에 따라 3가지로 명확히 분류했습니다. WebdriverIO에서는 wdio.conf에서 suite로 spec 파일의 실행 그룹을 관리할 수 있는데요.


크게 사용자가 상식적으로 정상적인 동작을 했다고 가정한 시나리오의 그룹인 Positive Suite(Happy Path), 사용자가 의도하지 않은 비유효 행동을 했다고 가정한 시나리오의 그룹인 Nagative Suite(Robustness), 사용자가 앱의 핵심 기능을 동작하기 위한 전체적인 프로세스가 잘되는지를 확인하는 시나리오의 그룹인 E2E Suite로 구분하여 개발하게 되었습니다.


Positive Suite 같은 경우 input_generators를 활용한 유효한 데이터로 검증을 진행하고 Negative Suite는 input_validation의 경곗값(Boundary Value) 데이터를 주입하여 방어 로직 및 에러를 검증하고, E2E Suite에서는 API Helper를 통한 사전 데이터를 세팅하고 앱의 전체 라이플사이클을 검증하게 됩니다


Policy 6 테스트 케이스 설계 및 개발 규칙

앞서 설명한 것처럼 테스트 스위트를 크게 3가지로 분류하게 되는데요.

Positive (정상 흐름): "사용자가 상식적으로 입력했을 때 잘 되는가?"

Negative (예외 흐름): "이상한 값을 넣었을 때 시스템이 방어하는가?"

E2E (전체 흐름): "가입부터 리포트 확인까지 끊김 없이 연결되는가?"


일관성 있는 개발과 동작을 위해 코드 레벨에서 표현하기 위한 규칙이 필요합니다. 예를 들면 아래와 같은 Rule을 통해서 테스트 케이스를 개발할 수 있습니다.

아아아아.png

[Rule 1] 명명 규칙 (Naming)

Format: [Tag] 기능 - 상황 - 기대결과

Example: it('[Negative] 로그인 - 틀린 비밀번호 입력 시 - 에러 메시지 노출',...)

[Rule 2] AAA 패턴 (Arrange-Act-Assert)

준비, 실행, 검증 단계를 줄 바꿈으로 구분한다.

[Rule 3] 검증 원칙 (Assertion)

expect 호출 전에는 반드시 await Page.scrollTo(element)를 호출하여 요소를 화면에 띄운다.


마치며

좋은 자동화 아키텍처는 단순히 테스트를 돌리는 것을 넘어 변화에 유연하게 대응하는 구조를 만드는 것입니다. Page Object는 "어떻게(How)" 동작할지를 책임지고 Test Spec은 "무엇을(What)" 검증할지를 책임지며 Data Layer는 둘 사이의 재료를 공급합니다. 이러한 관심사의 분리와 추상화 과정을 통해 안드로이드와 iOS 모두 변화에 유연하게 대응하는 견고한 자동화 환경을 구축할 수 있었습니다.

keyword
매거진의 이전글내가 추구하는 배포 후 E2E 테스트 모니터링 전략