Part 1에 이어서 이번 Post에서는 Android Unit Testing 방법 중 하나인 Instrumented Unit Testing에 대해서 알아보도록 하겠습니다.

4. Instrumented Unit Testing

Instrumented Unit Testing은 Android Device나 Emulator에서 Unit Test를 수행하는 것을 말합니다.1

이전에 소개한 Local Unit Testing 보다는 실행 속도가 느리지만 Device나 Emulator에서만 얻을 수 있는 정보(Context와 같은)를 얻을 수 있으며, Unit에 해당하는 Activity나 Service 등을 실제 실행하여 Test하기 때문에 TDD로 개발 시 Local Unit Testing과 병행하여 사용하는 것이 좋습니다.

4.1 Instrumented Unit Testing 적용

Android Studio Project에 Instrumented Unit Testing을 적용하기 위해서는 다음과 같은 작업이 필요합니다.

4.1.1 Android Support Repository 설치

Android Support Repository는 Android Testing Support Library를 포함하는 Android SDK의 Package로 Android App Testing을 위한 여러 Library를 포함합니다.2

이것을 사용하면 기존에 Instrumented Unit Testing에서 JUnit 3만 지원하던 것을 JUnit 4를 사용하여 구현할 수 있게 되며, Test Filtering이나 Instrumentation(Android System에서 Hooking을 위한 Class) 정보에 접근하는 등의 추가적인 기능을 지원하게 됩니다.

Default Settings

설치를 위해서, Tools > Android > SDK Manager를 실행한 다음, SDK Tools > Android Support Repository를 Check하고 Apply를 누르면 해당 Package가 설치됩니다.

4.1.2 build.gradle 수정

Support Repository 설치 후에는 다음과 같은 내용을 app/build.gradle에 추가합니다.

 1 android {
 2   defaultConfig {
 3     testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
 4   }
 5 }
 6 
 7 dependencies {
 8   androidTestCompile 'com.android.support:support-annotations:25.1.0'
 9   androidTestCompile 'com.android.support.test:runner:0.5'
10   androidTestCompile 'com.android.support.test:rules:0.5'
11   androidTestCompile 'org.hamcrest:hamcrest-library:1.3'
12 }

Line 3의 testIntrumentationRunner는 Instrumented Unit Testing을 위한 Runner를 지정한 것으로, AndroidJUnitRunner는 위에서 설치한 Support Repository에 포함되어 있습니다.

Line 8~11는 Support Library를 사용하기 위한 연관된 Library와 JUnit 4 Rule 지원 등을 위해 입력합니다.

위의 내용을 추가했다면 Sync Now를 눌러 동기화합니다.

4.2 Instrumented Unit Testing 구현

이전 항목을 통하여 Instrumented Unit Testing을 구현할 준비를 마쳤습니다. 이번 항목에서는 예제와 함께 실제 Test Code를 구현하며, 또한 Test Code 내에서 함께 사용할 수 있는 기법들을 간단하게 소개합니다.

4.2.1 Test Class 생성 및 Test 실행

Instrumented Unit Testing은 주로 Activity나 Service같은 Android Platform에 의존하는 Unit들을 Test하기 위한 것이기 때문에, 여기서는 MainActivity에 대한 Unit Test Code를 구현하는 것을 예로 들어서 설명하겠습니다.

Android Studio

먼저 MainActivity.java를 연 다음, Command + Shift + T를 누릅니다. 위와 같은 Popup이 표시되면 Create New test를 누릅니다.

Create Test

Local Unit Test Code를 구현할 때와 마찬가지로, Test Class 생성을 위한 Dialog가 표시됩니다.

JUnit 4를 사용하여 Test Code를 구현할 것이기 때문에 Testing library에 JUnit4를 선택하고, Generate에 두 Option을 모두 Check한 후 OK를 누릅니다.

Choose Destination Directory

위의 그림과 같이 Test Class의 위치를 선택하는 Dialog가 표시되면, 기본으로 선택된 app/src/androidTest 내의 Directory를 선택하고 OK를 누릅니다.

Test Class인 MainActivityTest.java가 생성되었다면, 다음과 같은 Code를 입력합니다.

 1 import android.support.test.InstrumentationRegistry;
 2 import android.support.test.runner.AndroidJUnit4;
 3 import android.support.test.filters.SmallTest;
 4 import android.test.ActivityInstrumentationTestCase2;
 5 
 6 import org.junit.After;
 7 import org.junit.Before;
 8 import org.junit.Test;
 9 import org.junit.runner.RunWith;
10 
11 import static org.hamcrest.CoreMatchers.is;
12 import static org.hamcrest.MatcherAssert.assertThat;
13 
14 @RunWith(AndroidJUnit4.class)
15 @SmallTest
16 public class MainActivityTest
17     extends ActivityInstrumentationTestCase2<MainActivity> {
18   private MainActivity activity;
19 
20   public MainActivityTest() {
21     super(MainActivity.class);
22   }
23 
24   @Before
25   public void setUp() throws Exception {
26     super.setUp();
27     injectInstrumentation(InstrumentationRegistry.getInstrumentation());
28     activity = getActivity();
29   }
30 
31   @After
32   public void tearDown() throws Exception {
33     super.tearDown();
34   }
35 
36   @Test
37   public void loadStringResource() throws Exception {
38     String appName = activity.getResources().getString(R.string.app_name);
39     assertThat(appName, is("InstrumentedUnitTest"));
40   }
41 }

위의 Code에서 Line 14의 @RunWith는 JUnit 4를 사용한 Test Code를 실행하기 위한 Runner를 지정합니다.

Line 15의 @SmallTest는 Test Filtering을 위해 사용하는 Annotation입니다. 위와 같이 입력하면, MainActivityTest에 있는 모든 Test가 Small Test로 지정됩니다. 이후에 설명할 Gradle 설정과 같이 사용하면, Test의 부하 정도에 따라 Test를 나누어 실행할 수 있게 됩니다.

Local Unit Testing에서 Test Class는 기본적으로 아무런 Class도 상속받지 않고 구현했지만, Instrumented Unit Testing에서는 Instrumented Test를 위해 Android SDK에서 제공하는 Test Class를 상속받아서 구현해야 합니다.

위의 경우, Activity에 대한 Instrumented Test Class를 구현하는 것이기 때문에 Android SDK의 ActivityInstrumentationTestCase2를 상속하여 구현되었습니다.

또한, Class 상속으로 인해서 생성자도 따로 구현해 주어야 합니다. Line 20-22와 같이 기본 생성자를 구현하고 Test할 MainActivity의 Class를 인자로 한 Superclass의 생성자를 호출합니다.

Line 27은 JUnit 3로 구현된 ActivityInstrumentationTestCase2를 상속받은 Test Class에서 JUnit 4를 사용하기 위해 추가되어야 합니다.3

Line 28은 Acitivty의 Lifecycle대로 실제 MainActivity Instance를 생성하여 실행한 다음, Instance를 Return하여 Test를 위해 사용할 수 있도록 합니다.

Line 36-40은 Activity의 Context를 통해 Resources에 접근하여 String Resource를 얻어와 Test하는 Code입니다. setUp()에서 얻은 activity Instance를 이용해 Context나 Resouces등을 얻을 수 있으며, Hamcrest가 지원하는 Matcher인 assertThat()을 사용하여 Test Code를 작성할 수 있습니다.

Instrumented Test를 위해 Android SDK에서 제공하는 Test Class는 Android API Reference에서 확인할 수 있습니다.

Android Studio

Test를 실행하기 위해서 편집기의 Tab에서 Mouse 오른쪽 Button Popup을 띄운 후, Run ‘MainActivityTest’를 누릅니다.

Select Deployment Target

Instrumented Unit Test는 Device나 Emulator에서 진행되므로, 위의 그림과 같이 Device를 선택하는 Dialog가 표시됩니다.

Test를 실행할 Device나 Emulator를 선택 후 OK를 누릅니다.

Android Studio

Test가 정상적으로 실행되면 위의 그림과 같이 하단의 Run Tool Window에 Unit Testing 결과가 표시됩니다.

4.2.2 Test Code에서 Mock Object 사용하기

Local Unit Testing과 마찬가지로 Instrumented Unit Testing에서도 Mock Object를 사용하여 Test Code를 작성할 수 있습니다.

Mocking을 위한 Library인 Mockito를 사용하기 위해서, app/build.gradle에 다음과 같은 내용을 추가하고 Sync Now를 눌러 동기화합니다.4

dependencies {
  androidTestCompile 'org.mockito:mockito-core:1.9.5'
  androidTestCompile 'com.google.dexmaker:dexmaker:1.2'
  androidTestCompile 'com.google.dexmaker:dexmaker-mockito:1.2'
}

그리고 MainActivityTest.java를 다음과 같이 수정합니다.

 1 import android.content.res.Resources;
 2 import android.support.test.InstrumentationRegistry;
 3 import android.support.test.filters.SmallTest;
 4 import android.test.ActivityInstrumentationTestCase2;
 5 
 6 import org.junit.After;
 7 import org.junit.Before;
 8 import org.junit.Test;
 9 import org.junit.runner.RunWith;
10 import org.mockito.Mock;
11 import org.mockito.MockitoAnnotations;
12 import org.mockito.runners.MockitoJUnitRunner;
13 
14 import static org.hamcrest.CoreMatchers.is;
15 import static org.hamcrest.MatcherAssert.assertThat;
16 import static org.mockito.Mockito.when;
17 
18 @RunWith(MockitoJUnitRunner.class)
19 @SmallTest
20 public class MainActivityTest
21     extends ActivityInstrumentationTestCase2<MainActivity> {
22   private MainActivity activity;
23 
24   @Mock
25   private Resources mockResources;
26 
27   public MainActivityTest() {
28     super(MainActivity.class);
29   }
30 
31   @Before
32   public void setUp() throws Exception {
33     super.setUp();
34     injectInstrumentation(InstrumentationRegistry.getInstrumentation());
35     activity = getActivity();
36     MockitoAnnotations.initMocks(mockResources);
37   }
38 
39   @After
40   public void tearDown() throws Exception {
41     super.tearDown();
42   }
43 
44   @Test
45   public void loadStringResource() throws Exception {
46     String appName = activity.getResources().getString(R.string.app_name);
47     assertThat(appName, is("InstrumentedUnitTest"));
48   }
49 
50   @Test
51   public void loadMockStringResource() throws Exception {
52     when(mockResources.getString(R.string.app_name)).thenReturn("InstrumentedUnitTest");
53     String appName = mockResources.getString(R.string.app_name);
54     assertThat(appName, is("InstrumentedUnitTest"));
55   }
56 }

Mockito를 사용한 위의 예제에서 변경된 것은 다음과 같습니다.

Line 18에서 Runner는 Mockito 사용을 위한 Runner인 MockitoJUnitRunner를 사용합니다.

또한 Line 24-25에서는 @Mock을 사용하여 Android SDK의 Abstract Class인 Resources를 Mock Object로 선언했습니다.

선언된 Mock Object를 Test Code에서 사용하기 전에, Line 36과 같이 MockitoAnnotations.initMocks()를 호출하여 이전에 선언한 Mock Object를 Instance로 만듭니다.

그리고 Line 50-55와 같이 when()을 사용하여 Mock Object의 Interface 호출 및 결과를 지정하고, 실제로 호출해 봄으로서 Mockito의 동작을 Test합니다.

Android Studio

수정한 Test Code를 실행하면, 위의 그림과 같이 Test가 정상적으로 완료되는 것을 확인할 수 있습니다.

4.2.3 Test Suite 사용하기

Test Suite는 Test Class나 Test Suite를 모아 Group으로 만들어 Test 할 수 있도록 만든 것으로, 개발자의 목적에 따라(예를 들어, 서로 연관된 것들을 모아서 Test하는 경우) Unit Test를 할 수 있도록 도와줍니다.

Android Studio

Test Suite를 생성하기 위해서는 먼저 Convention에 따라 Test Suite를 위한 Package를 생성해야 합니다.

Project Tool Window에서 app/src/AndroidTest/java/<Package Name>을 Mouse 오른쪽 Button으로 눌러 Popup을 띄운 후, New > Package를 선택합니다.

New Package

Dialog에서 Test Suite를 위한 Package인 suite를 입력합니다.

Android Studio

생성된 Package에 Mouse 오른쪽 Button을 눌러 Popup을 띄운 후, New > Java Class를 누릅니다.

Create New Class

Kind는 Class를 선택하고, Name에 AppTestSuite를 입력한 후 OK를 누릅니다.

그리고 다음과 같이 입력하여 Test Suite Class를 구현합니다.

import org.junit.runner.RunWith;
import org.junit.runners.Suite;

@RunWith(Suite.class)
@Suite.SuiteClasses({ExampleInstrumentedTest.class, MainActivityTest.class})
public class AppTestSuite {}

Test Suite는 위의 Code와 같이 @RunWith와 @Suite.SuiteClasses로 구현합니다.

실제 Class인 AppTestSuite는 아무런 구현이 없으며, @RunWith를 이용하여 Test Suite를 위한 Runner인 Suite.class를 지정합니다.

또한 @Suite.SuiteClasses에 Test를 수행한 Test Class나 Test Suite Class를 배열 형식으로 나열하여 이 Test Suite를 통해 수행되는 Test를 지정할 수 있습니다.

MainActivityTest와 함께 나열된 ApplicationTest는 Project를 생성 시 자동으로 생성된 것을 사용하였습니다.

Android Studio

생성한 Test Suite를 실행하기 위해서 Tab에 Mouse 오른쪽 Popup을 띄운 다음 Run ‘AppTestSuite’를 누릅니다.

Android Studio

Test Suite가 실행되면, 위의 그림과 같이 MainActivityTest의 Test와 ApplicationTest의 Test가 자동으로 실행되며 그 결과가 IDE 하단에 표시됩니다.

4.3 Command-line에서 Instrumented Unit Test를 실행하는 방법

Android Studio가 아닌 Command-line에서 Instrumented Unit Test를 실행하려면, Project Home Directory에서 다음과 같이 입력하면 됩니다. (Device나 Emulator 연결 필요)

$ ./gradlew connectedCheck
또는
$ ./gradlew cC

Test Results

Test가 실행된 결과는 app/build/reports/androidTests/connected/index.html에서 확인할 수 있습니다.

4.4 Test Filtering 적용

위에서 잠깐 언급했지만, @SmallTest, @MediumTest, @LargeTest를 이용하여 Test Method나 Class의 부하 수준을 지정할 수 있습니다.

Test Filtering을 위한 Annotation에 대한 자세한 정보는 Google Testing Blog에서 확인할 수 있습니다.

Test Code에서 Annotation을 사용했다면, 다음과 같이 app/build.gradle을 수정하여 부하가 가장 적은 Test만 실행할 수 있습니다.5

android {
  defaultConfig {
    testInstrumentationRunnerArgument "size", "small"
  }
}

부하가 큰 Test를 제외한 모든 Instrumented Unit Test를 실행하려면 다음과 같이 app/build.gradle을 수정하면 됩니다.

android {
  defaultConfig {
    testInstrumentationRunnerArgument 'notAnnotation', 'android.support.test.filters.LargeTest'
  }
}

Test Filtering은 Command-line에서 Gradle Wrapper를 이용하거나, Android Studio의 Gradle Tool Window에서 connectedCheck를 실행할 때만 적용됩니다.

4.5 ActivityTestRule

ActivityInstrumentationTestCase2가 API 24에서 Deprecate됨에 따라서, Activity Unit Testing을 위해서 Android Testing Support Library에서 제공하는 ActivityTestRule을 사용하도록 권고하고 있습니다.

ActivityTestRule을 사용하면 기존의 Test Class를 상속받아서 구현하는 구조가 아니기 때문에 Code가 더 간결해지고, ActivityTestRule을 상속받아서 구현한 Rule을 사용하게 될 경우, 좀 더 Activity의 Life Cycle에 맞게 Test를 수행할 수 있습니다.6

이전에 사용했던 Test Code를 ActivityTestRule을 사용하도록 변경하면 다음과 같습니다.

 1 import static org.hamcrest.CoreMatchers.is;
 2 import static org.hamcrest.MatcherAssert.assertThat;
 3 import static org.mockito.Mockito.when;
 4 
 5 import org.junit.After;
 6 import org.junit.Before;
 7 import org.junit.Rule;
 8 import org.junit.Test;
 9 import org.junit.runner.RunWith;
10 import org.mockito.Mock;
11 import org.mockito.MockitoAnnotations;
12 import org.mockito.runners.MockitoJUnitRunner;
13 
14 import android.content.res.Resources;
15 import android.support.test.filters.SmallTest;
16 import android.support.test.rule.ActivityTestRule;
17 
18 @RunWith(MockitoJUnitRunner.class)
19 @SmallTest
20 public class MainActivityTest {
21   @Rule
22   public ActivityTestRule<MainActivity> testRule = new ActivityTestRule<MainActivity>(MainActivity.class);
23 
24   @Mock
25   private Resources mockResources;
26 
27   @Before
28   public void setUp() throws Exception {
29     MockitoAnnotations.initMocks(mockResources);
30   }
31 
32   @After
33   public void tearDown() throws Exception {
34   }
35 
36   @Test
37   public void loadStringResource() throws Exception {
38     String appName = testRule.getActivity().getResources().getString(R.string.app_name);
39     assertThat(appName, is("InstrumentedUnitTest"));
40   }
41 
42   @Test
43   public void loadMockStringResource() throws Exception {
44     when(mockResources.getString(R.string.app_name)).thenReturn("InstrumentedUnitTest");
45     String appName = mockResources.getString(R.string.app_name);
46     assertThat(appName, is("InstrumentedUnitTest"));
47   }
48 }

Code를 보시면 아시겠지만, 더이상 상속을 통해 Test Class를 구현하지 않고 JUnit 4의 @Rule을 사용하여 Test Rule을 지정하여 사용하는 것을 확인 할 수 있습니다. 또한 @Before에서 Activity 실행 관련한 Code가 필요 없기 때문에 Code가 더 간결합니다.

ActivityTestRule을 이용하여 위와 같이 Test Rule을 지정해 놓으면 @Before로 지정된 Method가 수행되기 전에 해당 Activity가 시작되며, @After로 지정된 Method가 수행된 후에 그 Activity가 종료됩니다.

Test Method에서 Activity를 사용하려면 Line 38과 같이 ActivityTestRule의 getActivity()를 이용하여 실행 중인 Activity를 얻으면 됩니다.

Tested Environments

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

References