한국의 개발자들을 위한 Google for Developers 국문 블로그입니다.
코루틴에서 두 차례의 연속적인 LiveData 내보내기 테스트 방법을 소개합니다
2019년 11월 19일 화요일
<블로그 원문은
이곳
에서 확인하실 수 있으며 블로그 번역 리뷰는 노현석(Android GDE)님이 참여해 주셨습니다>
이 글은 오픈소스
Plaid 애플리케이션
에서 코루틴의 CoroutineDispatcher를 일시 중지했다가 다시 시작하여 두 차례의 연속적인 LiveData 내보내기를 단위 테스트한 방법에 관한 글입니다.
테스트가 정확하고 빠르고 신뢰할 수 있도록 하려면 이 글의 끝 부분에 있는 모범 사례 섹션을 확인해보세요 .
문제
우리는 두 차례의 연속적인 LiveData 내보내기(그중 하나는 코루틴에서 실행됨)를 테스트하고 싶었지만, 모든 코루틴을 즉시 실행하는
Dispatchers.Unconfined
주입으로는
불가능했습니다. 이 때문에 우리가 단위 테스트에서 내보내기를 단언할 수 있게 될 무렵에는 첫 번째 LiveData 내보내기는 놓치고 두 번째 내보내기만 확인할 수 있었습니다. 더 자세한 설명이 이어집니다.
< Plaid의 Dribbble Shot 세부 정보 화면
Dribbble
(Plaid의 데이터 소스 중 하나) 세부 정보 화면에서, 우리는 화면을 최대한 빠르게 표시하고 싶었지만 (마크다운을 Spannable로 변경하는 데 시간이 걸리는 바람에) 화면을 표시하기까지 몇몇 요소가 처리에 시간이 걸릴 수 있었습니다. 이 문제를 해결하기 위해, 우리는 UI 상태의 단순화된 버전을 빠르게 내보낸 다음, 백그라운드 작업을 시작하여 처리된 버전을 생성한 후 내보내기로 결정했습니다.
이를 위해 우리는 LiveData와 코루틴을 사용하는데, UI 통신에는 LiveData, 주 스레드에서 벗어나 작업을 수행하는 데는 코루틴을 사용합니다. ViewModel이 시작되면 기본 UI 모델을 UI가 구독하는 LiveData로 내보냅니다. 그런 다음, 실행을 백그라운드로 이동하고 완전한 UI 모델을 생성하는
CreateShotUiModel UseCase
를 호출합니다. UseCase가 끝나면 ViewModel이 완전한 UI 모델을 이전과 동일한 LiveData로 내보냅니다. 아래 코드에서 이런 과정을 확인할 수 있습니다.
코드보기
>>
우리는 두 UI 상태가 모두 UI로 내보내졌는지 테스트하고자 합니다. 하지만 두 UI 상태가 연속으로 내보내지고 LiveData 인스턴스에는 두 번째 값만 포함되어 있었기 때문에 첫 번째 값을 확인할 수 없었습니다. 테스트에서 주입되는
Dispatchers.Unconfined
로 인해 processUiModel에서 시작된 코루틴이 동기적으로 실행되는 바람에 이런 일이 일어났습니다.
Plaid는
CoroutinesDispatcherProvider
라는 클래스를 사용하여 코루틴과 함께 작동하는 클래스에 코루틴 Dispatcher를 주입합니다.
LiveData는
마지막으로 받는 값
만 유지합니다. 테스트에서 LiveData의 내용을 테스트하기 위해, 우리는
LiveData.getOrAwaitValue()
확장 함수를 사용합니다.
우리의 요구사항이 적용된 다음 단위 테스트가 실패합니다.
코드보기>>
이 동작을 어떻게 테스트할 수 있을까요?
해결 방법
우리는 ViewModel이 생성한 코루틴의 CoroutineDispatcher를 일시 중지했다가 계속할 수 있도록 코루틴 라이브러리(kotlinx.coroutines.test 패키지)에서 새로운
TestCoroutineDispatcher
를 사용했습니다.
( Disclaimer
: TestCoroutineDispatcher는 아직 실험 중인 API입니다. )
TestCoroutineDispatcher의 주입된 인스턴스를 사용해 코루틴이 실행을 시작하는 시점을 제어할 수 있습니다. 테스트 로직은 다음과 같습니다.
테스트가 시작되기 전에 디스패처를 일시 중지하고 ViewModel 초기화 중에 빠른 결과가 내보내졌는지 확인합니다.
ViewModel에서 processUiModel 메서드의 코루틴을 시작하는 테스트 Dispatcher를 재개합니다.
느린 결과가 내보내졌는지 확인합니다.
코드보기>>
여기
를 통해 테스트에서 주입된
CoroutinesDispatcherProvider
사이의 차이점을 확인하세요.
우리는 테스트 본문과 단언을 testCoroutineDispatcher.
runBlockingTest
본문 람다 내에 포함하는데, 그 이유는 이 람다가
runBlocking
과 유사하게 testCoroutineDispatcher를 동기적으로 사용하는 코루틴을 실행하기 때문입니다.
부인 2 (원문 : Disclaimer)
: 모든 테스트마다 TestCoroutineDispatcher를 설정하고 해제해야 하는 상황을 피하기 위해, 다른
테스트
에서 사용하는 것처럼 이
JUnit 테스트 규칙
을 사용할 수 있습니다.
다른 접근 방식
이 문제를 해결하기 위해 선택할 수 있는 다른 방법이 있습니다. 우리는 이 문제에 직면할 때 최선의 방안이라 생각하는 방법을 선택했는데, 이 방법은 애플리케이션 코드를 변경할 필요가 없기 때문이었습니다.
다른 구현 방법은 ViewModel에서 새로운
liveData 코루틴 빌더
를 사용하여 두 개의 항목을 내보내고, 테스트에서는
LiveData.asFlow()
확장 함수를 사용하여
이 PR
에서 볼 수 있듯이 해당 요소를 확인할 수 있습니다. 이 접근 방식은 테스트에서 디스패처 중지하지 않고 테스트를 구현에서 분리하는 데 도움이 되지만, Lifecycle 코루틴 extension에서 제공되는 최신 API를 사용하려면 ViewModel 구현을 변경해야 합니다.
모범 사례
정확하고 빠르고 신뢰할 수 있는 테스트를 위해서는 다음과 같이 해야 합니다.
항상 Dispatcher를 주입하세요!
테스트에 TestCoroutineDispatcher를 사용할 수 있도록 ViewModel에 Dispatcher가 주입되지 않았다면 우리가 문제를 해결하지 못했을 것입니다.
모범 사례에 따라,
Dispatcher를 사용하는 클래스에는 항상 Dispatcher를 주입
하세요. 클래스에서 직접 코루틴 라이브러리(예: Dispatchers.IO)와 함께 제공되는 미리 정의된 Dispatcher를 사용하면 이를 종속성으로 전달해 테스트가 더 어려워지므로 이런 Dispatcher를 사용하면 안 됩니다.
아래 코드에서 보는 것처럼, 어떤 클래스에서든 직접 사용되는 Dispatcher가 보일 경우 이는 코드 스멜입니다.
코드보기>>
특히
AAC ViewModel
과 Dispatchers.Main을 기본으로 설정된 viewModelScope의 사용에 관해 얘기할 때는 이를 주입하는 대신 재정의해야 합니다. 우리는 다른 클래스에 주입되는 동일한 TestCoroutineDispatcher 인스턴스와 함께
이 JUnit 규칙
을 사용하여 재정의합니다.
예시는 여기
에서 확인하세요. 이에 관해 더 자세히 알아보려면
viewModelScope
에 관한
기사
를 읽어보세요.
Dispatchers.Unconfined 대신 TestCoroutineDispatcher 주입
TestCoroutineDispatcher
의 인스턴스를 클래스에 주입하고
runBlockingTest
메서드를 사용하여 테스트에서 그 디스패처를 동기적으로 사용하는 코루틴을 실행하세요. 또한 코루틴을 원하는 대로 계속하고 일시 중지할 수 있도록 하려면 TestCoroutineDispatcher를 사용할 수도 있습니다.
일반적으로는 미리 정의된 세 Dispatcher(예 : Main, Default, IO)에 동일한 TestCoroutineDispatcher 인스턴스를 주입합니다. 멀티 스레드 시나리오에서 작업 타이밍을 확인해야 할 경우(예: 동시에 실행 중인 코루틴의 순열을 테스트하고 싶음), 미리 정의된 Dispatcher마다 다른 TestCoroutineDispatcher 인스턴스를 만들어 주입하세요.
Dispatchers.Unconfined는 어떤가요?
코루틴 내에서 코드를 동기적으로 실행하는 경우 테스트를 위해 Dispatchers.Unconfined를 주입할 수도 있습니다(kotlinx-coroutines는 현재 이것을 테스트에 사용함). 하지만 Unconfined는 TestCoroutineDispatcher보다 유연성이 떨어집니다. Unconfined를 일시 중지할 수 없고 즉각적인 디스패치로 제한됩니다.
또한 Unconfined는 다른 디스패처를 사용하는 코드에 대한 가정과 타이밍도 위반합니다. 이 점은 병렬 계산을 테스트할 때 더욱 현저히 드러나는데, 예를 들어 계산은 자신이 작성된 다른 계산에서 실행되고 다른 시간에 끝나는 그러한 계산의 다른 순열은 테스트할 수 없습니다.
Unconfined와 TestCoroutineDispatcher는 둘 다 명시적으로 병렬 실행을 피합니다. 하지만 TestCoroutineDispatcher를 통해 일시 중지된 모드에서 동시 실행 순서를 좀 더 강력하게 제어할 수 있지만, 그것만으로는 모든 순열을 테스트하기에 충분치 않습니다. 여기서는 일반적인 테스트 조언이 적용되는데, 복잡한 동시 실행 동작을 할 경우 테스트 가능성을 염두에 두고 설계해야 합니다.
LiveData 테스트
LiveData 테스트에 관한 더 많은 모범 사례는
Jose Alcerreca
의
이 주제에 관한 게시물
을 확인해 보세요.
이 변경 사항에 대한 전체 Plaid PR을 보려면
여기
를 클릭하세요.
Contents
ML/Tensorflow
Android
Flutter
Web/Chrome
Cloud
Google Play
Community
Game
Firebase
검색
Tag
인디게임페스티벌
정책 세미나
창구프로그램
AdMob
AI
Android
Android 12
Android 12L
Android 13
Android 14
Android Assistant
Android Auto
Android Games
Android Jetpack
Android Machine Learning
Android Privacy
Android Studio
Android TV
Android Wear
App Bundle
bootcamp
Business
Chrome
Cloud
Community
compose
Firebase
Flutter
Foldables
Game
gdg
GDSC
google
Google Developer Student Clubs
Google Play
Google Play Games
Interview
Jetpack
Jetpack Compose
kotlin
Large Screens
Library
ma
Material Design
Material You
ML/Tensorflow
mobile games
Now in Android
PC
Play Console
Policy
priva
wa
wear
Wearables
Web
Web/Chrome
Weeklyupdates
WorkManager
Archive
2024
11월
10월
9월
8월
7월
6월
5월
4월
3월
2월
1월
2023
12월
11월
10월
9월
8월
7월
6월
5월
4월
3월
2월
1월
2022
12월
11월
10월
9월
8월
7월
6월
5월
4월
3월
2월
1월
2021
12월
11월
10월
9월
8월
7월
6월
5월
4월
3월
2월
1월
2020
12월
11월
10월
9월
8월
7월
6월
5월
4월
3월
2월
1월
2019
12월
11월
10월
9월
8월
7월
6월
5월
4월
3월
2월
1월
2018
12월
11월
10월
9월
8월
7월
6월
5월
4월
3월
2월
1월
2017
12월
11월
10월
9월
8월
7월
6월
5월
4월
3월
2월
1월
2016
12월
11월
10월
9월
8월
7월
6월
5월
4월
3월
2월
1월
2015
12월
11월
10월
9월
8월
7월
6월
5월
4월
3월
2월
1월
2014
12월
11월
10월
9월
8월
7월
6월
5월
4월
3월
2월
1월
2013
12월
11월
10월
9월
8월
7월
6월
5월
4월
3월
2월
1월
2012
12월
11월
10월
9월
8월
7월
6월
5월
3월
2월
1월
2011
12월
11월
Feed