오늘 배운 것

이전에 작업하다가 미뤄두었던 RN-Django e2e test 이슈를 다시 가져와보았다. 매번 앱을 켜고 잘 동작하는지 확인하는 과정을 e2e test로 대신할 수 있기 때문에, 이 작업이 많이 필요하다고 느꼈고 이것만 잘 구축된다면 제법 테스트하기 쉬운 앱이 될 것 같았다.

 

기존에 사용하려던 라이브러리는 detox였다. 이 라이브러리는 여러 가지 시나리오를 구체적으로 테스트할 수 있다는 장점이 있어서 이걸 써보고 싶었다. 그러나 현재 프로젝트에서는 react native 위에 expo를 사용하고 있는데, detox 공식문서에서는 공식적으로 expo와 같이 사용하기 위한 별도의 지원을 하고 있지는 않다고 했다. 즉 만약 오류가 발생하면 알아서 해결해야 한다는 것이다. 

 

그래서 고민했던 다른 옵션이 maestro였다. maestro는 detox와 달리 yaml 파일로 테스트 코드를 작성하며, 간편하게 작성할 수 있다는 장점이 있어 보였다. 그러나 detox와 달리 세부적인 케이스에 대해서 테스트를 하는 부분에서는 좀 약점이 있었다. 그리고 maestro로 테스트를 하려면 npm으로 패키지를 다운받는 게 아니라 CLI를 사용해야 한다는 것이 마음에 안 들었다. 

 

하지만 마음에 안 든다고 expo dependency를 이겨내면서 detox로 테스트하기에는 괜한 시간을 쓰는 것 같았기에, 일단 maestro로 진행해 보기로 했다. 

 

우선 아래와 같은 명령어로 CLI(iterm)에 maestro를 설치해 주자.

brew tap mobile-dev-inc/tap
brew install maestro

 

'maestro test'를 입력하면 CLI에서 자동으로 로컬에서 실행 중인 에뮬레이터를 찾는다고 한다. 

 

그렇다면 .yaml 파일은 어디에다 작성해야 할까? 'maestro test' 명령어 뒤에 테스트하고 싶은 yaml 파일의 경로를 입력하면 된다고 한다. 우리 프로젝트의 경우, 테스트 폴더에다 별도로 모든 파일들을 넣어두어야 할지, 아니면 각 view나 component마다 test 파일을 두어야 할지 고민이다. 이 부분은 팀원들에게 물어보고 결정해야겠다. 

 

그리고 구글링하다보니 도움이 되는 또 다른 글을 발견했다! react native를 사용해서 maestro로 end to end test를 하는 글이었다. 

 

빼먹은 부분이 있었는데, CLI에서 maestro 명령어를 자유롭게 사용하기 위해서 환경변수 설정을 해 주어야 했다. '.zshrc' 파일에 다음과 같은 명령어를 추가해 주자. 

export PATH="$PATH":"$HOME/.maestro/bin"

 

그리고 잘 동작하는지 확인해 보자. 우선 가장 간단하게는 로그인 화면을 테스트해볼 수 있겠다. 버튼을 누르면 구글 폼이 나타나는지를 테스트 해 보자. 그런데 아직 maestro yaml 파일 문법에 익숙하지 않아서, GPT 찬스로 예제 코드를 짜 달라고 했다. 

 

그리고 생각해보니 구글 로그인에 써 있는 텍스트가 다국어 버전별로 달랐다. 그래서 테스트할 때 다국어 상황에서 언어를 판단해서 특정 텍스트를 감지할 수 있는지도 궁금했다. 

이 부분은 로그인 버튼에 별도의 ID를 적용함으로써 알 수 있다고 했다. testID를 사용하면 버튼의 텍스트나 속성 등이 조금씩 달라지더라도 각 버튼을 고유하게 식별할 수 있다고 한다. 현재 앱에는 다국어 지원이 적용된 상황이라 텍스트만으로 버튼을 판단하기는 어려울 것 같아서, 해당 방법을 사용하기로 했다. 

 

그런데 구체적으로 어떻게 하는지를 보니 testID를 적용하려면 코드나 컴포넌트에 testID라는 속성을 하나씩 추가해 주어야 한다. 이러면 테스트 코드를 위해서 코드를 또 다 바꿔줘야 한다... 이게 맞을까? 어떻게 하면 코드의 변경을 최소화하면서 테스트 코드를 작성할 수 있을까?

 

여러 가지 방법이 있었다. 두 가지로 나눌 수 있겠다. 

  1. HOC(high order component)를 통해 코드의 변경을 최소화하면서 testId 적용하기
  2. 계층 구조를 사용해서 testID나 text를 사용하지 않고, 예를 들면 'container의 0번째 자식 컴포넌트' 이런 식으로 지정할 수도 있다고 한다. 

1번을 봤더니 여전히 코드에 손을 대야 하는 부분이 있었다. 가령 이런 식이었다. 

import React from 'react';
import { Button } from 'react-native';

const withTestID = (WrappedComponent, testID) => {
  return (props) => {
    return <WrappedComponent {...props} testID={testID} />;
  };
};

const GoogleLoginButton = withTestID(Button, 'googleLoginButton');

const LoginScreen = () => {
  return (
    <GoogleLoginButton title="Login with Google" onPress={() => {}} />
  );
};

export default LoginScreen;

 

HOC를 사용하는 방법의 경우, withTestID라는 함수를 통해 WrappedComponent를 변환해서 로직을 좀 통일해 주기는 했지만 여전히 테스트를 위해서 일반 로직에 손을 대야 하는 건 똑같았다. 그렇다고 계층 구조를 사용하자니, 만약 화면이나 계층 구조가 변화한다면 그에 맞춰서 테스트 코드도 수정해 주어야 했다. testID처럼 고유하게 컴포넌트를 식별하고는 싶지만, 그걸 일일이 앱 코드에다가 testID를 넣어서 주입해 주고 싶지는 않았다. 

 

다시 GPT 찬스를 써 보았다. 코드에 직접 testID라는 속성을 추가하지 않으면서도, 테스트 런타임에서만 해당 값을 추가해줄 수 있는 방법이 있었다. 바로 useEffect나 componentDidMount 훅을 사용해서, 테스트 환경에서 실행될 경우에만 testID 값을 추가해주는 방식이었다. 코드의 속성을 수작업으로 편집하지 않아서 괜찮은 방법 같았다. 

 

다만 여전히 많은 컴포넌트들이 있어서 이 함수를 하나씩 추가해야 한다는 점에서는 부담이 되었다. 문득 여러 규모가 있는 기업들은 이런 테스트를 어떻게 하고 있을지 궁금했다. 토스의 FE 아티클 하나를 참고해보니 위에서 잠깐 고민했던 것처럼 TestID는 어쨌거나 코드에 직접 주입을 해 주어야 하기 때문에 단점이 있다는 내용이 있었다. 

 

그리고 찾다 보니 토스페이먼츠 노션에서 어제 궁금한 점으로 적었던 mock과 stub의 차이점도 알 수 있었다. mock은 객체가 내부에서 외부로 나갈 때 상호작용을 모방하는 객체이고, stub은 외부에서 내부로 들어오는 객체의 상호작용을 모방하는 객체라고 한다. 예를 들면 mock은 이메일을 발송하는 등의 작업이고, stub은 데이터베이스에서 더미 데이터를 입력으로 받아오는 등의 작업이라고 한다. 

 

그러면 어떻게 테스트를 짜야 맞을까? 

 

다시 생각해보니 다국어 지원은 어쨌든 언어만 달라지는 것이므로 이 상황에서 크게 고려할 점은 아니었다. 만약 언어가 통일된다면, 기존과 같이 텍스트 방식으로 일단은 테스트를 작성해 보는것도 나쁘지 않겠다. 그러다가 추후에 변경을 해도 되겠다. 현재 i18n의 기본 설정으로는 fallbackLng(변환 실패했을 때의 기본 언어)가 영어로 되어 있어서, 영어를 기준으로 텍스트를 작성해주면 되겠다. 

 

아주아주 간단한 테스트를 작성해봤다. 잘 돌아가는지를 테스트 해 보려고 한다.

// indexTest.yaml
appId: com.safezone.onestep
---
# 앱을 실행
- launchApp

# 로그인 버튼을 찾고 클릭
- tapOn:
    text: 'Sign in with Google'

# Google 로그인 폼이 나타나는지 확인
- assertVisible:
    text: 'onestep'
maestro test indexTest.yaml

 

다음과 같은 명령어를 실행했더니, 이런 화면이 나왔다. 앱을 시작하는 것은 성공했으나 'Sign in with Google'이 들어간 텍스트를 찾지 못한 것 같았다. 

 

파란색 디렉토리로 가서 스크린샷을 확인해 보았다. 알고보니 앱을 런칭하면 바로 로그인 화면이 뜨는 게 아니라 다음과 같은 화면이 떠서, 'Sign in with Google' 텍스트를 찾지 못한 것 같았다. 

 

이 경우는 어떻게 해야 할지 고민이다. 일단 GPT 찬스를 써 보았다. 

 

이 부분은 Expo Go를 사용하는 것과 연관이 있다고 한다. 이 문제를 방지하려면 launchApp 명령어를 사용하기보다는 링크를 통해 직접 앱을 열어야 한다. 그런데 링크를 또 어떻게 찾나... GPT 피셜 exp://172.x.x.x 이런 링크를 찾으라는데, 아무리 봐도 콘솔에 그런 링크가 안 떴다. 

 

그래서 나름의 편법을 발견했는데, 바로 앱을 띄워두고 터미널을 하나 더 열어서 maestro로 테스트를 해 보았다. 그랬더니 아까는 fail했던 테스트가 잘 되더라. 

 

위의 코드로는 로그인 프로세스가 완료되지는 않았다. 아래와 같이 코드를 더 추가해서 진행해 주었고, 성공한 사진을 볼 수 있었다. 이제 이 테스트 코드를 로직의 첫 번째에 실행시켜서 나머지 시나리오들도 테스트해보면 되겠다. 

appId: com.safezone.onestep
---
# 앱을 실행
- launchApp

# 로그인 버튼을 찾고 클릭
- tapOn:
    text: 'Sign in with Google'

# Google 로그인 폼이 나타나는지 확인
- assertVisible:
    text: 'onestep'

# 로그인 폼이 나타나면 구글 계정 클릭
- tapOn:
    text: '.*gmail.com'
#
- tapOn:
    text: '계속'

- assertVisible:
    text: 'Today'
    waitUntilVisible: true

 

 궁금한 점

  1. componentDidMount와 useEffect 훅의 차이점은 무엇일까? 

 

+ Recent posts