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] 같은 계층에 속해있으면서 같은 조건을 만족하는 StaticText를 어떻게 구분해낼 수 있을까? 본문

☁️ QA

[Appium] 같은 계층에 속해있으면서 같은 조건을 만족하는 StaticText를 어떻게 구분해낼 수 있을까?

용히동 2025. 9. 22. 23:15

 

이번 문제 또한, 테스트 코드를 작성하던 중 직면했다.
아래 사진 처럼, type은 XCUIElementTypeStaticText로 동일한 경우에 두 요소 중 원하는 요소를 찾아내기 위한 과정을 기술했다.

 

🎀 이해를 돕기 위한 사전 설명

- 할 일이라는 객체를 루틴이라는 객체가 감싸고 있고, 루틴과 할 일은 1:N 관계를 이룬다.

- 할 일은 루틴 내부 + 버튼을 통해 추가로 생성할 수 있으며, 우측에 보이는 "3시간 15분"이라는 값은 사용자가 직접 설정한 타이머의 시간이다.

- "루틴 생성과 동시에 할 일 생성" 은 루틴 제목이고, "할 일 타이머 시간 임의로 설정"은 할 일의 제목이다.

- 루틴 제목 아래에 있는 "3시간 15분"은 루틴 내 모든 할 일의 시간의 총합이다.

- 할 일 시간의 총 합계가 UI 상에서 정상적으로 표시되는지 확인하기 위한 테스트 이다.

 


📍 에러 발생 상황

- 루틴 내 모든 할 일의 시간을 합하고, 계산해 루틴 제목 아래의 값과 비교하는 로직을 작성했다.

- 할 일의 시간을 구분하는 로케이터는 아래와 같다.
   루틴 내 다수의 할 일이 존재할 수 있어, findElemets를 통해 아래 조건에 해당하는 요소를 모두 가져왔다.
   아래 코드는 해당 경로에서 "시간"과 "분"을 포함하고 있는 StaticText 요소를 모두 가져오는 작업을 수행한다.

List<WebElement> toDoList = driver.findElements(iOSClassChain("**/XCUIElementTypeCollectionView/**/XCUIElementTypeStaticText[`name CONTAINS '시간' OR name CONTAINS '분'`]"));

- 원활한 상황 이해를 위해 전체 코드를 첨부하겠다.

private void findAllToDoInRoutineAndCalculateTotalTime() {
    List<WebElement> toDoList = driver.findElements(iOSClassChain("**/XCUIElementTypeCollectionView/**/XCUIElementTypeStaticText[`name CONTAINS '시간' OR name CONTAINS '분'`]"));
    for (WebElement el : toDoList) {
        String totalTime = el.getAttribute("name");
        String[] splitTotalTime = totalTime.split(" ");

        // 시간 혹은 분 만으로 이뤄진 경우 (ex. 1시간 or 30분)
        if (splitTotalTime.length == 1) {
            if (splitTotalTime[0].contains("시간")) {
                // 시간 String 제외 후 합산
                findRoutineTotalHour += Integer.parseInt(splitTotalTime[0].substring(0, splitTotalTime[0].length() - 2));
            } else {
                // 분 String 제외 후 합산
                findRoutineTotalMinute += Integer.parseInt(splitTotalTime[0].substring(0, splitTotalTime[0].length() - 1));
            }
        } else { // 2시간 30분 처럼 시간과 분으로 이뤄진 경우 (length == 2)
            findRoutineTotalHour += Integer.parseInt(splitTotalTime[0].substring(0, splitTotalTime[0].length() - 2));
            findRoutineTotalMinute += Integer.parseInt(splitTotalTime[1].substring(0, splitTotalTime[1].length() - 1));
        }
    }

    if (findRoutineTotalMinute > 59) {
        int quotient = findRoutineTotalMinute / 60;
        findRoutineTotalHour += quotient;
        findRoutineTotalMinute = findRoutineTotalMinute % 60;
    }
}

💭 기대 결과

- 3시간 15분 과 같은 시간 요소만 가져와서, if-else문을 문제 없이 통과해 전체 시간의 합계를 구한다.

 

❌ 실제 결과

- Index out of bounds exception 발생‼️

Index out of bound exception 발생


🛠️ 해결 과정

- 정확한 상황을 파악하기 위해 디버깅한 결과, 예상치 못한 상황을 확인했다.

- 코드 조건문에서 볼 수 있듯이, "시간"과 "분"을 포함하는 해당 계층의 StaticText를 가져오도록 했기 때문에 한 개의 할 일이 존재함에도 두 개의 요소가 toDoList 객체에 들어가 있는 것을 볼 수 있다.

- 당시 상황의 기대 값은 toDoList에 3시간 15분 이라는 하나의 객체만 들어가 있는 것이다.

- 하지만, 해당 객체의 "name" Attribute를 가져온 totalTime에는 예상과 달리 할 일 제목이 들어가 있음을 확인 할 수 있었다.

- 할 일 제목에 "시간" 혹은 "분"을 포함하고 있지 않은 경우에는 정상적으로 동작했지만 새로운 TC에 대한 테스트를 추가하면서 제목에 "시간"을 포함시켰더니, 기대 상황과 다르게 동작한 것이다.

 

- 정확한 로케이터 작성을 위해, Appium inspector를 통해 계층 구조를 분석해봤다.

   Appium Inspector를 코드 작성을 위한 보조 수단으로 활용하고 있었지만, 이렇게 다이나믹한 상황의 변화는 처음 겪었다.

 

1. 하나의 할 일만 생성한 경우

: 할 일 제목과 시간이 Static Text로만 존재하며, Button 레이어 없음

하나의 할 일만 생성한 경우

2. 두 개의 할 일을 생성한 경우

: 할 일 제목과 시간에 Button 레이어가 생기고, 그 아래에 static text로 각각의 제목, 시간 존재

캡쳐에는 없지만, 하나의 할 일을 삭제했을 때도 Button 레이어가 그대로 유지됨

 

3. Button 레이어 확인을 위한 두 개의 할 일 재생성

: 좀 전 같은 상황에서는 보였던 Button 레이어가 존재하지 않고, static text로만 존재함

 

로케이터 값이 고정적이지 않고, 유동적으로 변하는 것을 직접 확인했다.

따라서, 어떤 경우에도 항상 계층에 존재했던 Static Text 값을 활용하기로 결정했다.

 

➕ 사실 앱 화면에 보이는 할 일 또한 클릭 가능하기 때문에, 두 번째 상황에서 마주한 Button이 정상적인 상황이라고 생각했다.

하지만 Button 레이어를 포함해 로케이터를 작성했을 때, 에러조차 뜨지 않고 요소를 찾지 못하는 상황을 발견했다.

수정하면서 많이 도전해봤지만, 성공하지 못했고 두 번째 사진 이후로 Button 레이어를 볼 수 없었기 때문에 Static Text 값을 활용하기로 결정한 것이다.

 


 

💡 해결 방법 고민

1. Xpath의 following-sibling

: 할 일 제목을 넘기고, 같은 계층에 위치하는 static text 중 시간/분을 포함한 형제 요소를 찾는다.

➡️ 많은 할 일이 있다고 가정했을 때, 추가 작업이 더 필요한 상황.

할 일의 제목을 매번 넘겨줘야 하고, 많을 경우 일일이 수작업으로 넘길 수도 없기 때문에 코드상으로 구현했을 때 결국 static text를 찾아서 시간을 표시한 요소인지, 제목 요소인지 분류하는 작업이 필요함.

하지만 분류가 가능하다면 그냥 분류한 값을 쓰면 되기 때문에 효율적이지 못한 방법이라고 판단함

 

2.  현재 방법을 유지하면서 index로 시간 값만 가져오기

: 현재 방법을 유지하면, 시간/분을 포함하고 있는 static text를 모두 가져오게 되고, 시간은 항상 두 번째에 위치하니까 index로 구별

➡️ 시간/분을 포함하고 있는 static text만 가져오기 때문에, 만약 시간/분이 포함되지 않은 제목을 가져오게 되면 index로 구별하는 로직에서 제목인지, 시간을 표시한 요소인지 구별하는 작업이 필요함.

 

3. index로 시간 값을 가져오기 위해 findElements 조건 변경하기(시간/분 제외)

: 시간/분을 포함하고 있지 않아도, 해당 계층에 위치한 모든 static text를 가져오고 index로 구별

➡️ 해당 계층에 위치한 모든 static text를 가져오면 모든 할 일은 [제목 - 시간]의 형태로 이뤄져 있어 항상 짝수임.

제목이 없는 할 일은 생성조차 되지 않으니, 제목은 항상 존재하며 모든 할 일의 타이머는 1분 이상으로 구성돼야 하므로 시간 또한 항상 존재하는 요소임. == 항상 [제목 - 시간] 의 형태를 유지하며, 각각의 요소는 static text로 같은 계층에 존재한다.

할 일이 여러 개 추가 되더라도 계층 구조는 동일하게 유지되기 때문에 항상 홀수 index의 값을 가져오면 시간 값이 된다.


‼️ 결과

- 계층 구조를 분석한 뒤, 해당 계층에 존재하는 Static Text는 제목, 시간 밖에 없음을 확인

- 화면에서 해당 계층에 해당하는 Static Text를 모두 가져왔을 때의 값은 앞서 설명한 것처럼 항상 짝수임

- UI 변경이 있는 배포가 이뤄지지 않는 이상, [제목-시간]의 형태로 쌍을 이뤄 존재할 것으로 판단

- 따라서, 모든 Static Text를 가져왔을 때 시간 값은 언제나 홀수 인덱스에 위치하므로 for문을 통해 홀수 인덱스의 값을 가져와 시간 값으로 활용함.

- 아래는 해당 코드이며, 코드 설명은 주석을 통해 확인할 수 있음!

private void findAllToDoInRoutineAndCalculateTotalTime() {
    List<WebElement> toDoList = driver.findElements(iOSClassChain("**/XCUIElementTypeCollectionView/**/XCUIElementTypeCell/**/XCUIElementTypeStaticText"));
    int index = 0;
    for (WebElement el : toDoList) {
        // 인덱스는 0부터 시작하고 모든 시간 값은 홀수번째에 위치
        if (index % 2 == 1) {
            String totalTime = el.getAttribute("label");
            String[] splitTotalTime = totalTime.split(" ");

            // 시간 혹은 분 만으로 이뤄진 경우 (ex. 1시간 or 30분)
            if (splitTotalTime.length == 1) {
                if (splitTotalTime[0].contains("시간")) {
                    // 시간 String 제외 후 합산
                    findRoutineTotalHour += Integer.parseInt(splitTotalTime[0].substring(0, splitTotalTime[0].length() - 2));
                } else {
                    // 분 String 제외 후 합산
                    findRoutineTotalMinute += Integer.parseInt(splitTotalTime[0].substring(0, splitTotalTime[0].length() - 1));
                }
            } else { // 2시간 30분 처럼 시간과 분으로 이뤄진 경우 (length == 2)
                findRoutineTotalHour += Integer.parseInt(splitTotalTime[0].substring(0, splitTotalTime[0].length() - 2));
                findRoutineTotalMinute += Integer.parseInt(splitTotalTime[1].substring(0, splitTotalTime[1].length() - 1));
            }
        }

        index++;
    }

    if (findRoutineTotalMinute > 59) {
        int quotient = findRoutineTotalMinute / 60;
        findRoutineTotalHour += quotient;
        findRoutineTotalMinute = findRoutineTotalMinute % 60;
    }
}

 

🐞 의도한 대로 정확하게 동작하는 지 디버깅을 통해 테스트 해보았다.

- 7개의 할 일 생성, 아래는 테스트하고자 하는 화면이다.

 

- 기존 테스트 코드는 모두 루틴과 할 일을 새로 생성하므로, 시간 계산 테스트용 코드 추가 작성

   : 루틴과 할 일을 추가로 생성하지 않고, 이미 생성된 값만 계산한다.

@Test
public void 클래스체인_테스트() {
    WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));
    routineTab = wait.until(ExpectedConditions.presenceOfElementLocated(accessibilityId("clock.arrow.circlepath")));
    routineTab.click();

    WebElement createdRoutine = wait.until(ExpectedConditions.presenceOfElementLocated(accessibilityId("루틴 생성과 동시에 할 일 생성")));
    createdRoutine.click();

    findAllToDoInRoutineAndCalculateTotalTime();

    if (findRoutineTotalMinute != 0) {
        expectedRoutineTotalTime = findRoutineTotalHour + "시간 " + findRoutineTotalMinute + "분";
    } else {
        expectedRoutineTotalTime = findRoutineTotalHour + "시간";
    }

    // 전체 할 일 합산 시간 요소
    WebElement realRoutineTotalTime = driver.findElement(iOSClassChain("**/XCUIElementTypeScrollView/**/XCUIElementTypeStaticText[`name CONTAINS '시간' OR name CONTAINS '분'`]"));
    // 실제 합산 값과 비교
    assertEquals(realRoutineTotalTime.getAttribute("name"), expectedRoutineTotalTime);
}

 

1. Static Text 요소를 잘 찾는가?

: 제목 - 시간 값이 쌍을 이루므로 Static Text 요소는 모두 14개여야 한다.

➡️ 앞서 작성한 조건에 맞는 Static Text를 가져와 담은 toDoList는 size가 14개로 정상적으로 동작했으며, index가 1일 때의 요소에서 추출한 label Attibute은 3시간 15분 으로 잘 가져온 걸 확인할 수 있다.

 

2. 항상 홀수 값에 시간이 들어가는가?

: 홀수 값에 항상 시간이 있어야 하고, 다른 제목 값이 포함되면 안된다.

모든 디버깅 사진을 올리고 싶었지만, 너무 길어질 것 같아서 생략한다.

코드는 의도한 대로 동작했다.

 

3. 뿌듯한 결과

 


Appium Inspector를 통해 많은 요소를 가져오고, 코드에 작성하면서 꽤나 많은 상황을 마주했다고 생각했다.

예상치 못한 에러가 발생할 때도 많았고, 여태까지 나름대로 잘 해결해왔다고 생각했는데 이번에 마주한 상황은 정말 난처했다.

 

그리고 아직도 이렇게 해결하는 것보다 더 좋은 방법이 있지 않을까? 라는 생각이 끊임없이 든다.

하지만 이번 일로 또 새로운 경험치를 쌓을 수 있었던 것 같아서 뿌듯하기도 하다. 테스트를 진행하면서 로직을 수정하고, 개선해나가는 과정이 재밌고 즐거웠다.

 

특히나 이번 문제는, 답답하기도 했지만 꽤나 즐거운 과정이었다. 앞으로 더 많은 경험을 쌓아야겠지만, 오늘도 성장한 것 같아 뿌듯하다!✨