본문 바로가기

안드로이드

Android Espresso #4 - ActivityRules

반응형

이번에는 UI 테스트에서 사용하는 ActivityRule 에 대해서 설명할 것이다.

 

Rule 에는 ActivityTestRule, ActivityScenarioRule 두 가지가 존재한다.
또, Rule 없이 사용하는 방법으로 ActivityScenario 가 존재한다.

구글은 ActivityScenarioRuleActivityScenario 를 쓰기를 권장하고 있으니 해당 부분만 보아도 된다.

TestRule

우선 Rule의 동작은 간단하다.

 

@Before 단계 이전에 실행되며
@After 단계가 끝난 후 종료된다.

 

@Before, @After 가 없으면 모든 @Test의 시작과 끝에 맞춰 실행된다.

 

ActivityRule의 경우로 보면

 

@Before 단계 이전에 액티비티 Rule이 생성되고
@After 단계가 끝난 후 액티비티 Rule이 종료된다.

 

중요한 건 액티비티 Rule이 실행, 종료되는 거지 액티비티가 그런 것이 아니다.
액티비티는 매 @Test 마다 새롭게 시작하고 종료된다.

ActivityTestRule

가장 기본적인 Rule이다.

생성자

@get:Rule
var activityTestRule: ActivityTestRule<MainActivity> =
    ActivityTestRule(MainActivity::class.java)

대상 액티비티만 지정하는 생성자이다.
상단에 @get:Rule 어노테이션을 통해서 해당 Rule을 사용하도록 지정해야 한다.

@get:Rule
var activityTestRule: ActivityTestRule<MainActivity> =
    ActivityTestRule(MainActivity::class.java, false, false)

대상 액티비티, 터치 모드, 액티비티 자동 실행 여부를 지정하는 생성자이다.

 

실행 여부는 true 가 기본값이다.
false로 지정 시 직접 액티비티를 실행시켜주어야 한다. 해당 코드는 뒤에 설명하겠다.

@get:Rule
var activityTestRule: ActivityTestRule<MainActivity> =
    ActivityTestRule(
        MainActivity::class.java,
        "com.onetwothree.espressosample",
        Intent.FLAG_ACTIVITY_NEW_TASK,
        false,
        false
    )

대상 액티비티, 패키지명, 실행 플래그, 터치 모드, 액티비티 자동 실행 여부를 지정하는 생성자이다.

 

패키지명은 해당 액티비티의 패키지명을 넣으면 된다. ( 굳이 왜 생성자로 있는지는 모르겠다. )

사용

ActivityTestRule에서 지원하는 기능은 몇 가지 없다.
실행, ui 스레드 사용, 종료, 액티비티 가져오기, 결과 가져오기 정도이다.

@Before
fun setupData() {
    //1
    activityTestRule.launchActivity(Intent()
        .apply { putExtra("MyArg", "Nothing") })

    //2
    activityTestRule.runOnUiThread {
        //3
        activityTestRule.activity.run{
            ... findViewById 등등 ...
        }
    }

}

1.

위에서 액티비티 자동 실행을 false로 지정했다면,
launchActivity 를 통해서 직접 액티비티를 실행시켜 주어야 한다.

 

인자로는 Intent를 넘겨야 하며, 필요시에는 데이터를 채워주면 된다.
당연하게도 자동 실행이 true 인 상태에서도 호출해도 된다.
새로운 액티비티가 다시 열릴 뿐이다.

 

2.

runOnUiThread 함수를 통해 ui thread에 접근할 수 도 있다.

 

3.

다음으로, 액티비티에 접근하려면 activity 변수를 사용하면 된다.

위의 예제는 액티비티에 접근하여 뷰를 가져오는 ( findViewById ) 작업을 하는 코드이다.

결과 가져오기

@Test
fun resultTest(){
    //1
    activityTestRule.activity.setResult(Activity.RESULT_OK,Intent().apply {
        putExtra("Result", "Ok")
    })

    //2
    activityTestRule.finishActivity()

    //3
    Assert.assertEquals(activityTestRule.activityResult.resultCode,Activity.RESULT_OK)
    val result = activityTestRule.activityResult.resultData.extras?.getString("Result")
    Assert.assertEquals(result, "Ok")
}

위의 코드는 액티 비트의 결과를 가져오는 코드이다.

 

1.

테스트를 위해서 결과를 지정하였다.

 

2.

결과를 가져오려면 일단, 해당 액티비티를 종료시켜야 한다. ( 실제 과정이랑 동일하게 생각하면 된다.. )

 

3.

activityResult 변수를 통해서 결과에 접근할 수 있다.
해당 변수는 resultCode, resultData 를 값으로 가지고 있다.

 

위의 코드처럼 값을 비교하여 결괏값에 대해서 테스트를 진행하면 된다.

ActivityScenario

ActivityScenarioRule 을 보기 전에 ActivityScenario 을 먼저 보도록 하겠다.


ActivityScenario 은 lifeCycle을 마음대로 바꾸면서 테스트할 수 있도록 해준다.
재생성, 현재 상태 확인, 라이프사이클 이동, 결과 가져오기 등이 가능하다.

 

ActivityTestRule 의 예제와 동일한 코드를 통해 차이를 알아보자.

생성 & 사용

lateinit var activityScenario: ActivityScenario<MainActivity>

@Before
fun setupData() {

    //1.
    //activityScenario = ActivityScenario.launch(MainActivity::class.java)

    //2.
    activityScenario = ActivityScenario.launch<MainActivity>(
        Intent(
            ApplicationProvider.getApplicationContext(),
            MainActivity::class.java
        ).apply {
            putExtra("MyArgs", "Nothing")
        })

    //3.
    activityScenario.onActivity {
        (it.findViewById<RecyclerView>(R.id.recyclerView))?.let {
            ...
        }
    }
}

Rule 이 아니기 때문에 어노테이션은 필요가 없고,
대신 매번 액티비티를 실행해주는 과정이 필수로 필요하다. 

 

1.
launch 에 실행할 액티비티를 넣어주는 방식이다.
만들어진 시나리오를 Test에서 사용하고 싶다면 변수로 들고 있으면 된다.

 

2.
Intent를 이용하여 생성하는 방식이다.

 

3.
액티비티에 접근하려면 onActivity 함수를 이용하면 된다.

결과 가져오기

@Test
fun resultTest() {

    //1
    activityScenario.onActivity {
        it.setResult(Activity.RESULT_OK, Intent().apply {
            putExtra("Result", "Ok")
        })

        //2
        it.finish()
    }

    //3
    Assert.assertEquals(activityScenario.result.resultCode, Activity.RESULT_OK)
       val result = activityScenario.result.resultData?.extras?.getString("Result")
       Assert.assertEquals(result, "Ok")
}

1.
테스트를 위해 결괏값을 지정해주었다.

 

2.
결괏값의 전달을 위해 액티비티를 종료시켰다.
여기서는 꼭 finish() 을 사용해야 한다.
뒤에 나오는 DESTROYED를 통해 앱을 종료시키면 시나리오에 결괏값을 넘기는 단계가 동작하지 않는다.

 

3.
result 변수를 이용하면 결괏값에 대한 정보를 가져올 수 있다.

 

여기까지는 함수명이나 필드명이 바뀌기만 하고 ActivityTestRule 과 큰 차이가 없다.

액티비티 종료하기

@After
fun close(){
    activityScenario.close()
}

테스트 종료 시마다 수동으로 액티비티를 종료해 주어야 한다.

( 옵션이기에 안 해줘도 된다. 하지만 코드 설명에서 "강하게 권장" 이 적혀있으니 잘 돌아가도 넣도록 하자. )


해당 close 함수에서는 앱을 파괴하고, 라이프사이클에 대한 옵저빙을 지우는 과정이 실행된다.

ActivityScenario.launch(MainActivity::class.java).use {
    ...
}

위의 과정이 귀찮다면 매번 테스트에서
ActivityScenario 를 생성하고 use 를 사용하여 자동으로 close 하도록 하면 된다.
( 이게 더 귀찮을 거다. )

LifeCycle 조정하기

@Test
fun moveToStateTest() {
    activityScenario.moveToState(Lifecycle.State.STARTED)
    Assert.assertEquals(activityScenario.state, Lifecycle.State.STARTED)

    activityScenario.moveToState(Lifecycle.State.CREATED)
    Assert.assertEquals(activityScenario.state, Lifecycle.State.CREATED)

    activityScenario.moveToState(Lifecycle.State.RESUMED)
    Assert.assertEquals(activityScenario.state, Lifecycle.State.RESUMED)

    activityScenario.moveToState(Lifecycle.State.DESTROYED)
    Assert.assertEquals(activityScenario.state, Lifecycle.State.DESTROYED)

    activityScenario.recreate()
}

moveToState 를 이용하면 라이프사이클을 바꿀 수가 있다.
INITIALIZED, STARTED, CREATED, RESUMED, DESTROYED 총 5단계를 제공한다.
대신, INITIALIZED는 moveToState 함수의 인자로 사용할 순 없다.

 

현재 상태는 state 변수를 통해 확인할 수 있다.

 

recreate 함수를 이용하면 앱을 onSaveInstanceState와 함께 재실행할 수도 있다.

ActivityScenarioRule

ActivityScenarioRule 을 볼 차례이다.
내부에 ActivityScenario 를 변수로 가지고 있으니 기능과 사용법이 동일하다.

생성

@get:Rule
var activityScenarioRule: ActivityScenarioRule<MainActivity> =
    ActivityScenarioRule(MainActivity::class.java)

@get:Rule
var activityScenarioRule: ActivityScenarioRule<MainActivity> =
    ActivityScenarioRule(
        Intent(
            ApplicationProvider.getApplicationContext(),
            MainActivity::class.java
        ).apply {
            putExtra("MyArgs","Nothing")
        }
    )

생성 방식은 ActivityScenario 와 동일하다.
대신 Rule 어노테이션을 붙은 게 끝이다.

 

Rule 이기에 따로 생성과 파괴 과정을 진행할 필요가 없다.
매 테스트마다 자동으로 실행되고 파괴된다.
( 이게 둘의 유일한 차이다. )

사용

@Before
fun setupData() {
    activityScenarioRule.scenario.onActivity {
        (it.findViewById<RecyclerView>(R.id.recyclerView))?.let {
            ...
        }
    }
}

여기도 동일하다.
대신 scenario 변수를 가져와서 onActivity 를 이용하면 된다.

결과 가져오기

@Test
fun resultTest() {
    activityScenarioRule.scenario.onActivity {
        it.setResult(Activity.RESULT_OK, Intent().apply {
            putExtra("Result", "Ok")
        })

        it.finish()
    }

    Assert.assertEquals(activityScenarioRule.scenario.result.resultCode, Activity.RESULT_OK)
    val result = activityScenarioRule.scenario.result.resultData?.extras?.getString("Result")
    Assert.assertEquals(result, "Ok")
}

여기서도 scenario 변수를 가져와서 동일한 과정을 진행하면 된다.

 

차이?
애매하지만 결과를 가져오는 속도가 현저히 느리며, 시간 초과로 인해 테스트가 실패하는 경우도 있다.
ActivityScenario 와 비교하면 코드 차이가 전혀 없는데도, 문제가 발생한다.
( 대신 디버그로 돌릴 시 엄청 빠르다.. 제멋대로다. )

LifeCycle 조정하기

fun moveToStateTest() {
    activityScenarioRule.scenario?.let { activityScenario ->
        activityScenario.moveToState(Lifecycle.State.STARTED)
        Assert.assertEquals(activityScenario.state, Lifecycle.State.STARTED)
        activityScenario.moveToState(Lifecycle.State.CREATED)
        Assert.assertEquals(activityScenario.state, Lifecycle.State.CREATED)
        activityScenario.moveToState(Lifecycle.State.RESUMED)
        Assert.assertEquals(activityScenario.state, Lifecycle.State.RESUMED)
        activityScenario.moveToState(Lifecycle.State.DESTROYED)
        Assert.assertEquals(activityScenario.state, Lifecycle.State.DESTROYED)

        activityScenario.recreate()
    }
}

추가 적인 설명은 하지 않겠다. ActivityScenario 와 동일하다.

마무리

이번에는 테스트에 필요한 ActivityRule에 대해 알아보았다.
구글이 ActivityScenarioRuleActivityScenario 를 권장하고 있으니 참고하면 좋을 것 같다.

반응형