일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
- Rxjava Observable
- 안드로이드 유닛 테스트
- 2022 플러터 안드로이드 스튜디오
- 스택 자바 코드
- 안드로이드 라이선스
- 서비스 쓰레드 차이
- ANR이란
- 서비스 vs 쓰레드
- 큐 자바 코드
- 안드로이드 os 구조
- ar vr 차이
- rxjava hot observable
- 안드로이드 유닛 테스트 예시
- 멤버변수
- 클래스
- jvm 작동 원리
- rxjava cold observable
- jvm이란
- 스택 큐 차이
- 안드로이드 유닛테스트란
- android ar 개발
- 2022 플러터 설치
- 플러터 설치 2022
- rxjava disposable
- 안드로이드 라이선스 종류
- 안드로이드 레트로핏 crud
- android retrofit login
- 객체
- 자바 다형성
- 안드로이드 레트로핏 사용법
- Today
- Total
나만을 위한 블로그
[Android] 리사이클러뷰 UI test 작성하기 본문
예전에 자바로 안드로이드 리사이클러뷰의 UI test를 작성하는 예시를 포스팅한 적이 있다.
https://onlyfor-me-blog.tistory.com/446
이번엔 코틀린으로 리사이클러뷰 UI test를 작성하는 예시를 정리한다. 먼저 아래 라이브러리를 추가해야 한다. 리사이클러뷰 UI test에 필요한 클래스가 이 라이브러리를 추가해야 사용할 수 있기 때문이다.
androidTestImplementation 'androidx.test.espresso:espresso-contrib:3.2.0'
이제 UI test에 사용할 액티비티를 만들어야 한다. 대충 아래 코드들을 적절하게 프로젝트에 복붙한다.
<!-- text_row_item.xml -->
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable
name="text"
type="String" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="72dp"
android:layout_marginHorizontal="16dp">
<TextView
android:id="@+id/textview"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{text}"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".recyclerview.uitest.RecyclerViewUiTestActivity">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_test"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
import android.os.Bundle
import androidx.recyclerview.widget.LinearLayoutManager
import com.example.kotlinprac.BaseActivity
import com.example.kotlinprac.R
import com.example.kotlinprac.databinding.ActivityRecyclerViewUiTestBinding
class RecyclerViewUiTestActivity :
BaseActivity<ActivityRecyclerViewUiTestBinding>(R.layout.activity_recycler_view_ui_test) {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val dataSet: MutableList<String> = mutableListOf()
for (i in 0 until 10) {
dataSet.add("글자 $i")
}
bind {
rvTest.apply {
layoutManager = LinearLayoutManager(this@RecyclerViewUiTestActivity)
val adapter = CustomAdapter(dataSet)
setAdapter(adapter)
}
}
}
}
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.example.kotlinprac.databinding.TextRowItemBinding
class CustomAdapter(
private val list: List<String>
) : RecyclerView.Adapter<CustomAdapter.ViewHolder>() {
private lateinit var binding: TextRowItemBinding
inner class ViewHolder(binding: TextRowItemBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(item: String) {
binding.text = item
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
binding =
TextRowItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return ViewHolder(binding)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = list[position]
holder.bind(item)
}
override fun getItemCount(): Int = list.size
}
실행 후 import문을 적절히 수정하고, 실행되는 걸 확인한 다음 테스트 코드를 작성한다.
UI test기 때문에 androidTest가 적힌 패키지에 테스트 파일을 작성해야 하는 것에 주의한다.
import android.view.View
import androidx.recyclerview.widget.RecyclerView
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.contrib.RecyclerViewActions
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import com.example.kotlinprac.recyclerview.uitest.RecyclerViewUiTestActivity
import org.hamcrest.Description
import org.hamcrest.Matcher
import org.hamcrest.TypeSafeMatcher
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
@LargeTest
class RecyclerViewTest {
@JvmField
@Rule
val activityScenarioRule: ActivityScenarioRule<RecyclerViewUiTestActivity> =
ActivityScenarioRule(RecyclerViewUiTestActivity::class.java)
companion object {
const val ITEM_BELOW_THE_FOLD = 5;
}
@Test
fun recyclerView_isDisplayed() {
onView(withId(R.id.rv_test)).check(matches(isDisplayed()))
}
@Test
fun itemWithText_doesNotExist() {
onView(withId(R.id.rv_test))
.perform(RecyclerViewActions.actionOnItemAtPosition<RecyclerView.ViewHolder>(ITEM_BELOW_THE_FOLD, click()))
val itemText = "글자 5"
onView(withText(itemText)).check(matches(isDisplayed()))
}
@Test
fun firstAndLastItemsInRecyclerView_checkAllDisplayed() {
var itemCount = 0
activityScenarioRule.scenario.onActivity { activity ->
val recyclerView: RecyclerView = activity.findViewById(R.id.rv_test)
itemCount = recyclerView.adapter?.itemCount ?: 0
}
if (itemCount > 0) {
onView(withId(R.id.rv_test)).perform(RecyclerViewActions.scrollToPosition<RecyclerView.ViewHolder>(0))
onView(withId(R.id.rv_test)).check(matches(atPosition(0, isDisplayed())))
onView(withId(R.id.rv_test)).perform(RecyclerViewActions.scrollToPosition<RecyclerView.ViewHolder>(itemCount - 1))
onView(withId(R.id.rv_test)).check(matches(atPosition(itemCount - 1, isDisplayed())))
}
}
private fun atPosition(position: Int, itemMatcher: Matcher<View>): Matcher<View> {
return object : TypeSafeMatcher<View>() {
override fun describeTo(description: Description) {
description.appendText("has item at position $position: ")
itemMatcher.describeTo(description)
}
override fun matchesSafely(view: View): Boolean {
val viewHolder = (view as RecyclerView).findViewHolderForAdapterPosition(position)
return itemMatcher.matches(viewHolder?.itemView)
}
}
}
}
보다시피 이 테스트 파일에선 총 3가지의 테스트를 수행한다.
- 리사이클러뷰가 화면에 표시되는지 확인
- 5번 인덱스의 리사이클러뷰 아이템이 "글자 5"인지 확인
- 첫 아이템, 마지막 아이템이 화면에 표시되는지 확인
리사이클러뷰가 표시되는지 확인하는 것과 리사이클러뷰 안의 모든 아이템들이 표시되는지 확인하는 건 엄연히 다르다.
그 외에는 RecyclerViewActions 클래스를 통해 여러 함수들을 호출해서 클릭 액션과 스크롤 액션을 검증하는 테스트 코드들이기 때문에 넘기고, atPosition()을 짚고 넘어간다.
atPosition()은 리사이클러뷰 안의 특정 위치에 있는 아이템이 2번째 매개변수(itemMatcher)와 일치하는지 검증하기 위한 Matcher<View>를 리턴하는 함수다.
position은 당연히 리사이클러뷰 안에서 검증하고자 하는 아이템의 위치고, itemMatcher로는 isDiaplayed(), hasFocus() 등을 넣을 수 있다. 함수들의 종류는 ViewMatchers 클래스를 통해 호출할 수 있으니 아래 공식문서를 참고한다.
https://developer.android.com/reference/androidx/test/espresso/matcher/ViewMatchers
0번 인덱스와 마지막 인덱스에 각각 아이템이 표시되는지 확인하기 위해 isDiaplayed()만 사용하고 있지만, 다른 ViewMatchers 함수를 넣어서 다른 내용을 검증하는 함수를 만들 수도 있다. 테스트 시나리오를 작성한 다음 공식문서에서 어떤 함수를 쓰면 좋을지 찾아보며 구현해 나가면 될 것이다.
'Android' 카테고리의 다른 글
[Android] 폴더블 기기 펼침 여부 확인하는 방법 (Jetpack WindowManager) (0) | 2023.11.08 |
---|---|
[Android] WebView Cache 전략 설정 방법 (0) | 2023.10.22 |
[Android] UI test에서 TedBottomPicker 사용하는 법 (0) | 2023.09.21 |
[Android] CameraX 코드랩 뜯어보기 - 4 - (完) (0) | 2023.09.19 |
[Android] BottomNavigationView의 탭 선택된 효과 최대한 없애기 (0) | 2023.09.15 |