본 글은 MockWebServer 사용 방법에 초점을 두어 작성되었습니다.
들어가기 전
UI 렌더링 최적화 테스트를 위해 리스트를 만들어 테스트를 진행해야 했다.
더미 리스트를 만들어서 넣는 것보다 실제 네트워크를 통해 데이터를 받아와 테스트하고 싶었다.
개발된 API가 따로 없어 공공 API를 활용해야 했지만, 실제 Unit Test에서 많이 사용되는 MockWebServer를 활용해 보고 싶어 선택하게 되었다.
프로젝트 설명
MockWebServer를 활용해 공지사항 json response를 받아온다.
받아온 response를 diffUtil RecyclerView를 통해 리스트 뷰로 보여주도록 한다.
프로젝트 구성
테스트용 프로젝트이지만 관심사 분리를 위해 폴더링은 아래와 같이 진행하였다.
Activity > ViewModel > Repository > RepositoryImpl로 진행되도록 하였다.
📂 com.example.test-board
┣ 📂 application
┣ 📂 data
┃ ┣ 📂 remote
┃ ┣ 📂 repository
┃ ┃ ┣ NoticeRepositoryImpl
┃ ┣ 📂 service
┣ 📂 di
┃ ┣ NetworkModule
┃ ┣ RepositoryModule
┃ ┣ ServiceModule
┣ 📂 domain
┃ ┣ 📂 model
┃ ┣ 📂 repository
┃ ┃ ┣ NoticeRepository
┣ 📂 presentation
┣ 📂 util
MockWebServer 적용
MockwebServer 의존성 추가
//MockServer
implementation("com.squareup.okhttp3:mockwebserver:4.11.0")
implementation("com.squareup.moshi:moshi-kotlin:1.9.3")
implementation("com.squareup.retrofit2:converter-moshi:2.8.1")
MockwebServer Response JSON 작성
NoticeRemote 클래스에 mockserver에서 보낼 response json을 생성해 두었다.
val notices = """
[
{
"id": 1,
"title": "[공지] 김사원의 공지",
"content": "공지의 내용입니다.",
"createdAt": "2024.04.02",
"writer": "김사원"
},
{
"id": 2,
"title": "[공지] 신인턴의 공지",
"content": "공지의 내용입니다.",
"createdAt": "2024.04.04",
"writer": "신인턴"
},
{
"id": 3,
"title": "[공지] 신과장의 공지",
"content": "공지의 내용입니다.",
"createdAt": "2024.04.06",
"writer": "신과장"
}
] """.trimIndent()
notices 변수에 response json을 생성하였다.
String.trimIndent()
이때 사용한 trimIndent()
는 코틀린 String의 메서드 중 하나로
모든 입력 라인의 공통 최소 들여쓰기를 감지하고 모든 라인에서 그만큼 제거한다.
NetworkModule 작성
본 프로젝트에서는 DI를 적용하기 위해 Hilt를 사용하였다.
따라서 Retrofit, MockWebServer의 내용을 담은 NetworkModule을 작성하였다.
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
@Provides
@Singleton
fun provideMockWebServer(): MockWebServer =
MockWebServer().apply {
enqueue(
MockResponse()
.setBody(NoticeRemote.notices)
.setResponseCode(200)
.setHeader("Content-Type", "application/json")
)
}
@Provides
@Singleton
fun provideMoshi(): Moshi = Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.build()
@Provides
@Singleton
fun provideRetrofit(moshi: Moshi, mockWebServer: MockWebServer): Retrofit = runBlocking(Dispatchers.IO) {
Retrofit.Builder()
.baseUrl(mockWebServer.url("/").toString())
.addConverterFactory(MoshiConverterFactory.create(moshi))
.build()
}
}
위 코드를 하나씩 살펴보자.
provideRetrofit()
Retrofit을 제공하는 함수이다. 매개변수로는 provideMoshi(), provideMockWebServer()로부터 받은 Moshi, MockwebServer가 사용된다.
기존에 서버와 API 통신을 할 때는 baseUrl에 서버의 Url이 들어갔다.
하지만 MockwebServer로 API를 mocking 하기 위해서는 MockwebServer의 url을 넣어주어야 한다.
또한 Json 파일을 Kotlin 클래스로 전환하기 위해 MoshiConverter를 활용하였다.
Retrofit에 제공된 moshi로 MoshiConverterFactory를 추가한다.
이렇게 설정한 Retrofit을 build 한다.
provideMoshi()
Retrofit에 Converter를 추가하기 위해 필요한 Moshi를 제공하는 함수이다.
provideMockWebServer()
API Mocking을 하기 위해 필요한 MockWebServer를 제공하는 함수이다.
이때 MockServer의 Response와 Header, ResponseCode를 설정할 수 있다.
@Provides
@Singleton
fun provideMockWebServer(): MockWebServer =
MockWebServer().apply {
enqueue(
MockResponse()
.setBody(NoticeRemote.notices)
.setResponseCode(200)
.setHeader("Content-Type", "application/json")
)
}
- setBody() : Mock API로부터 받을 Response Json을 넣으면 된다.
- enqueue는 MockResponse를 한번 넣는 것으로 앱 실행 중에 통신을 또 시도하게되면 enqueue된 response가 없어 Response를 받아올 수가 없게 된다.
- @Test 환경에서는 @Before, @After를 통해서 MockWebServer를 Set하는 과정을 넣는다.
NoticeRepositoryImpl 작성
NoticeRepository를 구현하는 NoticeRepositoryImpl을 작성한다.
class NoticeRepositoryImpl @Inject constructor(private val apiService: NoticeApiService) :
NoticeRepository {
override suspend fun getNoticeList(): Result<List<Notice>> =
withContext(Dispatchers.IO) {
runCatching {
apiService.getNotices()
// 코루틴 runCatching 해서 api service의 response를 Result로 감쌈.
}
}
}
getNoticeList를 오버라이드해 apiService.getNotices()를 실행한다.
NoticeViewModel 작성
Repository로부터 데이터를 받고 Activity에 띄울 데이터를 전달하는 ViewModel을 작성한다.
@HiltViewModel
class NoticeViewModel @Inject constructor (private val noticeRepository: NoticeRepository) : ViewModel() {
private var _noticeListState = MutableStateFlow<UiState<List<Notice>>>(UiState.Empty)
val noticeListState get() = _noticeListState
init {
getNoticeList()
}
private fun getNoticeList() {
viewModelScope.launch {
val result = noticeRepository.getNoticeList()
result.onSuccess { noticeList ->
_noticeListState.value = UiState.Success(noticeList)
}.onFailure { exception ->
_noticeListState.value = UiState.Error(exception.message)
}
}
}
}
StateFlow를 통해 Repository의 result를 받는다.
val result는 Repository로부터 받은 Result<List>로
성공되었으면 noticeListState.value = UiState.Success(noticeList)로 설정한다.
실패하였으면 UiState.Error()를 value로 설정하여 서버 error에 따른 대응을 할 수 있도록 한다.
NoticeListActivity 구현
viewModel에서 받아온 UiState를 바탕으로 성공할 시 adapter에 list data를 적용한다.
@AndroidEntryPoint
class NoticeListActivity :
BindingActivity<ActivityNoticeListBinding>(R.layout.activity_notice_list) {
private lateinit var noticeAdapter: NoticeListAdapter
private val viewModel : NoticeViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
initNoticeList()
collectNoticeListState()
}
// 중략 ..
private fun collectNoticeListState() {
lifecycleScope.launch {
viewModel.noticeListState.collect { uiState ->
when (uiState) {
is UiState.Success -> {
noticeAdapter.submitList(uiState.data)
}
// 중략 ..
}
}
}
}
}
이렇게 코드를 작성하면 Response Json 파일로 실제 GET API를 이용한 것처럼 RecyclerView에 데이터가 뿌려지는 것을 볼 수 있다.
네트워크를 통해서 데이터를 받아오고 UI에서 해당 데이터를 그려내는 모습을 보고싶었기 때문에 이렇게 사용하였으나
실제로 MockWebServer는 프로덕션 환경이 아닌 테스트 환경에서만 사용해야 한다고 한다.
데이터를 받아오고 UI 를 그려내는 것 까지 테스트하고 싶다면
MockWebServer + Espresso 를 함께 사용하여서 테스트를 진행하면 된다.
기회가 된다면 Espresso와 MockWebServer를 활용하여 UI 테스트를 진행하도록 하겠다.
실제 현업에서 하나의 기능을 기획하고 개발할 때 API에 의존된 기능일 경우 모바일 단에서 API를 기다려야 하는 경우가 있다. 이럴 때 실제 개발 API가 아닌 명세만으로 Test를 해볼 수 있다는 점에서 유용하게 활용될 수 있을 것 같다.
'Android > 공부' 카테고리의 다른 글
[Android/공부] Compose로 UI 적용기 -1 (0) | 2024.10.14 |
---|---|
[Android] MockWebServer란? : okhttp mockwebserver (0) | 2024.09.25 |
[Android] XML 과 Compose 렌더링 방식의 차이 (0) | 2024.09.23 |
[Android/공부] 안드로이드 라이브러리 / AAR 만들기 (0) | 2024.07.17 |
[Android/공부] JNI 활용 안드로이드 디버깅 / USB 탐지 (JAVA) (0) | 2024.07.11 |