일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- 스택 자바 코드
- 안드로이드 유닛 테스트
- 안드로이드 유닛테스트란
- 플러터 설치 2022
- 안드로이드 os 구조
- 서비스 vs 쓰레드
- 스택 큐 차이
- 클래스
- 안드로이드 레트로핏 crud
- 객체
- 자바 다형성
- Rxjava Observable
- 멤버변수
- 안드로이드 유닛 테스트 예시
- android ar 개발
- 2022 플러터 설치
- rxjava hot observable
- jvm 작동 원리
- ar vr 차이
- 2022 플러터 안드로이드 스튜디오
- 안드로이드 레트로핏 사용법
- ANR이란
- jvm이란
- 안드로이드 라이선스 종류
- rxjava cold observable
- 큐 자바 코드
- 안드로이드 라이선스
- 서비스 쓰레드 차이
- rxjava disposable
- android retrofit login
- Today
- Total
나만을 위한 블로그
[Android] espresso를 사용한 UI 테스트(+리사이클러뷰) 본문
이전 글들에선 순수 자바 로직만 테스트했다면 이젠 UI도 테스트해야 한다.
UI를 테스트할 때는 주로 에스프레소라는 라이브러리를 사용하는데, 부분적으로 hamcrest라는 단위 테스트 라이브러리를 사용할 수도 있다. 이 포스팅에선 둘 다 사용하는 방향으로 작성했다.
먼저 앱 수준 gradle에 필요한 의존성들을 넣어준다. 안드로이드 디벨로퍼에서 발견한 의존성 문구들을 전부 가져왔고 오늘 날짜 기준으로 3.4.0이 최신이다.
testImplementation 'junit:junit:4.+'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
androidTestImplementation 'androidx.test.espresso:espresso-contrib:3.4.0'
androidTestImplementation 'androidx.test.espresso:espresso-intents:3.4.0'
androidTestImplementation 'androidx.test.espresso:espresso-accessibility:3.4.0'
androidTestImplementation 'androidx.test.espresso:espresso-web:3.4.0'
androidTestImplementation 'androidx.test.espresso.idling:idling-concurrent:3.4.0'
androidTestImplementation 'androidx.test:runner:1.4.0'
androidTestImplementation 'androidx.test:rules:1.4.0'
testImplementation "org.mockito:mockito-core:3.6.28"
testImplementation "org.mockito:mockito-inline:3.4.0"
testImplementation 'org.hamcrest:hamcrest:2.2'
다음은 UI 테스트를 위한 간단한 액티비티다. 버튼을 누르면 인텐트에 데이터를 넣어서 다른 화면으로 이동하는 로직만 있는 액티비티다.
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical">
<TextView
android:id="@+id/resultView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="160dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:text="텍스트"
android:textAppearance="?android:attr/textAppearanceLarge" />
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/second_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="250dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/resultView"
android:text="버튼"/>
</androidx.constraintlayout.widget.ConstraintLayout>
import android.content.Intent;
import android.os.Bundle;
import android.widget.Button;
import androidx.appcompat.app.AppCompatActivity;
public class SecondActivity extends AppCompatActivity {
private final String TAG = this.getClass().getSimpleName();
private Button button;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_second);
button = findViewById(R.id.second_button);
button.setOnClickListener(v -> {
Intent intent = new Intent(this, ResultActivity.class);
intent.putExtra("data", "data");
startActivity(intent);
});
}
}
그리고 리사이클러뷰에 필요한 파일들을 만들어준다. 테스트하기 위한 리사이클러뷰기 때문에 정말 대충 틀만 만들었다.
먼저 아이템 XML이다. recyclerview_item.xml 이란 이름으로 만들었다.
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/title_textview"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<TextView
android:id="@+id/message_textview"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</LinearLayout>
다음은 모델 클래스다.
public class ResultModel {
private String title;
private String message;
public ResultModel(String title, String message) {
this.title = title;
this.message = message;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
}
다음은 어댑터다.
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import java.util.List;
public class ResultAdapter extends RecyclerView.Adapter<ResultAdapter.ResultViewHolder> {
private Context context;
private List<ResultModel> list;
public ResultAdapter(Context context, List<ResultModel> list) {
this.context = context;
this.list = list;
}
@NonNull
@Override
public ResultAdapter.ResultViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(context).inflate(R.layout.recyclerview_item, parent, false);
return new ResultViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull ResultAdapter.ResultViewHolder holder, int position) {
final ResultModel model = list.get(position);
holder.title.setText(model.getTitle());
holder.message.setText(model.getMessage());
}
@Override
public int getItemCount() {
return list.size();
}
public class ResultViewHolder extends RecyclerView.ViewHolder {
private TextView title;
private TextView message;
public ResultViewHolder(@NonNull View view) {
super(view);
title = view.findViewById(R.id.title_textview);
message = view.findViewById(R.id.message_textview);
}
}
}
다음은 액티비티의 자바 파일과 XML 파일이다.
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ResultActivity">
<TextView
android:id="@+id/getTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="250dp"
android:text="글자"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:textSize="40sp"
android:textColor="@color/black"/>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/result_recyclerview"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@+id/getTextView"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
import androidx.appcompat.app.AppCompatActivity;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import android.content.Intent;
import android.os.Bundle;
import android.widget.TextView;
import java.util.ArrayList;
import java.util.List;
public class ResultActivity extends AppCompatActivity {
private TextView getTextView;
private RecyclerView recyclerView;
private ResultAdapter adapter;
private List<ResultModel> list;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_result);
list = new ArrayList<>();
getTextView = findViewById(R.id.getTextView);
recyclerView = findViewById(R.id.result_recyclerview);
for (int i = 0; i < 11; i++) {
list.add(new ResultModel("제목", "내용"));
}
adapter = new ResultAdapter(this, list);
recyclerView.setAdapter(adapter);
recyclerView.setLayoutManager(new LinearLayoutManager(this));
recyclerView.setHasFixedSize(true);
Intent intent = getIntent();
getTextView.setText(intent.getStringExtra("data"));
}
}
이제 테스트를 해보자. UI 테스트를 하려면 패키지 중 (androidTest)가 적힌 패키지 안에 자바 파일을 만들면 된다.
import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.action.ViewActions.click;
import static androidx.test.espresso.assertion.ViewAssertions.matches;
import static androidx.test.espresso.matcher.ViewMatchers.hasDescendant;
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
import static androidx.test.espresso.matcher.ViewMatchers.withId;
import static androidx.test.espresso.matcher.ViewMatchers.withText;
import static org.hamcrest.Matchers.instanceOf;
import androidx.test.espresso.contrib.RecyclerViewActions;
import androidx.test.ext.junit.rules.ActivityScenarioRule;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.LargeTest;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
@RunWith(AndroidJUnit4.class)
@LargeTest
public class SecondActivityTest {
@Rule
public ActivityScenarioRule<SecondActivity> activityScenarioRule = new ActivityScenarioRule<>(SecondActivity.class);
@Test
public void testActivity() {
onView(withId(R.id.second_button))
.perform(click());
onView(withId(R.id.getTextView))
.check(matches(withText("data")));
onView(withId(R.id.result_recyclerview))
.perform(RecyclerViewActions.scrollToPosition(5));
onView(withId(R.id.result_recyclerview))
.check(matches(isDisplayed()));
onView(withId(R.id.result_recyclerview))
.perform(RecyclerViewActions.scrollTo(hasDescendant(withText("제목"))).atPosition(0));
onView(withId(R.id.result_recyclerview))
.perform(RecyclerViewActions.scrollToHolder(instanceOf(ResultAdapter.ResultViewHolder.class)).atPosition(10));
}
}
이제 이 파일에 쓰여진 어노테이션, 메서드들이 뭔지 정리해본다.
- @RunWith : androidx.test 패키지를 사용하기 때문에 필요한 어노테이션이다. 이게 있냐 없냐에 따라 UI를 포함한 androidTest인지 일반 단위 테스트인지 구분한다. 내 기준으로 AndroidJUnit4가 최고 버전이기 때문에 이걸 썼다.
- @LargeTest : 메서드나 클래스에 사용할 수 있는 어노테이션이다. androidx.test 라이브러리로 작성된 테스트에 추가하는 걸 권장한다고 안드로이드 디벨로퍼에서 말하고 있다. 이것 말고도 @SmallTest, @MediumTest 어노테이션이 있는데 이 어노테이션의 구분은 아래와 같다.
- Small : 파일 시스템, 네트워크와 상호작용하지 않음
- Medium : 테스트를 실행 중인 상자(box)의 파일 시스템에 접근
- Large : 외부 파일 시스템, 네트워크 등에 접근
안드로이드 개발자 블로그에선 SmallTest는 100ms 미만, MediumTest는 2초 미만, 대규모 테스트는 120초 미만이 소요된다. 대충 @LargeTest 하나 박아두면 편할 것 같아서 난 저거 쓸란다.
- @Rule : 테스트할 액티비티를 연결하는 어노테이션이다. ActivityScenarioRule을 정의할 때 붙인다.
- ActivityScenarioRule : 테스트가 시작되면 주어진 액티비티를 시작하고 테스트가 종료되면 액티비티를 닫는다. 테스트가 끝나면 이 Rule은 아무 작업도 수행하지 않고, deprecated된 ActivityTestRule 대신 나온 것이다.
다음은 메서드 안에 사용한 static 메서드들이다.
- onView() : 액티비티 안의 뷰와 상호작용하기 위한 진입점. withId()와 같이 사용한다.
- withId() : R.id.xxx 식으로 리소스 id를 사용해서 테스트할 뷰를 찾는 메서드
- perform() : 매개변수로 받는 메서드를 수행하는 메서드
- click() : 뷰 클릭을 수행하는 메서드
- matches() : ViewMatcher 객체를 통해 지정한 뷰가 내가 원하는 상태인지 확인하는 메서드
- withText() : WithTextMatcher 객체를 생성해 리턴하는 메서드. 매개변수로 받은 텍스트를 이용해 대상을 찾는다
- isDisplayed() : 뷰의 가시성을 확인하는 메서드
다음은 리사이클러뷰에 사용한 메서드다.
- scrollTo() : 뷰를 통해 스크롤하는 메서드. 매개변수로 찾으려는 Matcher를 넣으면 그 뷰로 이동한다. 리사이클러뷰 특성 상 같은 조건의 뷰가 여러 개 나올 수 있어 atPosition()으로 특정 position을 선택해야 한다
- scrollToPosition() : position을 써서 스크롤하는 메서드. 그 위치에 아이템이 있다면 테스트는 성공한다
- scrollToHolder() : 뷰홀더를 통해 스크롤하는 메서드. hamcrest의 instanceOf() 또는 커스텀 Matcher를 인자로 넘기거고, scrollTo()처럼 atPosition()으로 하나를 선택해줘야 한다
'Android' 카테고리의 다른 글
[Android] 여러 체크박스들을 배열에 담아서 클릭 리스너 붙이는 법 (0) | 2022.03.31 |
---|---|
[Android] Gson이란? Gson 사용법 (0) | 2022.02.20 |
[Android] 단위 테스트 시 static 메서드를 테스트하는 방법 (0) | 2022.02.06 |
[Android] 단위 테스트 시 쉐어드 프리퍼런스를 사용하는 방법 (0) | 2022.02.05 |
[Android] 웹뷰에 표시된 Node.js GET 요청을 레트로핏으로 받아오기 (0) | 2022.02.03 |