회사에서 프로젝트를 진행하며 한 가지 귀찮은 점이 생겼다.
바로 테스트마다 회원가입을 자주 해야한다는 점.
매 기능이 생겨날때마다 혹시 모를 사이드 이펙트를 체크하기 위해서는 회원가입을 진행해야했는데, 문제는 이 때 인증하는 과정이 여러 번 존재하여 시간도 오래 걸리고 귀찮은 부분이 많이 존재했다.
그래서 나는 이것을 반자동화하기로 결정하였다.
처음에는 완전 자동화를 꿈꾸었지만, 문자 인증이나 카카오 인증 등 실 사용자만이 할 수 있는 여러 인증 상황이 존재했기에 어느 정도 타협하기로 마음을 먹은 것이다.
# 하지만 한 가지 문제가 있었다..
테스트 코드 중간에 잠시 일시 정지를 하고, 인증이 완료된 이후에 일시 정지를 풀어줘야 하는 로직을 구현해야 하는 것인데 이러한 기능을 제공해주는 코드는 따로 없었기에 나는 다음과 같이 코드를 구현하기로 하였다.
- 테스트 코드에서 사용할 브로드 캐스트 리시버 생성
- 테스트 코드 실행 시 어느 지점에서 브로드 캐스트 리시버를 통해 작업을 잠시 일시 정지 (무한 루프)
- 사용자 인증 완료
- 인증 완료 시 ADB 를 통해 브로드 캐스트 리시버로 데이터를 전송
- 브로드 캐스트 리시버를 참조하던 테스트 코드에서는 변화값을 감지하여 무한 루프 탈출
# 구현을 진행해보자 !
대충 실행 순서는 설명하였으니, 이제 진행해보도록 하겠다.
우선 BroadCastReciever 인터페이스와 구현체를 만들어준다.
/**
* PauseBroadCastReceiver 에서 사용되기 위한 인터페이스
* */
private interface PauseBroadCastReceiverHandler {
val pauseStateFlow: MutableStateFlow<Boolean>
suspend fun handlePauseStateFlow(handleFlow: suspend (MutableStateFlow<Boolean>) -> Unit)
}
/**
* 실제 사용될 브로드캐스트리시버
*/
class PauseBroadCastReceiver : BroadcastReceiver(), PauseBroadCastReceiverHandler {
override val pauseStateFlow = MutableStateFlow(true)
override fun onReceive(context: Context?, intent: Intent?) {
when (intent?.action) {
TEST_PAUSE_ON -> pauseStateFlow.update { true }
TEST_PAUSE_OFF -> pauseStateFlow.update { false }
}
}
override suspend fun handlePauseStateFlow(
handleFlow: suspend (MutableStateFlow<Boolean>) -> Unit,
) = handleFlow(pauseStateFlow)
companion object {
const val TEST_PAUSE_ON = "devgyu.blogproject.ANDROID_TEST_PAUSE_ON"
const val TEST_PAUSE_OFF = "devgyu.blogproject.ANDROID_TEST_PAUSE_OFF"
}
}
위의 코드를 간단히 설명하면 다음과 같다.
테스트 코드에서 감지할 pauseStateFlow 를 True 로 생성하고 테스트 코드 내에서 이를 handlePauseStateFlow 메소드를 통해 전달받는다.
그 이후 onReceive 에서 IntentAction 을 감지하여 이 값을 액션에 맞춰 Flow 를 업데이트를 해준다.
handlePauseStateFlow 를 suspend 함수로 만든 이유는 lambda 를 suspend 함수로 전달해주기 위함인데, 이것에 대한 자세한 설명을 이따가 진행하도록 하겠다.
이제 AndroidTest Folder 로 돌아와서 다음을 입력하자.
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
private lateinit var pauseBroadCastReceiver: PauseBroadCastReceiver
private lateinit var context: Context
private val intentFilter = IntentFilter().apply {
addAction(PauseBroadCastReceiver.TEST_PAUSE_ON)
addAction(PauseBroadCastReceiver.TEST_PAUSE_OFF)
}
@Before
fun init() {
context = InstrumentationRegistry.getInstrumentation().targetContext
pauseBroadCastReceiver = PauseBroadCastReceiver()
context.registerReceiver(pauseBroadCastReceiver, intentFilter, Context.RECEIVER_EXPORTED)
}
}
pauseBroadCastReceiver 와 context 는 모든 테스트 코드에서 사용할 수 있게, 또한 @After 에서 BroadCastReceiver 의 등록을 취소해줄 수 있게 클래스 멤버 변수로 설정하고, @Before 에서 초기화해준 뒤 BroadCastReceiver 를 등록해준다.
@Test
fun BlogTest() = runTest {
// 테스트 코드 ..
// 테스트 코드 ..
// 사용될 곳에 입력
pauseBroadCastReceiver.handlePauseStateFlow { pauseStateFlow ->
withContext(Dispatchers.Default) {
while (pauseStateFlow.value) {
println("잠시 0.5초 대기")
delay(500L)
}
println("인증 완료")
pauseStateFlow.update { true }
}
}
}
이후 위와 같이 사용될 곳에 pauseBroadCastReceiver.handlePauseStateFlow 를 구현해준다.
현재 함수는 suspend 함수이므로 TestScope 내에서 호출이 되어야 하며, while 의 조건을 통해 무한루프를 시켜 코드가 다음으로 진행하지 못하게 막아준다.
여기서 withContext(Dispatchers.Default) 로 설정한 이유는 runTest 는 TestDispatchers 로 실행되는데, TestDispatchers 의 경우 Delay 가 실제 시간이 아닌 가상시간으로 적용되기 때문에, 실제 0.5초에 맞춰 print 를 하기 위해 CoroutineContext 를 변경한 것이다.
하지만 여기까지 진행하더라도, 테스트 진행 중간에 BroadCast 에 Intent.Action 을 전달하는 법을 알지 못하면 모두 물거품이 된다.
그러니 다음과 같은 명령어를 실행하여 브로드캐스트에 액션을 전달해주도록 하자.
// 테스트 멈춤
adb shell am broadcast -a "devgyu.blogproject.ANDROID_TEST_PAUSE_ON"
// 테스트 진행
adb shell am broadcast -a "devgyu.blogproject.ANDROID_TEST_PAUSE_OFF"
만약 adb 명령어가 잘 안된다면 adb 설치 후 디바이스와 adb 를 연결한 후 사용하도록 하자.
나의 경우 위와 같은 명령어를 매번 기입하는게 귀찮아서 다음과 같이 그래들 명령어를 추가하였다.
// Project Build.gradle.kts
with(tasks){
register("TestResume"){
exec { commandLine("adb", "shell", "am", "broadcast", "-a", "biz.pluscompany.ANDROID_TEST_PAUSE_OFF") }
}
}
만약 나처럼 명령어를 설정해줬따면 터미널에 ./gradlew TestResume 만 입력해줘도 테스트를 멈춤 -> 진행 상태로 바꿀 수 있게 된다 !
아래는 이것들을 모두 정리한 코드이다
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
private lateinit var pauseBroadCastReceiver: PauseBroadCastReceiver
private lateinit var context: Context
private val intentFilter = IntentFilter().apply {
addAction(PauseBroadCastReceiver.TEST_PAUSE_ON)
addAction(PauseBroadCastReceiver.TEST_PAUSE_OFF)
}
@Before
fun init() {
context = InstrumentationRegistry.getInstrumentation().targetContext
pauseBroadCastReceiver = PauseBroadCastReceiver()
context.registerReceiver(pauseBroadCastReceiver, intentFilter, Context.RECEIVER_EXPORTED)
}
@Test
fun BlogTest() = runTest {
// 테스트 코드 ..
// 테스트 코드 ..
// 잠시 멈춰야 되는 곳에 입력
pauseBroadCastReceiver.handlePauseStateFlow { pauseStateFlow ->
withContext(Dispatchers.Default) {
while (pauseStateFlow.value) {
println("잠시 0.5초 대기")
delay(500L)
}
println("인증 완료")
// 추후 다시 사용할 수도 있으니 다시 true 로 변경
pauseStateFlow.update { true }
}
}
}
@After
fun after(){
context.unregisterReceiver(pauseBroadCastReceiver)
}
}
잠시 일시정지를 시킨 상태에서 테스트가 끊길수도 있으니 runTest Timeout 을 길게 잡아주도록 하자.
지금처럼 설명하는 때에는 너무나도 간단히 말하지만, 이렇게 구현할 생각을 하던 당시에는 몇 시간동안 어떻게 처리를 해야 중도에 실행시킬 수 있을까를 고민했다.
혹시나 나처럼 반자동화 코드를 필연적으로 구현해야 하는 사람들이 생긴다면 이 게시글이 도움이 되었다면 좋겠다 !
시간이 지나고 다시 생각해보니 WaitUntil 로 Lifecycle 체크를 한다면 해결되는 문제였어서, WaitUntil 을 쓰는 것으로 변경하였다.
하지만 뭔가 이러한 방식도 쓸 날이 생기지 않을까 .. 싶어서 우선 게시글을 남겨두겠다.