봄날은 갔다. 이제 그 정신으로 공부하자

UI Test 적용하기 본문

학습

UI Test 적용하기

길재의 그 정신으로 공부하자 2020. 11. 25. 14:14

해당 글은 앱(android) UI 테스트 기능을 적용하면서 습득한 지식을 바탕으로 작성한 글입니다.

이전 글은 여기서 확인...

als2019.tistory.com/13

 

우선 UNIT Test부터 적용하기

이 글은 현재 재직중인 회사의 앱에 UNIT 테스트를 적용한 후기 글 입니다. 이전 글을 읽지 않으신 분은 이전 글로... als2019.tistory.com/12 테스트 자동화 어떻게 만들까? Unit 테스트를 시작하기 전

als2019.tistory.com

 

UI 단위로 단위 테스트를 하기보다는 앱에서 지원하는 모든 정책과 기능을 시나리오 관점에서 테스트하는 UI 시나리오 테스트를 작성하여 앱에 적용하였습니다.

현재까지 작성된 시나리오는 83개 이며 테스트 시간은 30분 가량 소요됩니다.

 

기능별로 테스트 시나리오를 만들고 테스트를 하기 때문에 빠른 테스트 시나리오 작성을 위해 새로운 테스트 구조인  Movie 구조를 만들었습니다. (자세한 이야기는 후술됩니다...)

 

Espresso

UI 테스트를하기 위해서는 테스트에 필요한 Espresso 라이브러리를 앱의 build.gradle에 아래와 같이 추가해야 합니다.

androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.0'
androidTestImplementation('androidx.test.espresso:espresso-contrib:3.1.0') {
    exclude group: 'com.android.support', module: 'appcompat'
    exclude group: 'com.android.support', module: 'support-v4'
    exclude module: 'recyclerview-v7'
}

UI 테스트는 UNIT 테스트와 유사한점이 많지만 기본적으로 화면을 띄워 화면의 유효성을 검사하는 테스트이므로 차이점도 많습니다. 기본적인 Espresso 라이브러리 사용법은 아래와 같습니다.

Espresso + 샷 추가

위 표와 같이 기본적인 Espresso 명령어만 있으면 테스트에 큰 문제 없이 대부분의 내용은 테스트 가능합니다.

하지만 비동기 처리, custom widget, layout이 아닌 코드에서 직접 widget을 추가하는 경우에 대한 처리에 대응하기 위해서는 기본적인 함수외에 아래와 같은 추가 작업이 필요합니다.

 

비동기 처리를 위한 Sleep

비동기적으로 서버와 통신을 하는 로직이 있는 경우(화면 전환 시), 바로 다음 command를 호출하면 오동작이 발생하므로

충분한 시간의 sleep을 준 후, 다음 command를 호출하도록 해야 합니다.

Sleep 처리는 UI 테스트 시 의외로 자주 호출되므로 재사용을 간편하게 하기 위해 아래와 같이 별도의 함수로 만들어 줍니다.

public void zzzzz(int timeout){
    try {
        Thread.sleep(timeout);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}

 

Custom Tab widget

android에서 자주 사용되는 것이 tab layout인데 tab의 갯수 및 이름이 layout xml이 아닌 코드 상에서 동적으로 추가되다보니 아래와 같은 추가 작업이 필요합니다.

onView(withId(R.id.tabs).perform(selectTabAtPosition(0));

위 명령어는 "R.id.tabs"라는 id를 가진 tab layout의 0번째 탭을 선택하라는 명령어 입니다.

여기에서 눈여겨 보아야 할 부분은 "selectTabAtPosition()" 함수인데 해당 함수는 Espresso에서 지원해주는 함수가 아닌 직접 정의한 함수 입니다. 해당 함수의 내용은 아래와 같습니다.

public ViewAction selectTabAtPosition(final int position) {
    return new ViewAction() {
        @Override
        public Matcher<View> getConstraints() {
            return allOf(isDisplayed(), isAssignableFrom(PagerSlidingTabStrip.class));
        }

        @Override
        public String getDescription() {
            return "with tab at index" + String.valueOf(position);
        }

        @Override
        public void perform(UiController uiController, View view) {
            if (view instanceof PagerSlidingTabStrip) {
                PagerSlidingTabStrip tabLayout = (PagerSlidingTabStrip) view;
                tabLayout.getViewPager().setCurrentItem(position);
            }
        }
    };
}

위 함수의 핵심 부분은 perform() 함수 입니다.

perform() 함수 내용을 보면 매개변수로 받은 view instance가 Tab instance가 맞는지 확인하고 맞는 경우, type casting을 통해 tab instance로 변경한 후, 실행을 원하는 함수를 직접적으로 호출해줍니다.

 

Custom + 동적 추가 ListView widget

식권앱은 android에서 지원하는 RecyclerView를 사용하지 않고 별도의 Custom ListView를 사용하고 있으며,

또한 Custom ListView 선언을 layout xml이 아닌 코드에서 동적으로 추가하는 방식이라 ID가 없습니다.

추가로 ListView 아이템 선택 방식이 아닌 아이템의 특정 영역을 선택하는 경우 화면 전환이 이루어지도록 되어 있어 리스트 아이템 클릭에 대해서도 추가 작업이 필요합니다.

onView(allOf(withTagValue(is((Object)"MyHistoryList")), isDisplayed())).perform(clickHistoryListItem(0));

Custom ListView widget instance가 코드에서 동적으로 추가되는 부분에서 setTag() 함수를 사용해서 "MyHistoryList"를 추가합니다. 그럼 위와 같이 withTagValue()를 사용해서 해당 ListView를 찾을 수 있습니다.

그다음에 "clickHistoryListItem()" 함수를 통해 아이템 클릭을 처리합니다.

"clickHistoryListItem()" 함수는 아래와 같습니다.

public ViewAction clickHistoryListItem(final int position){
    return new ViewAction() {
        @Override
        public Matcher<View> getConstraints() {
            return allOf(isDisplayed(), isAssignableFrom(PullToRefreshListView.class));
        }

        @Override
        public String getDescription() {
            return "with listview at index" + String.valueOf(position);
        }

        @Override
        public void perform(UiController uiController, View view) {
            if (view instanceof PullToRefreshListView) {
                PullToRefreshListView list = (PullToRefreshListView) view;
                PlanningHistoryAdapter adapter = (PlanningHistoryAdapter)list.getTestableAdapter();
                adapter.onClickItem(position);
            } 
        }
    };
}

perform() 함수를 보면 view Instance를 Custom ListView인 PullToRefreshListView의 Instance로 변경한 후

getTestableAdapter()함수를 통해 Adapter를 가져온 후 onClickItem()함수를 호출해 아이템 클릭을 처리합니다.

눈치 빠른 분은 알겠지만 getTestableAdapter()함수와 onClickItem()함수는 테스트를 위해 정의한 함수 입니다.

 

UI Test 구조 설계

이번 UI Test를 계속 만들다보니 테스트 시나리오를 만들고 동작하는 것을 보니 한편의 영화를 보는 것 같아 UI Test 모듈을 영화 상영을 중심으로 설계하였습니다.

영화 상영에는 "영화가 담긴 필름(Movie)"과 "영사기(Projector)" 그리고 "영화관(Theater)"이 필요합니다.

각 모듈별 설명은 아래와 같습니다.

사용자가 Movie(비지니스 로직)를 만들고 이렇게 만들어진 Movie를 극장(Theater)에서 영사기(Projector)를 통해 상영하는 방식으로 동작합니다. 실행되는 코드는 아래와 같습니다.

 

Movie

영화를 만들기 위해 Film에 영화의 샷을 하나씩 추가합니다.

public class MovieMainScreenAction {
    public ArrayList<Film> getFilm(String id, String password){
        ArrayList<Film> roll = new ArrayList<>();
        roll.add(new Film(Film.ACTION_SLEEP_SHORT));
        roll.add(new Film(Film.ACTION_CHECK, new int[]{R.id.login_id, R.id.login_password, R.id.login}));
        roll.add(new Film(Film.ACTION_CHECK, new String[]{"비밀번호를 잊으셨나요?"}));
        roll.add(new Film(Film.ACTION_INPUT, R.id.login_id, id));
        roll.add(new Film(Film.ACTION_INPUT, R.id.login_password, password));
        roll.add(new Film(Film.ACTION_SLEEP_SHORT));
        roll.add(new Film(Film.ACTION_CLICK, R.id.login));
        roll.add(new Film(Film.ACTION_SLEEP_LONG));
        ...

    }
}

 

Projector

영사기에 영화 Film을 걸고(crankIn)하고 영화를 상영(play)합니다.

public class MovieProjector extends BaseProjector{
    ArrayList<Film> roll = new ArrayList<>();

    public MovieProjector crankIn(ArrayList<Film> roll){
        this.roll = roll;
        return this;
    }

    public void play(){
        for(Film film : roll) {
            ...
        }
    }
}

 

Theater

극장을 찾은 사용자에게 영사기(Projector)를 사용해 영화(Movie)를 상영합니다.

@RunWith(JUnit4.class)
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
public class UiTestTheater {
    @Test(timeout = 300000)
    public void MyFirstMovie() {
        activityRule.launchActivity(new Intent());
        MovieMealCardAction movie = new MovieMealCardAction(); // 1. 영화를 만들고
        ArrayList<Film> roll = movie.getFilm("01014050301", "0301"); // 2. 만들어진 영화의 필름을 가져온 후 
        MovieProjector projector = new MovieProjector(); // 3. 영사기를 준비한 다음,
        projector.crankIn(roll); // 4. 영사기에 영화 필름을 걸어준 후,
        projector.play(); // 5. 영화 상영을 시작함.
    }
}
Comments