Notice
Recent Posts
Recent Comments
Link
«   2026/02   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
Archives
Today
Total
관리 메뉴

성장기의 히동

[Appium, iOS] 포그라운드에서 iOS 앱 푸시 알림 테스트 - 알림센터에 등록되지 않는 경우 본문

☁️ QA

[Appium, iOS] 포그라운드에서 iOS 앱 푸시 알림 테스트 - 알림센터에 등록되지 않는 경우

용히동 2025. 10. 23. 01:52

🌼

이번 글에서는 iOS 앱 테스트를 진행하면서, 포그라운드 앱 PUSH 알림 테스트에
여러 번 실패한 과정을 기록했다.
테스트한 어플에서는 포그라운드에서 푸시 알림이 왔을 때 알림 센터에 적재되지 않았다.


따라서 이번 글에서는 포그라운드에서 푸시 알림이 동작했을 때, 알림 센터에 적재되지 않는 경우의
자동화를 다뤘다.

 

어떻게 해결했는지! 가 궁금하다면 길고 긴 글의 후반부를 보면 될 것이다.


 

아래의 사진에서 볼 수 있듯이, 타이머가 종료되면 앱 푸시 알림이 와야 한다.

기본적으로 앱 푸시 알림은, 앱 서버가 아니라 외부 서버에서 앱 서버로 "밀어넣는" 방식이다. 이것을 간과했다.

타이머 정상 종료 후 push 알림이 오는 상태

 

타이머가 정상적으로 종료됐을 때, Push 알림은 아래와 같은 규칙을 지닌다.

1. Title :
타이머 이름 + 종료! (미리 등록된 할 일 + 종료!)

2. Subtitle :

서버에서 미리 등록된 랜덤의 메시지

 

여기서 중요하게 생각한 건, 랜덤으로 등장하는 Subtitle이 아니라 Title이다.

사용자가 등록한 제목을 그대로 가져와서 종료됐다고 알려줘야만 한다. 혹시라도 다음 타이머의 이름이 등장하거나, 어디서 나왔는지 모를 문구가 뜨면 버그이기 때문이다.

 

 

💭 테스트 구상

아래와 같은 사고 흐름을 탔다.

1. 타이머는 정상적으로 종료돼야 한다.

- 루틴 내 모든 타이머가 종료되면 루틴 탭으로 복귀한다. 이 때, 루틴 탭에 실행중인 타이머가 없어야 한다.

❓만약 루틴 내에 다수의 타이머가 존재하면 어떻게 종료됐음을 알지?

➡️ 푸시 알림이 와야 한다 ⭐️⭐️⭐️⭐️⭐️

 

위 과정에서 중요한 것은, 개별 타이머가 종료됐음을 확인하는 방식이다.

1-1. 루틴 내 단일 타이머만 존재하는 경우

- 잔여 타이머 시간이 0이 된다

- 푸시 알림으로 타이머가 종료됐음을 표시한다

- 루틴 탭으로 복귀한다

 

1-2. 루틴 내 다수의 타이머가 존재하는 경우

- 잔여 타이머 시간이 0이 된다

- 푸시 알림으로 타이머가 종료됐음을 표시한다

- 다음 타이머를 자동으로 실행한다

 

두 개의 공통점은, 잔여 시간이 0이 된다 / 푸시 알림을 보내 타이머가 종료됐음을 알 수 있도록 한다는 것이다.

푸시 알림을 테스트 하기 위해 타이머의 개수는 중요하지 않다고 판단했기 때문에, "푸시 알림의 동작 여부"에만 초점을 맞췄다.

 

포그라운드에서 온 푸시 알림은 알림 센터에 적재되지 않는다. 그럼 어떻게 알지?

내 눈으로 보는게 제일 쉽지~~&^&^&^&$%^%^#%#@%#@%#%$#^


🛠️ 테스트 시도 과정

1. 타이머 잔여 시간이 0이 되는지 확인

타이머가 정상적으로 종료되는 경우, 세 번째 사진과 같이 타이머 잔여 시간이 0으로 표시되면서 푸시 알림이 오는 방식이다.

루틴 내 몇 개의 타이머가 존재하던 간에, 모든 타이머는 항상 종료되기 직전에 "0" 이라는 잔여 시간을 표시한다.

이는 타이머가 정상적으로 종료됐음을 확인하는 방식에서, 루틴 내에 몇 개의 타이머가 있는 지의 여부를 TC로 나눠서 확인할 필요가 없어 간편하다고 판단했다.

 

따라서 잔여 시간이 0이 되는 시점을 잡아 정상적으로 종료됐음을 확인하고자 했다.

1. 타이머 실행 화면 / 2. 타이머 종료 직전 화면 / 3. 타이머 잔여시간 0초 도달과 동시에 Push 알림이 오는 화면

 

최소 타이머 등록 및 실행 시간은 1분이기 때문에, 타이머가 종료될 때까지 해당 화면에서 대기할 필요가 있었다.

따라서, WebDriverWait를 활용해 화면에 "0"이 표시되는 순간을 기다렸다.

WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(70));
try {
    wait.until(ExpectedConditions.presenceOfElementLocated(iOSClassChain("**/XCUIElementTypeStaticText[`name == \"0\"`]")));
} catch (Exception e) {
    System.out.println("타이머가 종료되지 않았습니다");
    fail();
}

 

하지만 0이 되는 순간은 워낙 찰나였고,
바로 루틴 탭으로 복귀하기 때문에 코드에서 이를 잡지 못하고 타이머가 종료되지 않았다는 String을 출력했다.

 

 

2. PUSH 알림 배너에서 대상 Text 찾기

타이머가 종료됨과 동시에 PUSH 알림이 배너로 출력되는 모습

 

옆의 사진에서 볼 수 있듯이, 앱이 실행중인 상황인 포그라운드에서 PUSH 알림이 정상적으로 동작하고, 이를 Appium Inspector 에서도 캡쳐할 수 있었다.

(Appium Inspector에서 캡쳐한 화면임)

 

Appium Inspector에 캡쳐됐다면, 요소로도 잡을 수 있을 것 같아서 찾아봤는데 없었다.

당연하다.

글 초입에 설명했다시피, 알림 배너는 앱의 동작이 아니라 외부 서버에서 밀어넣은 것이기 때문이다.

이해한대로 설명해보자면, Appium Insepector 실행할 때 Json에 AppBundleId 넣잖아요..?

근데 저 푸시 알림을 앱에서 보내는 걸까요? 아니란 말입니다!

정리하자면, 테스트 대상 앱의 요소가 아니라서 안됩니다!

 

이걸 잡을 수 있었으면 정말 편했을 텐데여 후.

 

 

3. 코드 직접 까보고 내부 이벤트 감지하기

푸시 알림이 왔다는 것은, 코드에서 푸시 알림을 요청하는 부분이 있다는 것.

그래서 코드를 직접 까봤다. 개발자에 의하면, 아래 코드는 알림을 보내는 트리거의 이름이라고 했다.

그리고 Gemini는 저게 내부에서 발생하는 알림의 이름이라고 했다.

Notification.Name 정의부

따라서, 외부 서버에 푸시 알림을 요청하기 전에 내부적으로 타이머가 종료됐다는 트리거가 분명히 있을 거라고 판단했다.

위 코드의 doneTimer 라는 이름의 내부 알림만 검증하면 보다 안정적인 검증을 진행할 수 있을 것 같아서 시도했다.

 

이 방법을 시도할 때 Appium 공식 레퍼런스에 적힌 mobile: expectNotification을 사용했다.

Arguments의 name이 바로 앞서 언급한 "doneTimer"가 들어갈 곳이다.

example과 달라서 걱정했는데, 개발자가 설정하는 값이라 그냥 Notification.Name에 정의된 그대로 쓰면 된다고 한다.

var result = driver.executeScript("mobile: expectNotification", Map.ofEntries(
    Map.entry("name", "doneTimer"),
    Map.entry("timeoutSeconds", "5.5")
));

위와 같이 코드를 작성했고, 실패했다^^~!

 

이 방법은 나중에 또 다른 상황에 직면했을 때 내부적으로 로직이 안정적으로 동작하고 있는지 확인할 때 이용하면 좋을 것 같다고 생각했다.

이 참에 하나 배웠으니까 좋다...~👍🏻

 

4. ⭐️ 실기기 로그 분석하기

4-1. 내부 코드 분석해서 푸시 알림이 포그라운드에서 동작할 때 출력되는 Print문 실기기 로그에서 찾기

4-2. ⭐️실제로 실기기에서 푸시 알림이 동작한 로그 찾기⭐️(해결책)


 

4-1. 내부 코드 분석해서 푸시 알림이 포그라운드에서 동작할 때 출력되는 Print문 실기기 로그에서 찾기

이번에도 내부 코드를 까봤다.

포그라운드에서 푸시 알림이 수신됐을 때 출력할 로그

print는 콘솔에 찍히는 것 뿐만 아니라, 실기기 로그에도 찍힌다고 한다.

그렇다면 실기기 로그에서 찾을 수 있어야 하지 않을까?

 

실기기 콘솔에서 검색해 봤는데, 아무 로그도 발견되지 않았다.

이 print문이 동작하는게 아니라 다른 건가? 싶어서 다 검색해봤는데도 없었다.

 

그렇다고 저 로그가 Appium 서버 로그에 찍힐 일은 없었다.

Appium 서버 로그는 일반적으로 click과 같은 명령어, 세션 등의 로그만 담고 있기 때문이다.

 

그렇다면 이 방법은 푸시 알림이 실기기에서 실제로 동작했는지 판별할 수 있는 기준점이 될 수 없기 때문에 다른 방법을 찾았다.

 

4-2. ⭐️실제로 실기기에서 푸시 알림이 동작한 로그 찾기⭐️(해결책)

실기기 콘솔 로그를 직접 열어볼 줄은 몰랐는데, 실제로 푸시 알림이 동작했다를 검증하려면 이보다 정확한 방법은 없을 것 같았다.

앞선 방법으로 로그를 한 번 까보니까 해볼 수 있을 것 같았다.

 

첫째로 필요한 시점에 발생하는 로그만 확인해보기 위해서, 지우기 버튼을 눌러가면서 푸시 알림이 동작한 이후의 로그만 확인했다.

둘째로 로그를 하나 하나 살펴보면서, 의미 있는 로그를 찾으려고 했다.

 

1. 알림 페이로드를 받아 사용자 테스트 알림으로 처리했다는 확인 로그

Got user testing notification with payload {
    controllerClass = SBHWidgetContainerViewController;
    event = ViewDidAppear;
}

- payload에 보이는 controllerClass는 다양했다. 아무래도 하나의 Controller에서 처리하지 않기 때문인 것 같다.

- event는 ViewDidDisAppear, ViewDidAppear로 나뉘었다.

 

2. 알림이 화면에 나타났다는 이벤트까지 시스템이 인지했다는 로그

Calling handler for AX notification kAXUserTestingNotification from AX element pid: 34, elementOrHash.elementID: 0.1 with payload {
    controllerClass = SBHWidgetViewController;
    event = ViewDidDisappear;
}

- controllerClass는 역시나 다양했다.

- event는 위 로그와 동일했다.

- 차이점이 있다면, 보다 구체적인 로그가 잔뜩 쌓여있었다. 중요한 정보는 아니라서, 아래 조그맣게 남긴다.

Calling handler for AX notification kAXPidStatusChangedNotification from AX element pid: 34, elementOrHash.elementID: 0.0 with payload {

    "display-type" = 0;

    pid = 187;

    "suspended-status" = suspended;

}

 

 

여기서 가장 중요했던 건, 푸시 알림이 정상적으로 동작했다는 것을 검증해낼 수 있는 두 로그의 공통점이었다.

"event = ViewDidAppear" 

 

따라서, 아래와 같이 테스트 로직을 구상했다.

 

1. 테스트 시작 시점보다 이후의 로그를 가져올 것.

2. 보다 확실하게 하기 위해, 로그를 가져오기 전에 로그 버퍼를 비운다.

3. event = ViewDidAppear 이라는 문자열을 포함한 로그를 찾는다면 콘솔창에 출력하고 테스트 성공 처리

 

아래는 실제로 작성하고, 테스트에 성공한 코드이다.

        @Test
        public void 알람_ON_푸시_알림이_포그라운드에서_정상적으로_동작하는지_확인() throws InterruptedException {
            /*
             * 테스트 편의상 타이머는 모두 1분으로 설정함
             * 포그라운드에서 푸시 알림이 동작할 경우 알림 센터에 쌓이지 않아 로그로 확인함
             * */

            // 테스트 시작 시간
            final long verificationStartTime = Instant.now().toEpochMilli();
            System.out.println("시작 타임스탬프: " + verificationStartTime);
            
            final String expectedNotificationLogMessage = "event = ViewDidAppear";

            ... (중략)
            
            // 타이머가 종료돼서 루틴 탭으로 복귀할 때까지 대기
            WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(70));
            wait.until(ExpectedConditions.presenceOfElementLocated(accessibilityId("clock.arrow.circlepath")));

            // 로그 버퍼 초기화 -> 이전 로그 걸러냄
            cleanLogsBuffer(driver);

            // 푸시 알림 로그 확인 동작
            Wait<WebDriver> waitLog = new FluentWait<WebDriver>(driver)
                    .withTimeout(Duration.ofSeconds(10))
                    .pollingEvery(Duration.ofSeconds(1))
                    .ignoring(Exception.class);

            System.out.println("푸시 알림 로그 대기 시작: " + toDoTitle);

            try {
                boolean foundLog = waitLog.until(new Function<WebDriver, Boolean>() {
                    @Override
                    public Boolean apply(WebDriver webDriver) {
                        LogEntries logEntries;
                        try {
                            logEntries = driver.manage().logs().get("syslog");
                        } catch (Exception e) {
                            System.out.println("로그 가져오기 실패: " + e.getMessage());
                            return false;
                        }

                        for (LogEntry logEntry : logEntries) {
                            // 테스트 시작 이후의 로그 선별
                            if (logEntry.getTimestamp() >= verificationStartTime) {
                                System.out.println("현재 메시지: " + logEntry.getMessage() + "현재 타임스탬프: " + logEntry.getTimestamp());
                                if (logEntry.getMessage().contains(expectedNotificationLogMessage)) {
                                    System.out.println("포그라운드 알림 로그 가져오기 성공: " + logEntry.getMessage());
                                    return true;
                                }
                            }
                        }
                        return false;
                    }
                });
                if (!foundLog) {
                    System.out.println("푸시 알림 검증 실패: 시간 내에 해당 로그를 찾지 못했습니다.");
                    fail();
                }
            } catch (Exception e) {
                System.out.println("푸시 알림 검증 실패: " + e.getMessage());
                fail();
            }
        }
    }

개인적으로 테스트 코드에서 Exception을 던지는 코드를 작성하는 건 선호하지 않아서, 테스트 실패로 처리한다. (fail() 메서드)

 

✅ 테스트 결과 화면

테스트 성공 및 콘솔 출력 화면
XCode 실기기 콘솔 로그 화면

 

실기기 콘솔 로그 화면을 잘 보면, 하나의 로그에 3줄의 String이 있는 것을 확인할 수 있다.

그런데, 테스트 성공 시 콘솔 화면을 보면 각각의 줄이 하나의 로그로 분리되어 캡쳐된 것을 확인할 수 있다.

 

참고해서 코드를 작성했으면 한다.

 

처음에 그냥 이렇게 찾으려고 통째로 넣었다가 절대 못 찾길래 한 줄씩 찍어봤는데 한 줄씩 잘려서 나오더라..!

Got user testing notification with payload {
    controllerClass = SBHMultiplexingViewController;
    event = ViewDidAppear;
}

 


❤️ 후기

포그라운드에서의 푸시 알림 테스트가 이렇게 오래 걸릴 줄 예상하지 못 했다.

이전까지는 앱 UI 요소였기 때문에 비교적 수월하게 진행해온 것임을 깨달았다.

 

현업에서 어떻게 푸시 알림이 동작했음을 자동화로 검증하는 지는 잘 모르겠지만, 그래도 이번 기회에 내가 시도해볼 수 있는 다양한 방법을 시도해본 것 같아서 매우 뿌듯한 시간이었다.

 

며칠 동안 고민하고, 시도하고, 실패하면서 많이 힘들기도 했지만 각각 시도해본 방법을 처음 생각해냈을 때만큼은 정말 신나고 기대됐다.

그리고 그 방법들을 시도하면서 다음에 어떤 부분에 활용해볼지를 고민하니, 이 시간들이 아깝지 않았다.

 

물론 정답이 아닐 수도 있고, 푸시 알림 정도야 눈으로 직접 확인하는 방식으로 진행하고 있을 지도 모르겠다.

하지만, 자동화 테스트 코드를 보다 견고하게 만들 수 있는 또 다른 방법을 찾아낸 것에 의의를 두고싶다.

재밌었다. 매우..✨