1. Unit Testing?

Unit Testing은 Software Testing을 위한 방법 중 하나로, Source의 각 Unit(대개 객체 지향에서는 Class)에 대한 Testing Code를 구현하고 실행함으로서, 예측 가능한 문제를 줄이고 Software Quality를 높이는데 도움을 줍니다.1

Mobile OS인 Android에서 동작하는 App을 위한 Unit Testing에는 두 가지 종류가 있습니다. 하나는 Android System과는 별개로 동작하는 것들을 Test하는 Local Unit Testing이고, 또 다른 하나는 Android System과 연관되어 동작하는 것들을 Test하기 위한 Instrumented Unit Testing입니다.

이번 Post에서는 Android Studio에서 생성한 Project에 이 두 가지 유형의 Unit Testing을 적용하고 Test하는 방법에 대해서 알아보도록 하겠습니다.

2. Local Unit Testing

Local Unit Testing은 Android System에 대한 의존성이 없거나 단순할 경우에 사용할 수 있는 Unit Testing 유형입니다. 기존에 Android App을 위한 Unit Testing은 Emulator나 Device에서 직접 실행하는 것이라 Test 속도가 느렸지만, 이 Local Unit Testing을 사용하면 개발자의 System에서 Test가 진행되기 때문에 Test 실행 시간을 줄일 수 있습니다.2

하지만, Android System에서 Test하는 것이 아니기 때문에 Android와 의존성이 거의 없는 Model Level의 Unit을 위해서 사용해야 하며, 또한 Android API의 Abstract Class에 대한 Mock Object를 생성하여 해당 Class에 대한 의존성이 있는 Code도 단순하게 Test할 수 있지만, 실제 Device에서의 동작은 다를 수 있으므로 Android System에 의존적인 Unit은 이후에 소개할 Instrumented Unit Testing을 이용하는 것이 좋습니다.

2.1 build.gradle 수정

Local Unit Testing을 Project에 적용하려면 app/build.gradle을 열어서 다음과 같은 Code를 입력하고 Sync Now를 누릅니다.

dependencies {
  testCompile 'junit:junit:4.12'
  testCompile 'org.mockito:mockito-core:1.10.19'
  testCompile 'org.hamcrest:hamcrest-library:1.3'
}

위의 내용은 Unit Testing Framework인 JUnit과, Mock Object 구현을 도와주는 Mockito, Test Class에서 사용하는 assert()와 같은 Matcher를 확장한 Library인 Hamcrest를 Test Class Build 시에 사용할 수 있도록 설정한 것입니다.

2.2 Test Class 생성 및 Test 실행

Android Studio

Test Class를 생성하고 Test를 실행하는 방법을 알아보기 위해서, 먼저 Test의 대상이 되는 간단한 Class를 하나 만들어 봅시다.

위의 그림과 같이 Project Tool Window에서 app/src/main/java/package.name에 Mouse 오른쪽 Button을 누른 후 New > Java Class를 누릅니다.

Create New Class

Dialog에서 Name에 Calc를, Kind는 Class를 선택하고 OK를 누릅니다.

Class가 생성되었으면 다음과 같이 간단한 Method를 구현합니다.

public class Calc {
  public int sum(int a, int b) {
    return a + b;
  }
}

Android Studio

이제 간단하게 구현한 Calc Class를 위한 Test Class를 생성해 봅시다. 단축키인 Command + Shift + T를 누르면, 아직 Calc Class에 대한 Local Unit Test Class가 만들어진 상태가 아니기 때문에 위의 그림과 같이 Test Class 생성을 위한 Popup이 표시됩니다.

Create New Test를 눌러서 Test Code를 생성합시다.

Create Test

위의 그림과 같이 Test Class 생성을 위한 Dialog가 표시되면 다음과 같이 입력하고 OK를 누릅니다.

  • Testing library: 기존에는 JUnit 3로 Test Code가 작성되어야 했으나, 이제는 JUnit 4도 사용가능하기 때문에 JUnit 4를 선택합니다.
  • Class name, Superclass, Destination package: 자동으로 선택된 것을 사용합니다.
  • Generate: 각 Test 시작과 끝에 수행할 것들을 위한 Method인 setUp()과 tearDown()을 사용할 것인지 여부에 따라 Check합니다.
  • Generate test methods for: Test 대상 Class에 구현된 Method 중 그에 대한 Test Method를 생성할 Method를 Check합니다. 위에서는 Calc에 구현된 Method인 sum()을 Check하여 그에 대한 Test Method가 자동으로 생성되도록 설정하였습니다.

Choose Destination Directory

위와 같이 Destination Directory Dialog가 표시되면 Local Unit Test가 위치하는 /app/src/test Directory를 선택하고 OK를 누릅니다.

Android Studio

Test Code 생성이 완료되면, 위의 그림과 같이 test/java/package.name/CalcTest.java가 생성되고, 해당 Test Class의 일부가 자동으로 구현되어 있는 것을 확인할 수 있습니다.

Unit Testing을 위해 CalcTest Class를 다음과 같이 구현합니다.

 1 import org.junit.After;
 2 import org.junit.Before;
 3 import org.junit.Test;
 4 
 5 import static org.hamcrest.CoreMatchers.is;
 6 import static org.hamcrest.MatcherAssert.assertThat;
 7 
 8 public class CalcTest {
 9   private Calc calc;
10 
11   @Before
12   public void setUp() throws Exception {
13     calc = new Calc();
14   }
15 
16   @After
17   public void tearDown() throws Exception {}
18 
19   @Test
20   public void sum() throws Exception {
21     // assertEquals(calc.sum(1, 2), 3);
22     assertThat(calc.sum(1, 2), is(3));
23   }
24 }

위의 Code에서 눈에 띄는 것은 기존에 JUnit 3에서 사용하는 Superclass(TestCase)를 상속받아 사용하지 않는다는 것과 @Before, @After, @Test와 같은 Annotation의 사용일 것입니다.

@Before은 Test 시작 전에 수행해야 할 것을 구현한 Method에, @After는 Test 후에 수행해야 할 것을 구현한 Method에, @Test는 실제 Test를 구현한 Test Method에 사용합니다.

JUnit 4에 대한 더 자세한 내용은 JUnit Wiki를 참고하기 바랍니다.

Calc의 sum()을 Test하기 위한 Method인 sum()에서 assertThat()은 Hamcrest에서 지원하는 Matcher입니다. 기존에 assertEquals(), assertNotNull()등으로 구분되어 있었던 Matcher들을 모두 아우르는 것이 바로 assertThat()입니다.

기존에 Line 21의 주석과 같이 assertEquals()로 구현하던 Code를 Line 22의 assertThat()으로 바꾸면, 좀 더 영문 문장 구조와 같은 Code(Assert that calc.sum(1,2) is 3)로 표현되면서 Test Code를 더 쉽게 이해하도록 만들어 줍니다. (어디까지나 영어가 주 언어인 사람들 기준이지만…)

Hamcrest에 대한 자세한 사용법은 Hamcrest Tutorial을 참고하기 바랍니다.

Android Studio

이제 작성한 Code를 Build하여 Test해 봅시다. CalcTest Tab에 Mouse 오른쪽 Button을 눌러 Popup을 띄운 후, Run ‘CalcTest’를 누릅니다.

Android Studio

Test가 정상적으로 수행되어 통과되면 위의 그림과 같이 Run Tool Window에 녹색 Progress Bar가 표시될 것입니다.

그리고 당연한 이야기지만, Device나 Emulator를 거치지 않고 개발자의 System에서 바로 Test하기 때문에 실행 속도가 더 빠르다는 것을 확인할 수 있습니다.

2.3 Test Code에서 Mock Object 사용하기

Abstract Class나 Interface를 Test하거나 Test Code에서 사용해야 할 경우, Mock Object(가짜 개체)를 사용하면 일반적인 방법으로 Instance로 만들 수 없는 Abstract Class나 Interface를 Test할 수 있습니다.

먼저 Mock Object를 사용하는 방법을 알아보기 위해서 기존의 Calc Class를 다음과 같이 Abstract Class로 변경해 봅시다.

public abstract class Calc {
  public int sum(int a, int b) {
    return a + b;
  }

  public abstract int multiply(int a, int b);
}

Abstract로 변경하면서 곱하기를 위한 Abstract Method(multiply())도 위와 같이 추가합니다.

그 다음, Calc Class를 위한 Test Class인 CalcTest를 다음과 같이 수정합니다.

 1 import org.junit.After;
 2 import org.junit.Before;
 3 import org.junit.Test;
 4 import org.junit.runner.RunWith;
 5 import org.mockito.Mock;
 6 import org.mockito.MockitoAnnotations;
 7 import org.mockito.runners.MockitoJUnitRunner;
 8 
 9 import static org.hamcrest.CoreMatchers.is;
10 import static org.hamcrest.MatcherAssert.assertThat;
11 import static org.mockito.Matchers.anyInt;
12 import static org.mockito.Mockito.when;
13 
14 @RunWith(MockitoJUnitRunner.class)
15 public class CalcTest {
16   @Mock
17   private Calc calc;
18 
19   @Before
20   public void setUp() throws Exception {
21     MockitoAnnotations.initMocks(calc);
22   }
23 
24   @After
25   public void tearDown() throws Exception {}
26 
27   @Test
28   public void sum() throws Exception {
29     when(calc.sum(anyInt(), anyInt())).thenCallRealMethod();
30     assertThat(calc.sum(1, 2), is(3));
31   }
32 
33   @Test
34   public void multiply() throws Exception {
35     when(calc.multiply(2, 3)).thenReturn(6);
36     assertThat(calc.multiply(2, 3), is(6));
37   }
38 }

위의 Code는 Instance로 만들 수 없는 Calc Class를 Instance로 만들기 위해서 Mock Object 관련 Library인 Mockito를 사용하여 Test Code를 구현한 것입니다.

Mockito를 사용하여 Mock Object를 생성하기 위해서는 먼저 Line 14과 같이 Class 선언에 @RunWith와 함께 Mockito의 Runner Class를 정의해 주어야 합니다.

그리고 Line 16과 같이 Mock Object를 생성할 Member에 @Mock을 붙인 후, setUp()이나 해당 Mock Object를 실제 Instance로 생성할 위치에 Line 21과 같이 MockitoAnnotations.initMocks()를 호출하면 Mock Object 생성이 완료됩니다.

이렇게 생성한 Mock Object는 Calc의 Abstract Method인 multiply()를 Test하는 Line 34의 multiply()에서 사용할 수 있습니다.

Line 35의 when()은 Mockito에서 제공하는 것으로서 아직 구현되지 않은 Method에 대하여 호출 시 어떻게 Return할 지를 지정할 수 있도록 도와줍니다. 예를 들어, Line 36의 Code는 calc.multiply(2, 3)을 호출하면 6을 Return하라는 의미입니다.

when()을 이용하여 Mock Object의 구현을 간단히 지정하였기 때문에, 그 다음 Line 36에서 calc.multiply()를 호출했을 때 Error가 발생하지 않고 정상적으로 Test가 통과됩니다.

이러한 기법은 Android System에 간단한 의존성만 가지는 경우(예를 들어, Abstract Class인 Context를 Mock Object로 만들어 사용하는 경우)를 위해 사용하기에 적합합니다. 물론 이 기법을 사용하기 위해서는 호출 시 예상되는 결과를 알아야 하며, 실제적인 기기에서의 결과와 차이가 있을 수 있다는 것을 유의해야 합니다.

Mockito의 자세한 사용법은 Mockito Documentation을 참고하기 바랍니다.

Android Studio

Test Code에 대한 Test를 실행하면 위의 그림과 같이 정상적으로 Test가 통과된 것을 확인할 수 있습니다.

2.4 Local Unit Test 전부를 한 번에 실행하는 방법

Local Unit Test가 많아지면 한 번에 전체 Test를 수행하는 것이 편리합니다. 여러 개의 Local Unit Test를 한 번에 실행하기 위해서는 다음과 설정하면 됩니다.

Android Studio

위의 그림과 같이 상단 Toolbar의 Edit Configurations를 Click합니다.

Run/Debug Configurations

좌측 상단의 + Button을 Click하고 JUnit을 선택합니다.

Run/Debug Configurations

다음과 같이 Test Configuration을 설정하고 OK를 누릅니다.

  • Test kind: All in package
  • Package: App의 Base Package
  • Use classpath of module: app (Gradle Module Name)

Android Studio

생성한 Configuration을 선택하고 Run을 누르면 Local Unit Test가 수행됩니다.

2.5 Command-line에서 Local Unit Test를 실행하는 방법

Command-line에서 Local Unit Test를 실행하려면, Project Home Directory에서 다음과 같이 입력하면 됩니다.

$ ./gradlew test --continue

Test Results

Command-line으로 실행한 결과는 app/build/reports/tests/<build type>/index.html에 저장됩니다.

3. 마치면서…

지금까지 Unit Testing에 대한 간단한 개념과 Local Unit Testing에 대해서 알아보았습니다. 이어지는 Part 2에서는 기기를 이용하여 Unit Test를 실행하는 Instrumented Unit Testing에 대해서 알아보도록 하겠습니다.

Tested Environments

  • macOS 10.12.2: Oracle JDK 1.8.0_112, Android Studio 2.2.3, Gradle 2.14.1

References