이번 영상에서는 안드로이드 스튜디오에서 단위 테스트를 수행하는 기초적인 방법에 대해 알아보도록 하겠습니다.
안드로이드 스튜디오에서 새로운 프로젝트를 생성하면 테스트에 관련된 기본 디펜던시들이 그래들에 자동으로 추가됩니다.
dependencies {
...
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.3")
androidTestImplementation("androidx.test.espresso:espresso-core:3.4.0")
}
이 때 testImplementation과 androidTestImplementation 키워드를 볼 수 있는데요, JVM만으로 수행할 수 있는 로컬 단위 테스트에 사용되는 디펜던시는 testImplementation으로 추가하고, 안드로이드 에뮬레이터나 실기기를 필요로 하는 계측 단위 테스트에 사용되는 디펜던시는 androidTestImplementation으로 추가하게 됩니다. 그러니까 이 기본 프로젝트에는 로컬 테스트용으로 JUnit4가 추가되어 있고, 계측 테스트용으로 JUnit Extensions와 Espresso가 추가되어 있는 것입니다.
그럼 여기에 Assertion을 더 읽기 용이하게 해 주는 Truth와, 테스트를 크기에 따라 필터링할 수 있게 하는 Runner를 로컬 테스트 용으로 추가하도록 하겠습니다.
dependencies {
+ testImplementation("androidx.test.ext:truth:1.4.0")
+ testImplementation("androidx.test:runner:1.4.0")
}
그럼 사칙연산을 수행하는 단순한 계산기 클래스를 만들고 이 클래스의 단위 테스트를 해 보겠습니다.
class Calculator {
fun addition(x: Int, y: Int): Int {
return x + y
}
fun subtraction(x: Int, y: Int): Int {
return x - y
}
fun multiplication(x: Int, y: Int): Int {
return x * y
}
fun division(x: Int, y: Int): Int {
return x / y
}
}
class 이름 위에 커서를 대고 오른쪽 클릭 > Generate > Test.. 를 선택하면 Create Test메뉴가 표시됩니다. Class name은 테스트 할 클래스 이름뒤에 Test를 붙인 이름으로 자동정의되는데, Testing Library는 JUnit4를 선택합니다. 그리고나서 OK를 클릭하면 이번엔 Choose Destination Directory 메뉴가 표시됩니다.
테스트 파일이 저장되는 디렉토리는 androidTest와 unitTest중에서 고를 수 있는 데 로컬 테스트라면 unitTest, 계측 테스트라면 androidTest 디렉토리에 저장합니다. 이 테스트는 안드로이드 의존성이 없는 로컬 테스트이므로 unitTest에 저장하면 되겠죠.
CalculatorTest 클래스는 @SmallTest로 분류하겠습니다. 어노테이션을 붙여주면 그래들에서 해당 테스트만을 선택적으로 테스트 할 수 있게 됩니다.
$ adb shell am instrument -w \
> -e package com.qualitybitz.booksearchapp \
> -e size small \
> com.android.test/android.test.InstrumentationTestRunner
그리고 내용을 작성하는데 우선은 Calculator 클래스의 인스턴스를 만들어 주어야겠죠. 그리고 addition기능을 테스트 하기 위한 메소드를 하나 만들어줍니다. 로컬 테스트용 메소드의 특이한 점은 함수 이름에 백틱을 붙여주면 메소드 이름을 문장처럼 정의할 수 있다는 점입니다.
꼭 따라야 할 필요는 없지만 여기서는 메소드의 내용을 Given-When-Then 법칙에 따라 정의해 보겠습니다. Given에서 테스트 컨디션을 정의하고, When에서 동작을 정의합니다. 그리고 마지막으로 Then에서 동작결과가 기대한 값이 나왔는지를 확인하면 되겠죠. assertThat 함수는 JUnit4의 것이 아닌 Truth 라이브러리를 사용하도록 변경 해 줍니다.
@SmallTest
class CalculatorTest {
private val calculator = Calculator()
@Test
fun `additional function test`() {
// Given
val x = 4
val y = 2
// When
val result = calculator.addition(x, y)
// Then
assertThat(result).isEqualTo(6)
}
}
그리고 Run CalculatorTest를 실행하면 단위 테스트가 수행됩니다. 단위 테스트는 안드로이드 프레임워크를 사용하지 않고 JVM 위에서 실행되기 때문에 빠르게 종료됩니다.
다음은 모든 메소드를 테스트 해 보겠습니다. additional function test 메소드를 복사해서 각 메소드의 테스트를 만들고 테스트를 실행하면 문제없이 통과하는 것을 알 수 있습니다.
@SmallTest
class CalculatorTest {
private val calculator = Calculator()
@Test
fun `additional function test`() {
// Given
val x = 4
val y = 2
// When
val result = calculator.addition(x, y)
// Then
assertThat(result).isEqualTo(6)
}
+ @Test
+ fun `subtraction function test`() {
+ // Given
+ val x = 4
+ val y = 2
+
+ // When
+ val result = calculator.subtraction(x, y)
+
+ // Then
+ assertThat(result).isEqualTo(2)
+ }
+
+ @Test
+ fun `multiplication function test`() {
+ // Given
+ val x = 4
+ val y = 2
+
+ // When
+ val result = calculator.multiplication(x, y)
+
+ // Then
+ assertThat(result).isEqualTo(8)
+ }
+
+ @Test
+ fun `division function test`() {
+ // Given
+ val x = 4
+ val y = 2
+
+ // When
+ val result = calculator.division(x, y)
+
+ // Then
+ assertThat(result).isEqualTo(2)
+ }
}
다음은 테스트를 실패시켜 보겠습니다. division 테스트에서 y = 0을 넣으면 테스트가 실패하게 됩니다. 그럼 Calculator에서 y = 0에 대한 예외처리를 해 주어야 합니다. 여기서는 0으로 나눌 경우 null을 반환하도록 하겠습니다. 그리고 테스트 메소드의 isEqualTo가 null인지 확인하면 테스트가 성공하게 됩니다.
class Calculator {
...
+ fun division(x: Int, y: Int): Int? {
+ try {
+ return x / y
+ } catch (e:Exception) {
+ return null
+ }
+ }
}
지난 강의에서 F.I.R.S.T Principles을 설명하면서 테스트는 다른 테스트 케이스에 의존하지 않아야 한다고 설명했습니다. 그런데 우리의 코드는 어떤가 하면, calculator 인스턴스를 모든 테스트에서 공유하고 있습니다.
어떤 테스트가 공유되는 인스턴스의 내용을 변경하면 다른 테스트가 그 인스턴스를 참조했을 때 원래는 성공이어야 할 테스트가 실패할 수도 있게 됩니다. 따라서 calculator 인스턴스는 테스트마다 독립된 객체를 사용해야 합니다. 그러기 위해서는 private val calculator = Calculator()를 모든 테스트마다 삽입하는 방법도 있겠지만, JUnit4에는 이런 단순과정을 해결할 수 있는 annotation이 있습니다. 우선 코드를 써 보겠습니다.
@SmallTest
class CalculatorTest {
// private val calculator = Calculator()
private lateinit var calculator: Calculator
@Before
fun setUp() {
calculator = Calculator()
}
@After
fun tearDown() {
}
}
@Before 블럭에는 각 테스트의 실행 직전에 수행되어야 하는 작업을 적으면 됩니다. 여기서는 calculator 인스턴스를 새로 만드는 작업을 정의했습니다. @After를 사용하면 테스트 종료 직후에 수행할 작업을 정의할 수 있는데요, 인스턴스를 삭제한다거나 데이터베이스를 닫는 등의 작업을 여기서 정의하면 됩니다. 테스트를 실행해 보면 모두 문제없이 성공하는 것을 확인할 수 있습니다.
@Before와 @After는 개별 테스트가 수행될 때마다 실행되는 Annotation인데요, 전체 테스트 사이클 중에 한 번씩만 수행되는 @BeforeClass와 @AfterClass라는 Annotation도 있습니다. 구체적인 실행순서에 대해서는 다음 그림을 참조하시기 바랍니다.

이렇게 해서 안드로이드 스튜디오에서 기초적인 단위 테스트를 수행하는 방법에 대해 알아보았습니다.