본문 바로가기
프로그래밍 개발/React Native

React Native - E2E UI 테스트 도입, Detox

by Jinseok Kim 2023. 8. 18.
반응형

E2E UI 테스트 도입, Detox

 

Detox

Made For CI Execute your E2E tests on CI platforms like Travis CI, CircleCI or Jenkins without grief.

wix.github.io

💡 현재 서비스 운영 중인 사용자 앱의 서비스가 점점 커지다 보니 개발 및 유지보수 후 예측할 수 없는 사이드 이펙트로 인한 중요 비즈니스 기능들의 이슈에 대한 영향이 더 많이 있을 수 있다고 판단하였습니다.

그래서 사용자 앱 서비스 프로덕션 배포전 E2E 테스트를 거쳐갈 필요가 있다고 판단하여 Detox라는 E2E 자동화 테스트 라이브러리를 도입하여 개발 환경에 E2E 자동화 테스트 환경을 구축하게 되었습니다.

 

 

 

 

Detox는 React Native 앱의 E2E 자동화 UI 테스트를 도와주는 프레임워크를 제공합니다.

 

실제 사용자가 UI 이벤트를 클릭하는 것처럼 시뮬레이터(IOS) 혹은 에뮬레이터(AOS)에서 사용 이벤트 플로우를 실제로 진행시켜 테스트하는 방식입니다.

 

GitHub - wix/Detox: Gray box end-to-end testing and automation framework for mobile apps

Gray box end-to-end testing and automation framework for mobile apps - GitHub - wix/Detox: Gray box end-to-end testing and automation framework for mobile apps

github.com

 

 

 

 

Detox 환경세팅

이미 React Native 개발 환경 구축이 되었다는 전제하에 아래 세팅을 진행하셔야 합니다.

 

 

 

Global 개발 환경세팅

npm install detox-cli --global

// [MacOS Only] Detox가 ios 시뮬레이터에서 작동하기 위한 설치
brew tap wix/brew
brew install applesimutils

 

 

 

JDK 버전 11.x 이상으로 업데이트 필요

 

M1 Monterey Java 11.0.13 설치 후 환경변수 설정하기 — DevMinGeonPark

M1 Mac에서 Java 11.0.13 버전을 설치하고 환경변수를 설정하는 방법을 정리한 글입니다. Zulu OpenJDK를 활용하여 설치하며, 환경변수 설정에 대한 설명과 함께 실행 결과도 확인할 수 있습니다.

blex.me

터미널에서 Java --version 했을 때 버전이 11.x 이상으로 나와야 합니다. 이유로는 Android에서 Java 버전이 낮을 경우 Detox가 세팅되어 있다면 빌드 실패 이슈가 있습니다.

 

Detox가 JDK 11.x 이상으로 작동한다고 보시면 됩니다.

 

 

 

 

 

Xcode Excludes Architectures 확인(m1 mac일 경우)

m1 mac은 Xcode 프로젝트 오픈 > TARGETS {project name} > Excludes Architectures가 arm64가 맞는지 확인해야 합니다.

 

 

 

 

 

 

적용할 프로젝트 개발 환경세팅

 

Project Setup | Detox

This article mainly covers standard React Native projects.

wix.github.io

Detox 공식 사 아트에 React Native 프로젝트 환경세팅이 친절하게 설명되어 있으니 차근차근 참고하여 세팅합니다.

 

 

 

위의 세팅들이 끝나고 빌드가 성공하면 세팅 완료입니다

 

 

공식 GitHub에 RN 프로젝트 세팅 예시가 있으니 참고하시면 더 정확히 세팅을 확인할 수 있습니다.
https://github.com/wix/Detox/tree/master/examples/demo-react-native

 

 

 

 

 

 

Detox 테스트해보기

 

 

root 디렉터리에 .detoxrc.js 파일을 만들고 안에다가 시뮬레이터/애뮬레이터 작동 디바이스 모델명 설정하실수 있습니다.

 

 

 

 

 

 

 

root 디렉토리 e2e폴더를 생성하고 안에서 테스트 코드를 작성할 수 있습니다.

 

테스트 코드 파일명은 뒤에. test가 붙어야 합니다.

 

 

 

 

첫 번째로 도입시킨 테스트 시나리오는 가장 중요한 결제 플로우 시나리오 테스트였습니다.

 

아래와 같이 테스트 코드를 작성하고 실행시키면 시뮬레이터가 작동하고 알아서 테스트 코드 작성한 대로 테스트가 진행됩니다.

(참고 >> 테스트 코드 안에서 tryCatch 구문으로 테스트 메서드가 실패하더라도 분기처리를 할 수 있습니다.)

 

 

purchase.test.js

import {by, device, element, expect} from 'detox';

describe('시술 상품 구매 테스트', () => {
  beforeAll(async () => {
    await device.launchApp({
      newInstance: true,
      permissions: {notifications: 'YES', userTracking: 'YES'},
    });
  });

  afterAll(async () => {
    await device.terminateApp();
  });

  it('시술 상품 구매를 위해 시술 상품 상세 페이지로 이동 할 수 있는가?', async () => {
    try {
      await expect(element(by.id('main-popup-close-btn'))).toExist();
      await element(by.id('main-popup-close-btn')).tap();
      //-----------------------메인 모달이 있을때-----------------------//
      try {
        await expect(
          element(by.id('review-induction-popup-close-btn')),
        ).toExist();
        await element(by.id('review-induction-popup-close-btn')).tap();
        //-----------------------후기 유도 모달이 있을때-----------------------//
        // 3. 검색 페이지 이동
        await expect(element(by.id('search-screen-btn'))).toExist();
        await element(by.id('search-screen-btn')).tap();
        // 4. 시술 상품 검색
        await expect(element(by.id('search-form-text'))).toExist();
        await element(by.id('search-form-text')).typeText('Laser');
        await waitFor(element(by.id('search-form-text')))
          .toHaveText('Laser')
          .withTimeout(2000);
        await element(by.id('search-form-text')).tapReturnKey();
        // 5. 검색한 시술 상품 클릭후 상세 페이지 이동
        await waitFor(element(by.id('search-product-item-0')))
          .toExist()
          .withTimeout(2000);
        await element(by.id('search-product-item-0')).tap();
      } catch (error) {
        //-----------------------후기 유도 모달이 없을때-----------------------//
        // 3. 검색 페이지 이동
        await expect(element(by.id('search-screen-btn'))).toExist();
        await element(by.id('search-screen-btn')).tap();
        // 4. 시술 상품 검색
        await expect(element(by.id('search-form-text'))).toExist();
        await element(by.id('search-form-text')).typeText('Laser');
        await waitFor(element(by.id('search-form-text')))
          .toHaveText('Laser')
          .withTimeout(2000);
        await element(by.id('search-form-text')).tapReturnKey();
        // 5. 검색한 시술 상품 클릭후 상세 페이지 이동
        await waitFor(element(by.id('search-product-item-0')))
          .toExist()
          .withTimeout(2000);
        await element(by.id('search-product-item-0')).tap();
      }
    } catch (error) {
      //-----------------------메인 모달이 없을때-----------------------//
      try {
        await expect(
          element(by.id('review-induction-popup-close-btn')),
        ).toExist();
        await element(by.id('review-induction-popup-close-btn')).tap();
        //-----------------------후기 유도 모달이 있을때-----------------------//
        // 3. 검색 페이지 이동
        await expect(element(by.id('search-screen-btn'))).toExist();
        await element(by.id('search-screen-btn')).tap();
        // 4. 시술 상품 검색
        await expect(element(by.id('search-form-text'))).toExist();
        await element(by.id('search-form-text')).typeText('Laser');
        await waitFor(element(by.id('search-form-text')))
          .toHaveText('Laser')
          .withTimeout(2000);
        await element(by.id('search-form-text')).tapReturnKey();
        // 5. 검색한 시술 상품 클릭후 상세 페이지 이동
        await waitFor(element(by.id('search-product-item-0')))
          .toExist()
          .withTimeout(2000);
        await element(by.id('search-product-item-0')).tap();
      } catch (error) {
        //-----------------------후기 유도 모달이 없을때-----------------------//
        // 3. 검색 페이지 이동
        await expect(element(by.id('search-screen-btn'))).toExist();
        await element(by.id('search-screen-btn')).tap();
        // 4. 시술 상품 검색
        await expect(element(by.id('search-form-text'))).toExist();
        await element(by.id('search-form-text')).typeText('Laser');
        await waitFor(element(by.id('search-form-text')))
          .toHaveText('Laser')
          .withTimeout(2000);
        await element(by.id('search-form-text')).tapReturnKey();
        // 5. 검색한 시술 상품 클릭후 상세 페이지 이동
        await waitFor(element(by.id('search-product-item-0')))
          .toExist()
          .withTimeout(2000);
        await element(by.id('search-product-item-0')).tap();
      }
    }
  });

  it('상품 선택 모달창을 열고 상품을 선택하고 예약 페이지로 이동 할 수 있는가?', async () => {
    // 1. 구매 예약 하려는 시술 상품 모달 열기 버튼 클릭
    await waitFor(element(by.id('product-purchase-selection-open-modal-btn')))
      .toExist()
      .withTimeout(3000);
    await element(by.id('product-purchase-selection-open-modal-btn')).tap();
    // 2. 첫번째 상품 클릭 및 선택 이벤트 기능 확인
    await expect(element(by.id('select-product-0'))).toExist();
    await element(by.id('select-product-0')).multiTap(2);
    // 3. 선택한 상품을 예약하려는 예약 페이지 이동 버튼 클릭
    await expect(element(by.id('confirmation-select-product-btn'))).toExist();
    await element(by.id('confirmation-select-product-btn')).tap();
  });

  it('시술 예약 필수 정보 입력이 가능하고 다음 구매 페이지로 넘어갈 수 있는가?', async () => {
    // 1. 예약 날짜 선택 모달 열기
    await expect(element(by.id('open-modal-scheduled-calendar-btn'))).toExist();
    await element(by.id('open-modal-scheduled-calendar-btn')).tap();
    // 2. 예약 날짜 달력에서 클릭
    await expect(element(by.id('select-scheduled-date'))).toExist();
    await element(by.id('select-scheduled-date')).tap();
    // 3. 예약 시간 선택 모달에서 시간 선택
    await element(by.id('date-and-time-reservation-modal-scrollview')).scrollTo(
      'bottom',
    );
    await expect(
      element(by.id('open-modal-scheduled-time-picker-btn')),
    ).toExist();
    await element(by.id('open-modal-scheduled-time-picker-btn')).tap();
    await expect(element(by.id('confirmation-scheduled-time-btn'))).toExist();
    await element(by.id('confirmation-scheduled-time-btn')).tap();
    // 4. 예약 날짜 시간 확정 버튼 클릭
    await expect(element(by.id('confirmation-scheduled-date-btn'))).toExist();
    await element(by.id('confirmation-scheduled-date-btn')).tap();
    // 5. 인원수 카운트 버튼 클릭
    await element(by.id('reservation-scrollview')).scrollTo('bottom');
    await expect(element(by.id('plus-persons-count-btn'))).toExist();
    await element(by.id('plus-persons-count-btn')).tap();
    // 6. 구매 페이지 이동 버튼 클릭
    await expect(element(by.id('confirmation-reservation-info-btn'))).toExist();
    await element(by.id('confirmation-reservation-info-btn')).tap();
  });

  it('구매 페이지에서 최종 구매 버튼을 클릭하고 구매를 위한 결제창이 등장하는가?', async () => {
    // 1. 구매 결제 요청 버튼 클릭
    await element(by.id('purchase-scrollview')).scrollTo('bottom');
    await expect(element(by.id('purchase-payment-request-btn'))).toExist();
    await element(by.id('purchase-payment-request-btn')).tap();
    // 2. 구매 요청 200 성공후 구매 결제창 웹뷰 등장 체크
    await waitFor(element(by.id('purchase-webview')))
      .toExist()
      .withTimeout(10000);
  });
});

 

 

 

 

구매하기 테스트 시나리오 아래와 같이 흘러갑니다.

 

시상품 구매하기 테스트 시나리오

  1. 개발용 시뮬레이터 작동
  2. 앱 실행
  3. 스테이징 모드로 변경
  4. 테스트용 회원 로그인
  5. 검색 페이지 이동
  6. 시술 상품 검색
  7. 검색하여 등장한 시술 상품의 상세 페이지로 이동
  8. 상품 상세 페이지에서 구매 상품 리스트 모달 열고 상품 선택 후 예약 페이지 이동
  9. 상품 예약 페이지에서 예약 필수 정보 입력 후 구매 페이지 이동
  10. 구매 페이지에서 구매 결제 요청 버튼 클릭
  11. 결제창 웹뷰 등장
  12. 상품 구매 테스트 성공
  13. 앱 종료

 




더 다 세한 Detox의 테스트 코드 작성은 여기 참고

 

Device | Detox

The device object is globally available in every test file, unless you use exposeGlobals: false in the behavior config,

wix.github.io

 

 

 

 

 

 

 

테스트 실행 스크립트 작성

 

 

Build

detox build --configuration ios.sim.debug //IOS
detox build --configuration android.emu.debug //AOS

Detox의 원리는 테스트용 앱 apk을 따로 build 하여 시뮬레이터/에뮬레이터 테스트 실행하는 것이어서 e2e테스트 실행 전 최신 프로젝트 코드로 build를 해줘야 합니다.

 

 

 

 

Test

detox test --configuration ios.sim.debug //IOS
detox test --configuration android.emu.debug //AOS

Detox 테스트용 build가 끝나면 프로젝트 root 디렉터리 터미널에서 위의 명령어를 실행시키면 시뮬레이터/에뮬레이터가 실행되며 자동으로 테스트가 진행됩니다.

 

 

 

 

 

저희 서비스는 아래와 같은 플로우로 테스트 및 배포를 진행합니다.

E2E용 Build -> 시뮬레이터 실행 -> E2E 자동 테스트 -> 테스트 Success -> Fastlane 자동 배포 명령실행

 

 

 

설정해 둔 E2E 테스트가 통과가 되어야 Fastlane 배포가 실행되도록 스크립트 명령어를 설정해 두었습니다.

 

 

 

혹시 따로 테스트용으로만 실행할 수 있느니 따로 빌드/테스트 실행을 할 수 있도록 스크립트 명령어를 빼두었습니다.

 

 

 

 

 

 

 

처음 환경 세팅이 조금 번거롭지만 마음 잡고 한번 테스트 코드 시나리오를 세팅해 두면 더 안전하게 서비스를 운영/개발하는데 도움이 많이 된다는 것을 느꼈습니다.

 

 

감사합니다!

 

 

 

 

반응형

댓글