일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 hot observable
- 객체
- 안드로이드 유닛 테스트 예시
- android retrofit login
- 안드로이드 os 구조
- 2022 플러터 설치
- jvm 작동 원리
- 스택 자바 코드
- 안드로이드 유닛테스트란
- 서비스 쓰레드 차이
- ar vr 차이
- rxjava disposable
- Rxjava Observable
- 안드로이드 라이선스
- android ar 개발
- 플러터 설치 2022
- rxjava cold observable
- 자바 다형성
- ANR이란
- 안드로이드 레트로핏 crud
- 스택 큐 차이
- 안드로이드 유닛 테스트
- 안드로이드 레트로핏 사용법
- 멤버변수
- 서비스 vs 쓰레드
- 클래스
- 2022 플러터 안드로이드 스튜디오
- jvm이란
- Today
- Total
나만을 위한 블로그
[Android] 유닛 테스트란? 유닛 테스트 예시(JAVA) 본문
※ 이 글에서 사용한 안드로이드 스튜디오 버전은 4.2.2다.
위키백과에서 말하는 유닛(단위) 테스트란 아래와 같다.
https://ko.wikipedia.org/wiki/%EC%9C%A0%EB%8B%9B_%ED%85%8C%EC%8A%A4%ED%8A%B8
유닛 테스트는 소스코드의 특정 모듈이 의도대로 정확히 작동하는지 검증하는 절차다. 즉, 모든 함수와 메서드에 대한 테스트 케이스를 작성하는 절차를 말한다. 이를 통해 언제라도 코드 변경으로 문제 발생 시, 단시간 내에 이를 파악하고 바로잡을 수 있게 해준다. 이상적으로 각 테스트 케이스는 분리돼야 한다. 이를 위해 가짜 객체(Mock object)를 만드는 것도 좋은 방법이다. 유닛 테스트는 개발자 뿐 아니라 더 심도있는 테스트를 위해 테스트에 의해 실행되기도 한다.
https://en.wikipedia.org/wiki/Unit_testing
절차적 프로그래밍에서 단위는 전체 모듈일 수 있지만 일반적으로 개별 기능이나 절차다. 객체지향 프로그래밍에선 단위가 클래스, 메서드 같은 전체 인터페이스다. 테스트 가능한 가장 작은 단위에 대한 테스트를 먼저 작성한 다음 이들 간의 복합 동작을 작성해서 복잡한 앱에 대한 포괄적인 테스트를 구축할 수 있다.
발생할 수 있는 문제를 격리하려면 각 테스트 케이스를 독립적으로 테스트해야 한다. 메서드 스텁, 모의 객체, 가짜 및 테스트 장치와 같은 대체물을 써서 모듈을 격리된 상태에서 테스트할 수 있다. 개발하는 동안 소프트웨어 개발자는 기준 또는 양호한 것으로 알려진 결과를 테스트에 코딩해 단위의 정확성을 검증할 수 있다...(중략)...단위 테스트에 적합한 매개변수는 수동으로 제공되거나 경우에 따라 테스트 프레임워크에서 자동으로 생성된다.
단위 테스트의 목표는 프로그램의 각 부분을 분리하고 개별 부분이 올바른지 보여주는 것이다. 단위 테스트는 코드 조각이 충족해야 하는 엄격한 서면 계약을 제공하고 결과적으로 여러 이점을 제공한다. 단위 테스트는 개발 주기 초기에 문제를 찾는다. 여기엔 프로그래머 구현의 버그, 장치 사향의 결함, 누락 부분이 모두 포함된다. 철저한 테스트 세트를 작성하는 프로세스는 작성자가 입력, 출력, 오류 조건을 통해 생각하도록 하여 장치의 원하는 동작을 명확하게 정의한다. 코딩이 시작되기 전이나 코드가 처음 작성될 때 버그를 찾는 비용은 나중에 버그를 감지하고 고치는 비용보다 낮다. TDD에선 코드가 작성되기 전에 단위 테스트가 생성된다. 테스트를 통과하면 해당 코드가 완료된 것으로 간주된다.
그럼 안드로이드에선 어떻게 단위 테스트를 할까?
자바 클래스 파일들을 만드는 폴더 밑을 보면 2개의 패키지가 있는 걸 볼 수 있다.
위의 패키지는 UI 테스트용 파일을 만드는 곳이고, 밑의 패키지는 로직을 테스트할 때 사용하는 곳이다.
왜 이렇게 나눠져 있냐면 두 패키지 안의 파일들이 실행 방법이 각각 다르기 때문이다.
test : 로컬에서 컴파일 후 JVM에서 바로 실행
androidTest : 프로젝트 빌드 -> 에뮬레이터 실행 -> apk 설치 (test에서 쓰이는 코드를 쓸 수 있긴 한데 속도가 느려 비효율적)
MVVM 패턴이 왜 만들어졌는지 찾아보면 뷰와 뷰모델 간의 의존성을 떨어뜨렸다 어쩌고 하는데 이것은 전부 test 패키지에서 코드를 작성하기 위함이다. test 패키지에 만들어진 파일들은 컨텍스트 같이 앱 구동 시 얻을 수 있는 것들을 접근하지 않는 걸 원칙으로 한다.
그래서 test 패키지를 유닛(단위) 테스트, androidTest 패키지를 Instrumented Test라고 한다.
패키지가 다른 만큼 두 패키지 안에 작성되는 코드들도 작성법이 조금씩 다르다.
먼저 유닛 테스트에 작성하는 예제부터 확인한다. Calculator라는 클래스를 만들고 2개의 메서드를 정의한다.
public class Calculator {
public int add(int a, int b) {
return a + b;
}
public int sub(int a, int b) {
return a - b;
}
}
이제 이 메서드들을 테스트하기 위해 (test)가 써진 패키지에 파일을 하나 생성한다. 그 안에 원래 있는 파일을 쓸 수도 있지만 그 파일 하나에서 모든 테스트를 수행할 것도 아니기에 파일을 만들어 테스트한다.
import org.junit.Before;
import org.junit.Test;
import static org.junit.Assert.*;
public class CalculatorTest {
private Calculator calculator;
@Before
public void setUp() throws Exception {
calculator = new Calculator();
}
@Test
public void testAdd() {
assertEquals(3, calculator.add(1, 2));
}
@Test
public void subTest() {
assertEquals(-1, calculator.sub(1, 2));
}
}
파일명에는 test라는 글자가 들어가야 하고 메서드에는 test를 붙이지 않아도 @Test 어노테이션이 붙어 있다면 테스트는 정상적으로 수행된다. 하지만 이 테스트 파일들에도 작성법의 관례 같은 게 있다고 하니 찾아보고 적용해보는 것도 좋을 듯하다. 여기선 test를 어디에 써도 된다는 걸 보여주기 위해 test 위치를 서로 다르게 했다.
이제 public 왼쪽을 보면 화살표가 보이는데 이걸 눌러 해당 파일을 실행시키면 테스트가 진행된다. 메서드가 2개 뿐이기 때문에 Calculator 파일의 테스트는 금방 끝날 것이다.
subTest와 testAdd 메서드가 모두 정상 작동했다면 이렇게 나온다. 이름들을 각각 클릭해서 자세한 내용을 확인할 수 있다.
테스트 메서드 안의 값을 실패하도록 수정해보면 이렇게 나온다.
testAdd() 안의 값을 바꾸자 subTest()는 성공해서 아무것도 안 나오지만 testAdd()는 뭔가 내용들이 많이 써져 있는 걸 볼 수 있다.
예상되는 결과값은 3인데 실제로 메서드를 돌려 보니 4가 나왔다고 알려주는 것이다.
이런 식으로 자신이 짠 로직을 테스트할 수 있다.
이번엔 UI 테스트를 해보자. 먼저 앱 수준 gradle 파일에 아래 의존성들을 추가해준다.
runner와 rules는 오늘자 기준으로 1.4.0이 최신 버전이다.
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
androidTestImplementation 'androidx.test:runner:1.4.0'
androidTestImplementation 'androidx.test:rules:1.4.0'
완성된 형태는 아래와 같다.
그 다음 메인 액티비티의 XML과 자바 파일이다.
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
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=".MainActivity">
<TextView
android:id="@+id/select_language"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:layout_marginTop="28dp"
android:text="Select Your Preferred Language"
android:textSize="19sp"
android:textStyle="bold" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/select_language"
android:id="@+id/linear_layout"
android:layout_centerHorizontal="true"
android:layout_margin="22dp"
android:orientation="vertical">
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="16dp"
android:id="@+id/english"
android:text="English"
android:onClick="onClick"
android:textAllCaps="true"
android:textColor="#0F9D58"
android:textStyle="bold" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="16dp"
android:id="@+id/german"
android:onClick="onClick"
android:text="German"
android:textAllCaps="true"
android:textColor="#0F9D58"
android:textStyle="bold" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/french"
android:padding="16dp"
android:onClick="onClick"
android:text="French"
android:textAllCaps="true"
android:textColor="#0F9D58"
android:textStyle="bold" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="#0F9D58"
android:id="@+id/hindi"
android:onClick="onClick"
android:text="Hindi"
android:padding="16dp"
android:textAllCaps="true"
android:textStyle="bold"/>
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="#0F9D58"
android:id="@+id/urdu"
android:onClick="onClick"
android:text="Urdu"
android:padding="16dp"
android:textAllCaps="true"
android:textStyle="bold"/>
</LinearLayout>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/chosen"
android:layout_centerHorizontal="true"
android:layout_marginTop="28dp"
android:text="Your Chosen Language is : "
android:textSize="19sp"
android:textStyle="bold"
android:layout_below="@+id/linear_layout"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/preferred_language"
android:layout_centerHorizontal="true"
android:layout_marginTop="28dp"
android:hint="-------------"
android:textSize="19sp"
android:textStyle="bold"
android:layout_below="@+id/chosen"/>
</RelativeLayout>
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.TextView;
public class MainActivity extends AppCompatActivity {
TextView preferred_language;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
preferred_language = findViewById(R.id.preferred_language);
}
public void onClick(View view) {
switch (view.getId()) {
case R.id.english:
preferred_language.setText("English");
break;
case R.id.french:
preferred_language.setText("French");
break;
case R.id.german:
preferred_language.setText("German");
break;
case R.id.hindi:
preferred_language.setText("Hindi");
break;
case R.id.urdu:
preferred_language.setText("Urdu");
break;
}
}
}
실행하면 아래 화면이 나온다.
이제 이 화면에서 버튼이 눌리는지, 눌린 버튼의 글자들이 editText에 set되는지 테스트 파일을 만들어 테스트한다. (androidTest)가 써진 패키지에 자바 클래스 파일을 만들어 아래 코드를 복붙한다.
파일명은 아무렇게나 하되 @Before을 제외한 다른 코드들은 꼭 들어가야 한다.
import androidx.test.ext.junit.rules.ActivityScenarioRule;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
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.withId;
import static androidx.test.espresso.matcher.ViewMatchers.withText;
@RunWith(AndroidJUnit4.class)
public class MainActivityTest {
@Rule
public ActivityScenarioRule<MainActivity> activityScenarioRule = new ActivityScenarioRule<>(MainActivity.class);
@Before
public void beforeTest() {
System.out.println("before()");
}
@After
public void afterTest() {
System.out.println("after()");
}
@Test
public void selectLanguageAndCheck() {
onView(withId(R.id.german))
.perform(click());
onView(withId(R.id.preferred_language))
.check(matches(withText("German")));
}
}
생소한 어노테이션과 메서드, 클래스명이 보인다. 하나씩 확인해보자.
먼저 @RunWith은 테스트 클래스의 시작 부분에 추가해야 하는 어노테이션이다. JUnit 3 또는 4 버전으로 UI 테스트를 진행한다면 추가해야 하는 어노테이션으로 근거는 아래의 디벨로퍼 링크다.
https://developer.android.com/training/testing/unit-testing/instrumented-unit-tests?hl=ko
내가 만든 프로젝트에서 JUnit 3을 쓰는지 4를 쓰는지 확인하는 방법은 의존성 문구들을 복붙해 넣는 앱 수준 gradle 파일을 확인해보면 된다.
4.2.2 버전 기준으로 JUnit 4 버전을 사용하는 걸 확인했다. 만약 JUnit 5를 쓴다면 @RunWith 어노테이션은 안 써도 될 것이다.
@Rule은 @Before 이전에 실행되고 @After 이후 종료되는 어노테이션으로, 테스트 클래스에서 동작 방식을 재정의하거나 추가하는 걸 말한다.
그리고 ActivityScenarioRule 또는 ActivityTestRule 중에서 하나를 선택할 수 있는데, 현재 ActivityTestRule을 사용하면 deprecated 됐다는 삭선 표시가 나온다. 더 이상 지원하지 않는 테스트 규칙이니 ActivityScenarioRule을 쓰는 게 낫겠다.
ActivityScenarioRule의 설명과 기타 내용들은 아래 링크로 가면 자세히 볼 수 있다.
https://developer.android.com/reference/androidx/test/ext/junit/rules/ActivityScenarioRule
ActivityScenarioRule은 테스트가 시작되기 전에 주어진 액티비티를 시작하고 테스트 후에 닫는다. 테스트에서 액티비티를 수동으로 완료할 수 있다. 테스트 후 이 Rule은 아무 작업도 수행하지 않는다. 이 Rule은 현재 쓰이지 않는 ActivityTestRule의 업그레이드된 버전이다
다시 코드를 보면
@Rule
public ActivityScenarioRule<MainActivity> activityScenarioRule = new ActivityScenarioRule<>(MainActivity.class);
좌항의 ActivityScenarioRule 제네릭 안에 메인 액티비티가 들어가 있는게 보인다. 그리고 우항에서 메인 액티비티로 ActivityScenarioRule을 초기화해 객체를 만드는 걸 볼 수 있다.
그러나 위의 코드에선 이 객체를 사용하지 않았는데, 만약 이 초기화 코드 없이 테스트를 실행하면 아래의 에러가 발생하게 된다.
androidx.test.espresso.NoActivityResumedException: No activities found. Did you forget to launch the activity by calling getActivity() or startActivitySync or similar?
-> androidx.test.espresso.NoActivityResumedException: 활동을 찾을 수 없습니다. getActivity() 또는 startActivitySync 등을 호출하여 활동을 시작하는 것을 잊으셨습니까?
내 안드로이드 스튜디오에선 아래와 같이 나온다.
30번째 줄은 @Test 안에서 첫 번째 perform()이다. 즉, 저 메서드를 실행하려다가 에러가 발생해서 안드로이드 스튜디오가 저런 에러를 뿜으며 테스트를 강제종료한 것이다. 왜일까? 저 쓰지도 않는 코드 1줄이 뭐가 중요하다고?
위에서 건 링크의 내용 중 이런 내용이 있다.
getScenario()를 통해 ActivityScenario 인스턴스에 접근할 수 있다
유추해 보면 비록 테스트 클래스 파일에서 ActivityScenarioRule 객체를 사용하지 않더라도 객체화해두는 것만으로 UI 테스트를 진행하기 위해 액티비티를 시작할 수 있는 것 같다.
getScenario()의 메서드에 대한 설명은 아래 링크로 가면 된다. 아직 ActivityScenarioRule 페이지를 켜놓고 있다면 스크롤을 밑으로 내리다보면 볼 수 있다.
아무튼 UI 테스트를 진행하기 위해선 저 코드를 꼭 써놔야 한다.
@Before은 테스트 시작 전에 호출해야 하는 내용들을 쓰는 곳이다. 테스트를 위해 초기화해야 하는 작업(리스트 등)을 진행하는 곳이다.
이와 유사한 어노테이션으로 @BeforeClass가 있는데 @Before는 @Test가 붙은 테스트 메서드가 2개라면 2번 실행되고, 4개라면 총 4번 실행되는 어노테이션이다. 즉 여러 번 실행될 수 있는 어노테이션인 반면, @BeforeClass는 단 한 번만 실행되는 어노테이션이다.
그래서 @Before 안에서는 리스트 또는 클래스의 객체화를 주로 진행하고, @BeforeClass 안에서는 static한 객체나 DB 객체를 초기화하는 데 사용한다.
@After는 테스트 케이스가 끝나면 호출해야 하는 내용들을 쓰는 곳이다. @Test가 끝난 후 실행되는 어노테이션이며 테스트가 실패하더라도 실행된다.
@Before와 유사하게 @AfterClass가 있는데 이것도 마지막에 단 한 번만 실행되는 어노테이션이다.
위의 내용을 정리하면 아래와 같다.
- @Before : 테스트 시작 전에 리스트 초기화, 클래스 객체화 등을 진행하는 곳. @Test가 여러 개라면 그 갯수만큼 실행된다
- @BeforeClass : static한 객체, DB 객체 초기화 코드들을 작성하는 곳. 단 한 번만 실행된다
- @After : 테스트 케이스가 끝나면 호출해야 하는 코드들을 작성하는 곳. @Test가 끝난 후 실행되며 테스트가 실패해도 실행된다. 테스트 메서드 이후 변수 재설정 등에 사용할 수 있다.
- @AfterClass : 단 한 번만 실행되는 어노테이션. 모든 테스트가 끝난 후 실행할 코드를 입력한다.
아래는 테스트 파일에서 쓰이는 어노테이션들의 실행 순서다.
@BeforeClass -> @Rule -> @Before -> @Test -> @Ignores -> @After -> @AfterClass
마지막으로 @Test는 ActivityScenarioRule 제네릭에 사용한 액티비티 안의 뷰들이 작동하는지 테스트하기 위해 사용하는 어노테이션이다.
코드에서 버튼 하나와 editText를 대상으로 테스트를 진행하는 걸 볼 수 있다.
이제 클래스명 왼쪽의 특이한 모양의 화살표를 누르면 테스트가 시작된다.
코드에 별다른 수정을 하지 않았다면 아래와 비슷한 화면이 보일 것이다.
Test Passed라는 녹색 창과 그 바로 위의 3단 탭의 녹색 v 표시가 보일텐데, 저 중 가운데 탭 또는 맨 밑의 탭을 누르면 Device Info라는 탭이 우측에 생긴다. 이걸 눌러 테스트를 진행한 기기의 안드로이드 OS 버전, RAM 등의 대략적인 정보들을 확인할 수 있다. 에뮬레이터로 진행하면 아래와 같이 나온다.
다음은 @Test가 써진 테스트 메서드 안에서 사용한 메서드들에 대해 확인해보자. 사용된 메서드들을 정리하면 아래와 같다.
- onView()
- withId()
- perform()
- closeSoftKeyboard()
- typeText()
- click()
- check()
- matches()
- withText()
이 메서드들은 모두 에스프레소 프레임워크에서 제공하는 메서드들이다. 사용하려면 import static으로 시작하는 임포트 문구들을 넣어줘야 한다. 자바 파일 상단에 적힌 임포트 문구들을 자세히 보고 여러 임포트 문구 중 하나를 골라야 하는 경우 참고해서 넣어주면 된다.
각 메서드들이 무슨 역할을 하는지는 아래 링크에 잘 나와있다.
참고한 사이트)
https://eodevelop.tistory.com/37
https://jae-young.tistory.com/43
'Android' 카테고리의 다른 글
[Android] 서비스 vs 쓰레드 (0) | 2021.10.14 |
---|---|
[Android] ANR이란? (0) | 2021.10.14 |
[Android] 안드로이드 OS의 구조 (0) | 2021.10.08 |
[Android] 인텐트란? (0) | 2021.10.07 |
[Android] 액티비티 vs 프래그먼트 차이 (0) | 2021.10.07 |