본문 바로가기

Android/Trouble Shoot

Appium + UiAutomator2 로 Android-Web 통합 테스트 자동화하기

Unsplash, Simon Kadula.

동기

재직 중인 회사에서 운영하는 대시민 서비스의 안드로이드 애플리케이션 일일 테스트를 맡게 되었습니다. 매일 아침, 앱 내의 모든 기능을 물리 디바이스에서 테스트하고, 그 결과를 엑셀 폼에 입력한 뒤 메신저로 공유하는 작업입니다.

 

처음에는 '아침에 정신도 차릴 겸 손으로 테스트해야겠다'고 생각했는데, 막상 해보니 한 시간 가까이 걸리는, 매일 아침마다 하기에는 매우 헤비한 작업이었습니다. 그도 그럴 것이, 전자 증명서 발급이나 QR 코드 스캔 등 테스트하기 복잡한 수많은 기능이 존재하기 때문입니다.

 

이틀 정도 진행하고 도무지 안되겠다 싶어, 자동화에 대한 허락을 구한 뒤, 이를 Appium 으로 자동화하였습니다. 오늘은 그 과정에 대해 기록하고자 합니다.


Appium + UiAutomator2

최초에는 안드로이드 표준 테스트 프레임워크인 Espresso 를 사용하려고 하였으나, 테스트 항목들을 모두 테스트하기에는 부족한 부분이 많았습니다. 대시민 서비스 특성상 외부 서비스 및 기존 운영 서비스들과의 연계가 주요 기능에 해당하기 때문이었습니다.(다른 앱을 실행하거나 특정 웹페이지에 로그인을 위한 정보를 전달하는 기능 등)

 

다양한 테스트 프레임워크를 찾아봤는데, 제 케이스에 정확하게 들어맞는 것은 Appium + UiAutomator2 가 유일했습니다. Selenium 과 Espresso 를 모두 사용하는 방향도 선택지 중에 있었는데, 코드 베이스가 두 군데로 나뉘게 되어 유지보수 측면에서 비용이 증가할 것이며, 웹 -> 앱의 테스트 연속성마저 상실하게 되기 때문에, 최종적으로 Appium 과 UiAutomator2 를 사용하기로 결정하였습니다.

 

소스 코드에 대한 수정이나 추가 구현도 다소 엄격하게 제한되는 프로젝트여서, 소스 코드에 최대한 의존하지 않는 방향으로 테스트 하기에도 Appium 을 사용하는 것이 가장 적절했습니다. 이 방식은 Appium 을 통해 ADB 명령을 물리 기기에 전달하는 방식이며, 간단한 전체 구조는 아래와 같습니다.

 

Appium 은 표준 WebDriverProtocol 을 준수하는 테스트 프레임워크이므로, HTTP 요청을 JSON 형태로 구성하여 Appium 서버에 전달하면, ADB 를 통해 해당 요청을 UiAnimator2 서버 앱에 전달합니다. 이 해당 앱이 안드로이드 OS API 를 호출해서 테스트를 진행하는 구조입니다. UiAnimator2 서버 앱은 Appium 을 활용해 앱 테스트를 진행할 때 기기에 추가로 설치됩니다. 


구현

테스트해야 하는 기능들을 간단하게 정리하면 다음과 같습니다.

  • 버튼 클릭 이후 사용자 지정 비밀번호를 입력하여 인증 정보 제출
  • QR 스캔 후 사용자 지정 비밀번호를 입력하여 인증 정보 제출

앱 자체가 사용자 인증 정보를 제출하는 데에 초점이 맞춰져 있으므로, 테스트할 기능이 크게 복잡하지는 않았습니다. 

1. 버튼 클릭 기능

targetTabXPath = f'//android.widget.HorizontalScrollView[@resource-id="{PACKAGE_NAME}:id/tabLayout"]//androidx.appcompat.widget.LinearLayoutCompat'
wait.until(EC.element_to_be_clickable((AppiumBy.XPATH, targetTabXPath))).click()
time.sleep(1)

 

wait.until(), element_to_be_clickable() 메서드를 통해 실제 클릭이 가능한 상태가 될 때까지 대기 후 클릭 이벤트를 실행할 수 있습니다. 이후에는 대기 시간을 주어 화면이 그려진 뒤에 필요한 작업을 수행할 수 있도록 하였습니다. 즉, 매우 직관적이고 간단하게, 그리고 안정적으로 버튼 클릭 테스트가 가능합니다.

 

앞서 Appium 이 표준 WebDriverProtocol 을 준수하기 때문에 JSON 형태로 구성된 HTTP 요청을 기반으로 동작한다고 언급하였는데, 출력되는 로그를 보면 실제로 POST 메서드를 통해 특정 세션에 요청을 보내고 응답까지 받는 것을 확인할 수 있습니다.

2. QR 스캐닝을 통한 인증 정보 제출 기능

다음으로 구현해야 했던 테스트는 QR 스캐닝을 통한 인증 정보 제출 기능에 대한 테스트였는데요. 현재까지는 PC 화면에 표시된 QR 코드를 직접 촬영하여 인증하는 방식으로 테스트가 진행되었는데, 이를 자동화하지 않는 편이 좋다고 판단했습니다. 물리 기기의 카메라를 활성화하여 실제로 QR 코드를 스캐닝하자니, 카메라 초점 문제 및 PC 점유 문제 등으로 인해 '자동화'에 대한 의미가 퇴색될 수 있었기 때문입니다.

 

별도의 팀 협의를 진행하였고 그 결과, 'QR 스캐닝 기능에 대한 검증보다는 스캐닝 이후 인증 정보 전달에 대한 검증이 필요하다' 로 결정되어 스캐닝에 대한 기능은 테스트하지 않았습니다. 

 

스캐닝 이후 인증 정보 전달 기능 구현을 위해 별도의 DeepLink Scheme 을 구성하는 것에 대한 허가를 받아, 이를 앱에 먼저 구현하였습니다. 이후 Python 스크립트 수준에서 QR 코드를 요청, 이를 디코딩한 문자열을 DeepLink 로 전달하여 인증을 진행하는 방식으로 테스트 코드를 작성하였습니다. 

...
from pyzbar.pyzbar import decode
...

print("QR 이미지 요청 중")
response = requests.get(QR_IMAGE_URL)
if response.status_code != 200:
    print(f"이미지 요청 실패: {response.status_code}")
    return

print("QR 코드 디코딩")
image_bytes = io.BytesIO(response.content)
image = Image.open(image_bytes)
    
decodeResult = decode(image)
if not decodeResult:
    print("QR 코드 디코딩 실패")
    return
    
qr_data = decodeResult[0].data.decode('utf-8')
print(f"QR 코드 데이터: {qr_data}")

deep_link_url = f"{SCHEME}://{qr_data}" 
driver.execute_script('mobile: deepLink', {
    'url': deep_link_url,
    'package': PACKAGE_NAME
})

3. 보고 파일 생성 기능

모든 기능을 테스트한 뒤에는 엑셀 파일의 특정 컬럼에 해당하는 모든 값을 테스트 수행 날짜로 변경한 뒤 보고를 진행하여야 하므로, 해당 기능까지 자동으로 진행될 수 있도록 구성하였습니다. 엑셀 파일을 열고 수정하는 데에는 openpyxl 라이브러리를 활용하여 진행하였습니다.

...
import openpyxl
from datetime import datetime
import os
...

def updateExcelReport():   
    todayStr = datetime.now().strftime("%Y-%m-%d")    
    todayFilenameStr = datetime.now().strftime("%Y_%m_%d")  
    newFilename = f"일일테스트_{todayFilenameStr}.xlsx"
    savePath = os.path.join(OUTPUT_REPORT_DIR, newFilename)

    try:
        workBook = openpyxl.load_workbook(REPORT_FORM_PATH)
        workSheet = workBook.active

        updateCount = 0
        for row in workSheet.iter_rows(min_row=2, min_col=8, max_col=8):
            for cell in row:
                cell.value = todayStr
                updateCount += 1

        if not os.path.exists(OUTPUT_REPORT_DIR):
            os.makedirs(OUTPUT_REPORT_DIR)
            
        workBook.save(savePath)
    except Exception as error:
        print(f"엑셀 처리 중 오류 발생: {error}")

후기

바빠지기 시작하는 시즌에 맡게 된 부담되는 작업이어서 첫 날 테스트 하자마자 자동화 생각이 들었습니다. 다만, Appium 에 대해 학습하고 이를 작성하기에는 적지 않은 시간이 소요될 것 같았는데, 예상했던 것보다는 금방 구현할 수 있었습니다. Appium 과 UiAnimator2 를 제어하는 코드가 매우 간결하기 때문인 것 같습니다.

 

자동화 이후 일주일 정도 사용해보았는데, 매일 한 시간 씩 테스트하고 이를 들여다 볼 필요가 없어지니 업무상 시간 분배하기가 매우 좋았습니다.

 

비록 제가 QA 엔지니어는 아니지만, End-to-End 로 이런 작업을 해보는게 생각보다 꽤 흥미로웠고, 이와 같이 파이프라인을 구축하는 작업 자체가 업무나 도메인 이해도를 향상시키는 데에도 좋았습니다. 또한, 이번 케이스처럼 기관으로부터 부여받은 작업이 아닌 다른 다양한 작업에도 능동적으로 Appium 과 UiAutomator2 를 활용할 수 있을 것 같아, 지속적으로 학습하면 더욱 좋을 것 같습니다. 지금 생각하기로는 CI/CD 작업에 이러한 테스트 플로우를 추가하는 일 정도가 있겠네요.

 

개발자는 정해진 납기 내에 좋은 제품을 전달하는 것이 중요하다고 늘 생각하는 편인데, 테스트 자동화를 통해 확보한 업무 리소스를 납기 준수와 품질 향상에 사용할 수 있으니, 제게는 큰 의미가 있는 일이었습니다. 

 

저와 같은 상황이시거나, 별도로 물리 디바이스 통합 테스트를 진행하여야 한다면 Appium + UiAutomator2 를 통해 한 번 구현해보시는 것도 좋을 것 같습니다.

 

긴 글 읽어주셔서 감사합니다.