본문 바로가기

안드로이드

Espresso intents - 카메라 촬영 테스트 하기

반응형

 

오늘은 에스프레소 테스트 도중에 카메라 촬영을 진행하는 방법을 알아보자.

 

시작하기 전에 

만에 하나라도 카메라에 보이는 장면을 실제로 촬영하기를 원한다면 에스프레소만으로는 불가능하며
카메라 기능을 직접 구현하거나, 내가 직접 만든 카메라 앱과 함께 테스트하거나 두 가지 방법뿐이다.
( 물론 본인이 모든 카메라 앱의 버튼의 위치나 버튼 ID를 때려 맞출 자신이 있다면 가능하다. )

그러니, 만약 그런 걸 원한다면 정신건강에 매우 해롭기 때문에, 깔끔하게 에스프레소랑은 거리를 두자.

 

우리의 목적은 인텐트를 조작하여 카메라가 실제로 촬영된 것처럼 돌아가도록 로직을 만들 것이다.

라이브러리

androidTestImplementation 'androidx.test.espresso:espresso-intents:3.2.0'

espresso의 확장 라이브러리로 인텐트를 조작을 간단하게 만들어주는 기능을 포함하고 있다.

기본 라이브러리 세팅은 Android Espresso #1에서 확인할 수 있다.

Intents 준비하기

새로운 라이브러리에서 Intent 관리는 Intents를 통해서 이루어진다.

 

@get:Rule
var intentTestRule = IntentsTestRule(GalleryActivity::class.java)

Intents 를 사용하는 가장 간단한 방법은 IntentsTestRule 을 TestRule로 지정하는 것이다.

 

ActivityTestRule 를 상속받아 구현되어 있고,

Intents 의 초기화 코드가 추가되었다.

Intents.init()
Intents.release()

위의 코드가 Intents 의 초기화를 담당하는 코드로

IntentsTestRule 에서는 테스트의 시작과 종료에서 각각 호출해 주고 있다.

 

물론 직접 호출하여 초기화하는 것도 가능하지만,

Intents.init() 코드와 IntentsTestRule 을 같이 사용하면 중복 초기화로 에러가 발생하니 주의하자.

 

Intents 는 테스트 중에 모든 Intent 정보를 가지고 있는데,

정보를 쌓기 시작하는 시점은 Intents.init() 이후 정보를 모두 날리는 시점 Intents.release() 이후이다.

Intents.init()

B 액티비티로의 이동

// 1 ...

Intents.release()

// 2 ...

예를 들면,

위의 같은 상황이면 "1"에서는 에서는 B 액티비티로의 이동에 관한 Intent 정보를 가져와서 검증하는 게 가능하지만 "2"에서는 Intents 가 초기화되지 않았기에 Intent에 대한 접근은 에러를 발생시킨다.

Intended

Intended 는 Intent의 검증을 위해 사용할 수 있고,

IntentMatchers 클래스를 통해서 다양한 매처들을 제공하고 있다.

 

따로 perform, check 같은 애들은 없고, 매칭 되는 Intent의 유무에 따라서 성공 여부가 나온다.

 

IntentMatchers몇가지를 소개하자면

Intents.intended(IntentMatchers.hasExtraWithKey("DESC"))
Intents.intended(IntentMatchers.anyIntent())

예제처럼 hasExtraWithKey 로 해당 Intent 가 키에 맞는 데이터가 있는지를 확인하거나

anyIntent 처럼 뭐 하나만 있으면 통과하게 할 수 도 있으며

Intents.intended(hasPackage("com.onetwothree.espressosample"))
Intents.intended(hasComponent("com.onetwothree.espressosample.DetailActivity"))

hasPackage, hasComponent 처럼 package, component 등을 통해서 원하는 Intent 가 맞는지 검증을 할 수 있다.

Intending

Intending 은 특정 Intent 가 실행되기 전에 가로채어 특정 결과 ( stub )를 던지도록 할 수 있다.

 

사용법은 간단하다.

내가 스텁 할 Intent를 찾을 수 있는 IntentMatchers 를 지정하고, 내가 원하는 값을 반환하도록 하면 된다.

val result = Instrumentation.ActivityResult(Activity.RESULT_OK, bundle)
Intents.intending(hasAction(MediaStore.ACTION_IMAGE_CAPTURE)).respondWith(result)

예제를 보자.

hasAction(MediaStore.ACTION_IMAGE_CAPTURE) 로 내가 낚을 인텐트를 지정하고

respondWith 에 "bundle"을 집어넣었다.

 

해석하면 "카메라 캡처 액션이 일어나면 번들을 던져주겠어!" 정도이다.

Intents.intending(hasComponent("com.onetwothree.espressosample.DetailActivity")).respondWithFunction {

    doSomething

    Instrumentation.ActivityResult(Activity.RESULT_OK, bundle)
}

문법만 다른 다른 예제를 보자

hasComponent("com.onetwothree.espressosample.DetailActivity") 로 내가 낚을 인텐트를 지정하고

respondWithFunction 에 무언가를 하고 bundle을 넣은 result를 반환했다.

 

해석하면 "DetailActivity 가 열리려 하면 로직을 돌려서 bundle을 만들고 RESULT_OK와 함께 던져주겠어!" 정도이다.

 

Intent 가 실행되는 걸 가로채는 것이기에 선언은 Intent 실행 시점 이전에 존재해야 한다.

카메라 촬영(한 것처럼) 하기

드디어, 에스프레소 테스트에서 카메라 촬영을 할 준비가 되었다.


앞부분에서 이미 나왔듯이 Intending 를 사용하여 카메라 결과를 대신 던져주는 방법으로 테스트를 진행할 것이다.

 

그럼 카메라 촬영을 시작해보자.

섬네일 받아오는 경우

첫 번째는 촬영을 호출해서 썸네일 받아오는 경우이다.

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent) {
    if (requestCode == REQUEST_IMAGE_CAPTURE && resultCode == RESULT_OK) {
        val bitmap = data.extras.get("data") as Bitmap
        ...
    }
}

Intent(MediaStore.ACTION_IMAGE_CAPTURE)를 별다른 설정 없이 실행했을 때

onActivityResult 에 떨어지는 결과물은 위의 코드처럼 data 라는 key에 있는 이미지 썸네일 비트맵이다.

 

우리는 카메라 촬영 요청이 왔을 때 data 라는 key에 이미지를 넣어서 던져주기만 하면 된다.

private fun createImageBitmap(drawableRes :Int): Instrumentation.ActivityResult {

    val bundle = Bundle().apply {
        val bitmap = ContextCompat.getDrawable(
        InstrumentationRegistry.getInstrumentation().targetContext, drawableRes)?.toBitmap()
        putParcelable("data", bitmap)
    }

    return Instrumentation.ActivityResult(Activity.RESULT_OK, 
        Intent().apply {
            putExtras(bundle)
        }
    )
}

우선, data 키에 이미지를 넣는 코드이다.

 

context를 가져올 때는 InstrumentationRegistry.getInstrumentation().targetContext

같은 코드를 사용하면 되며, 위에서 선언한 intentTestRule 를 통해서 context를 가져와도 된다.

 

BitmapFactory.decodeResource 를 통해서 resource를 bitmap으로 가져온 후

data 를 key 로 하여 bundle에 집어넣었다.

 

그리고 해당 번들과 함께 ActivityResult 객체를 반환했다.

val result = createImageBitmap(drawableRes)
intending(hasAction(MediaStore.ACTION_IMAGE_CAPTURE)).respondWith(result)

마지막으로 위와 같은 코드를 작성하면

ACTION_IMAGE_CAPTURE 이 발생될 때마다 카메라 실행 없이 우리가 만들어둔 ActivityResult 가 반환되게 된다.

파일을 저장하는 경우

두 번째로 내가 이 글을 쓰게 만든 원인인 파일을 저장하는 경우를 테스트할 것이다.

 

방법은 위의 예제와 비슷하다.

ACTION_IMAGE_CAPTURE 가 들어올 때마다 내가 넘긴 uri에 이미지를 잘 넣어주면 된다.

 

코드를 통해서 얼마나 간단한지 확인해보자.

intending(hasAction(MediaStore.ACTION_IMAGE_CAPTURE)).respondWithFunction { intent->
    try {
        val imageUri: Uri = intent.getParcelableExtra(MediaStore.EXTRA_OUTPUT)

        val context: Context = InstrumentationRegistry.getInstrumentation().targetContext

        val icon =  ContextCompat.getDrawable(
        	InstrumentationRegistry.getInstrumentation().targetContext,
        	drawableRes)?.toBitmap()

        val out: OutputStream? = context.contentResolver.openOutputStream(imageUri)
        icon?.compress(Bitmap.CompressFormat.JPEG, 100, out)

        out?.flush()
        out?.close()
    } catch (e: IOException) {
        Instrumentation.ActivityResult(Activity.RESULT_CANCELED, null)
    }
    Instrumentation.ActivityResult(Activity.RESULT_OK, null)
}

respondWithFunction 함수를 이용하면 intent 가 인자로 들어오기 때문에 저장할 uri를 확인할 수 있다.

코드들은 모두 비트맵을 가져오고 uri에 저장하는 과정일 뿐 크게 다른 작업을 하지는 않는다.


이제 ACTION_IMAGE_CAPTURE 이 발생될 때마다 uri에 성공적으로 저장될 것이다.

반응형

'안드로이드' 카테고리의 다른 글

Error ) ViewPager2 - onAttachedToRecyclerView  (0) 2021.01.03
Hilt - @SingletonComponent  (0) 2020.11.16
Android - java.time 패키지  (0) 2020.07.15
Android Hilt - WorkManager  (0) 2020.07.03
Android & Java - Reflection  (0) 2020.07.02