테스트 자동화

Appium, OpenCV를 활용한 Visual Testing

cwchoiit 2024. 4. 25. 15:31
728x90
반응형
SMALL

Appium을 이용해서 App Automation Testing을 할 때 가장 큰 난관 중 하나는 WebView에서 요소들을 가져올 수 없을 때다.

Appium Inspector 도구를 사용해도 WebView에서 필요한 요소들을 가져올 수 없을 때가 종종 있는데 이걸 Visual Testing으로 극복해보자. 

 

OpenCV 설치

우선, 이미지 처리를 위해 거의 모든걸 다 가지고 있는 OpenCV가 필요하다.

 

우선 환경설정을 해줘야한다. (MacOS)

export OPENCV4NODEJS_DISABLE_AUTOBUILD=1

 

나는 MacOS 유저이기 때문에 다음과 같이 Homebrew를 이용해 설치한다.

brew update
brew install opencv@4
brew link --force opencv@4

 

이 두가지 작업을 모두 다 하면 Appium이 OpenCV에 접근할 수 있도록 해주어야 한다.

Global node_modules 경로를 환경설정에 추가해주자. (MacOS)

export NODE_PATH=/usr/local/lib/node_modules

 

그리고 Appium을 설치했다면 저 node_modules 경로에 가면 appium 폴더가 있을텐데 그 폴더안에 들어가서 다음 명령어를 수행

npm i

 

이렇게 하면 OpenCV 설치와 Appium이 OpenCV에 접근 가능하도록 설정을 한 상태이다.

 

Appium Plugin 설치

이제 이미지 처리를 하기 위해 Appium의 플러그인을 설치해줘야 한다.

appium plugin install images

 

설치가 완료되면 이 플러그인을 사용하면서 Appium 서버를 실행해야 한다. 그래서 앞으로 Appium 서버를 실행할 땐 다음 명령어로 실행한다. 

appium --use-plugins images

 

 

이제 테스트 코드를 작성해보자.

테스트 코드 작성하기

우선 테스트 코드를 작성할 파일 경로는 다음과 같다.

src/test/java/AppiumSampleTest.java

 

AppiumSampleTest

@Slf4j
public class AppiumSampleTest {

    public static AndroidDriver driver;
    private static final double VISUAL_THRESHOLD = 0.99;

    @BeforeAll
    public static void setUp() throws MalformedURLException {
        UiAutomator2Options options = new UiAutomator2Options()
                .setUdid("HVA1FG23").setAutoGrantPermissions(true);

        URL appiumServer = URI
                .create("http://0.0.0.0:4723")
                .toURL();
        driver = new AndroidDriver(appiumServer, options);
    }

    @After
    public void tearDown() {
        if (driver != null) {
            driver.quit();
        }
    }
}

 

천천히 하나씩 해보자.

우선, 드라이버를 초기화해줘야한다. 그래서 모든 테스트마다 다 테스트 하기 전 드라이버를 초기화해야하니 @BeforeAll 애노테이션을 붙인 setUp()을 작성하자. 

 

그 안에서 하는 작업은 간단하다. 드라이버 초기화 시 옵션(UDID 설정, AutoGrantPermissions 허용)과 Appium Server를 드라이버에 넣어주면 된다. (여기서 옵션에 APK 경로 지정을 해서 시작하면서 해당 앱을 띄우게 해도 되는데 나는 테스트용이라 그냥 앱이 띄워져있다는 가정하에 실행하는거라 따로 옵션을 주지 않았다)

 

@After 애노테이션이 붙은 tearDown() 안에서는 드라이버를 quit()한다.

 

그리고 이 THRESHOLD 이 부분을 보자.

private static final double VISUAL_THRESHOLD = 0.99;

이미지의 유사도를 비교할 것이기 때문에 99퍼센트의 임계점에 대한 상수값이다.

 

 

이제 실제 테스트를 작성한다.

@Test
public void visualSimilarityMatching() throws IOException {

    log.info("System path = {} ", System.getProperty("user.dir"));

    File baseImage = new File("./src/test/resources/assets/base.png");

    if (!baseImage.exists()) {
        File screenshot = driver.getScreenshotAs(OutputType.FILE);
        FileUtils.copyFile(screenshot, new File("./src/test/resources/assets/base.png"));
    }

    SimilarityMatchingOptions opts =
            new SimilarityMatchingOptions().withEnabledVisualization();

    SimilarityMatchingResult res =
            driver.getImagesSimilarity(baseImage, driver.getScreenshotAs(OutputType.FILE), opts);

    if (res.getScore() >= VISUAL_THRESHOLD) {
        log.info("유사도 = [{}], 동일 화면임", res.getScore());
    } else {
        fail("유사도 = " + res.getScore() + ", 동일 화면이 아님");
    }
}

 

우선, 비교할 파일을 먼저 구비를 해둬야한다. 나의 경우 이런 이미지를 미리 src/test/resources/assets/base.png 경로에 넣어두었다.

 

그래서 이 파일이 저 경로에 없는 경우에 현재 드라이버와 연결된 앱의 화면을 스크린샷을 찍고 아닌 경우 넘어간다. 그 부분이 다음 코드.

File baseImage = new File("./src/test/resources/assets/base.png");

if (!baseImage.exists()) {
    File screenshot = driver.getScreenshotAs(OutputType.FILE);
    FileUtils.copyFile(screenshot, new File("./src/test/resources/assets/base.png"));
}

 

이제 Appium에서 제공하는 SimilarityMatchingOptions를 가져다가 사용해야 한다. 다음 코드.

SimilarityMatchingOptions opts =
                new SimilarityMatchingOptions().withEnabledVisualization();

SimilarityMatchingResult res =
        driver.getImagesSimilarity(baseImage, driver.getScreenshotAs(OutputType.FILE), opts);

 

그래서 이미지 두 개를 비교한다.

SimilarityMatchingResult res = driver.getImagesSimilarity(baseImage, driver.getScreenshotAs(OutputType.FILE), opts);
  • baseImage: 비교할 이미지
  • driver.getScreenshotAs(OutputType.FILE): 현재 드라이버를 통해 스크린샷을 찍으면 나오는 이미지

이 두 이미지를 비교한 결과가 res에 담긴다.

이제 이 코드를 테스트 해보자. 현재 내 Real Device는 딱 이 상태로 보여진다. 그러니까 아까 이미지(baseImage)랑 시스템 바 빼고 다 똑같다.

 

 

최종 코드 (AppiumSampleTest)

import io.appium.java_client.android.AndroidDriver;
import io.appium.java_client.android.options.UiAutomator2Options;
import io.appium.java_client.imagecomparison.OccurrenceMatchingOptions;
import io.appium.java_client.imagecomparison.OccurrenceMatchingResult;
import io.appium.java_client.imagecomparison.SimilarityMatchingOptions;
import io.appium.java_client.imagecomparison.SimilarityMatchingResult;
import lombok.extern.slf4j.Slf4j;
import net.sourceforge.tess4j.Tesseract;
import net.sourceforge.tess4j.TesseractException;
import org.apache.commons.io.FileUtils;
import org.junit.After;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.openqa.selenium.OutputType;

import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URL;

import static org.junit.jupiter.api.Assertions.*;

@Slf4j
public class AppiumSampleTest {

    public static AndroidDriver driver;
    private static final double VISUAL_THRESHOLD = 0.99;

    @BeforeAll
    public static void setUp() throws MalformedURLException {
        UiAutomator2Options options = new UiAutomator2Options()
                .setUdid("HVA1FG23").setAutoGrantPermissions(true);

        URL appiumServer = URI
                .create("http://0.0.0.0:4723")
                .toURL();
        driver = new AndroidDriver(appiumServer, options);
    }

    @After
    public void tearDown() {
        if (driver != null) {
            driver.quit();
        }
    }

    @Test
    public void visualSimilarityMatching() throws IOException {

        log.info("System path = {} ", System.getProperty("user.dir"));

        File baseImage = new File("./src/test/resources/assets/base.png");

        if (!baseImage.exists()) {
            File screenshot = driver.getScreenshotAs(OutputType.FILE);
            FileUtils.copyFile(screenshot, new File("./src/test/resources/assets/base.png"));
        }

        SimilarityMatchingOptions opts =
                new SimilarityMatchingOptions().withEnabledVisualization();

        SimilarityMatchingResult res =
                driver.getImagesSimilarity(baseImage, driver.getScreenshotAs(OutputType.FILE), opts);

        if (res.getScore() >= VISUAL_THRESHOLD) {
            log.info("유사도 = [{}], 동일 화면임", res.getScore());
        } else {
            fail("유사도 = " + res.getScore() + ", 동일 화면이 아님");
        }
    }
}

실행결과:

15:23:38.305 [Test worker] INFO AppiumSampleTest -- System path = /Users/choichiwon/monimo/monimo-app-ui-automation 
15:23:39.700 [Test worker] INFO AppiumSampleTest -- 유사도 = [0.9995598793029785], 동일 화면임

 

OpenCVAppium을 이용해서 이미지 유사도를 체크할 수 있게 됐다.

하나 더 해보자. 이미지에 어떤 부분이 있는지를 체크해내는 것도 있다.

@Test
public void visualOccurrenceMatching() throws IOException {
    File baseImage = new File("./src/test/resources/assets/base.png");
    File fragment = new File("./src/test/resources/assets/fragment.png");

    OccurrenceMatchingOptions opts = new OccurrenceMatchingOptions().withEnabledVisualization();
    OccurrenceMatchingResult res = driver.findImageOccurrence(baseImage, fragment, opts);

    log.info("조각 부분의 길이 = {}", res.getVisualization().length);
    log.info("조각 부분의 x좌표 = {} | y좌표 = {}", res.getRect().getX(), res.getRect().getY());

    assertTrue(res.getVisualization().length != 0);
    assertNotNull(res.getRect());
}

 

코드를 보면 baseImage 말고 fragment가 보인다. 이 이미지는 다음과 같다.

이 이미지가 저 baseImage에 있는지도 체크할 수 있다. 있다면 x,y 좌표도 알아낼 수 있다.

실행결과:

15:30:19.701 [Test worker] INFO AppiumSampleTest -- 조각 부분의 길이 = 118624
15:30:19.707 [Test worker] INFO AppiumSampleTest -- 조각 부분의 x좌표 = 452 | y좌표 = 996

 

좌표를 알아냈으니 WebView로 된 UI라도 원하는 요소를 선택할 수 있게됐다. 

728x90
반응형
LIST

'테스트 자동화' 카테고리의 다른 글

6. Appium과 Cucumber를 이용해 UI Automation Testing  (0) 2024.04.17
5. 프로젝트 환경 설정  (2) 2024.04.17
4. Appium Inspector 연결  (0) 2024.04.17
3. APK 설치  (0) 2024.04.17
2. Appium  (0) 2024.04.17