대메뉴 바로가기 본문 바로가기

데이터 기술 자료

데이터 기술 자료 상세보기
제목 스텝업 안드로이드 개발: 쉽게 따라하는 안드로이드 로컬 유닛 테스트(1)
등록일 조회수 5306
첨부파일  

스텝업 안드로이드 개발

쉽게 따라하는 안드로이드 로컬 유닛 테스트(1)



2014년 말 안드로이드 스튜디오(Android Studio)가 안드로이드의 공식 IDE(통합개발환경)로 선정됐다. 거의 10년이 넘는 기간 동안 수많은 개발자가 이클 립스(Eclipse)에 열광하며 개발을 했는데, 이제는 안드로이드 스튜디오로 갈아탈 시점이 다가왔다. 구글은 2015년 ‘구글 I/O’ 행사에서 ‘What’s New in Android Developments’라는 제목으로 세션을 진행했다. 세션에서는 크게 디자인(Design), 개발(Develop) 그리고 테스트(Test)로 나눠 새로운 기술이 소개 됐다. 이번 시간에는 당시 세션에서 다뤄진 안드로이드 로컬 유닛 테스트(Android Local Unit Tests)에 관해 다루고자 한다.



회사에서 안드로이드 앱을 개발한지 5년 정도 됐다. 필자는 짠 코드가 정상적으로 동작하는지 유닛 테스트를 작성해 종종 확인한 다. 복잡한 UI 테스팅보다는 로직 위주의 기능 테스트다. 처음 안 드로이드 유닛 테스트를 공부했을 때 가장 먼저 눈에 띈 어색한 영 어 단어는 ‘Instrumentation’이었다. 사전을 찾아보니 (차량, 기계 운용에 쓰이는) 기기 장치(기계 장비) 혹은 기악법, 기악 편성 연주 법이라고 한다(네이버 영어 사전). 그냥 디바이스라고 하면 되는데, 왜 이렇게 어려운 단어를 사용하는지 의문을 가졌던 기억이 난다.



로컬 유닛 테스트란

일반적인 자바(Java)의 유닛 테스트와 다르게 안드로이드 유닛 테스트를 실행하기 위해서는 타깃 디바이스를 연결하거나 에뮬레 이터를 사용해야 한다. 즉, 별도 디바이스가 있어야 내가 만든 테 스트 코드를 시험해 볼 수 있는 것이다. 최근에는 나아졌지만 에뮬 레이터의 성능은 아직 빠르지 않다. 타깃 디바이스에서 실행하는 경우에도 원래 앱을 변경했다면 테스트 코드를 실행하고 Unit Test의 녹색 ‘PASS’ 막대기를 보기까지 꽤 많은 시간이 소요된다. 간단한 예를 들어보자. <리스트 1>은 사용자가 입력한 계정 ID 가 올바른가를 검사하는 코드이다.



<리스트 1> 사용자 ID를 검사하는 코드 public static final Pattern USER_ID_PATTERN = Pattern.compile( "[a-zA-Z0-9._]{8,15}" //8~15자까지 가능. 영문자 및 특수기호(. 및 _)만 가능함 ); public static boolean isUserIdValid(String input) { if(input != null && USER_ID_PATTERN. matcher(input).matches()) { return true; } return false; }



먼저 기존 방식의 안드로이드 유닛 테스트를 작성했다. Android Testcase 클래스를 상속하고 내가 원하는 코드를 testXXX()로 작 성했다.



<리스트 2> AndroidTestCase를 활용한 유닛 테스트 public class UserIDPolicyTests extends AndroidTestCase { public void testUserID() { //normal case assertTrue(UserIDPolicy.isUserIdValid("LocalU nitTests")); assertTrue(UserIDPolicy.isUserIdValid("Enjoy_ Tests")); assertTrue(UserIDPolicy. isUserIdValid("donghwan.yu")); //exceptions assertFalse(UserIDPolicy. isUserIdValid("short")); assertFalse(UserIDPolicy. isUserIdValid("toooooooooooo_long")); assertFalse(UserIDPolicy.isUserIdValid("notdash")); } }



로컬 유닛 테스트는 타깃 디바이스가 아닌 내 PC에서 유닛 테스 트를 실행하는 방법이다. 내용은 <리스트 3>과 같다. 일단 소스 코 드를 보자.



<리스트 3> 로컬 유닛 테스트 예제 @SmallTest public class UserIDPolicyLocalTests { @Test public void checkUserID_JUnit3Style() { //normal case assertTrue(UserIDPolicy.isUserIdValid("LocalU nitTests")); assertTrue(UserIDPolicy.isUserIdValid("Enjoy_ Tests")); assertTrue(UserIDPolicy. isUserIdValid("donghwan.yu")); //exceptions assertFalse(UserIDPolicy. isUserIdValid("short")); assertFalse(UserIDPolicy. isUserIdValid("toooooooooooo_long")); assertFalse(UserIDPolicy.isUserIdValid("notdash")); } }



안드로이드 유닛 테스트를 작성해 본 사람이면 무언가 다른 점 을 발견할 수 있다. 우선 AndroidTestCase를 상속하지 않았다. 테 스트 메소드가 testXXX()로 시작되지 않는다. 그리고 @ SmallTest, @Test와 같은 자바 어노테이션이 보인다. 실행에 문 제가 없다. 이제 로컬 유닛 테스트 만드는 방법을 알아보자.



로컬 유닛 테스트 만들기

앞서 작성된 코드를 입력하는 방법이다. 안드로이드 스튜디오를 기준으로 설명한다. 아쉽게도 이클립스에서는 Gradle을 지원하지 않기 때문에 로컬 유닛 테스트를 작성할 수 없다. IDE를 사용하지 않고 command line에서 작성할 수도 있지만 추천하지는 않는다. 로컬 유닛 테스트는 /app/test/java에 위치해야 한다. 보통 앱 소스는 /app/main/java에 위치한다(<그림 1> 참조). 아마 처음 작 성하면 <그림 2>와 같이 조금 이상한 모양으로 나올 것이다.





그 이유는 안드로이드 스튜디오의 기본 유닛 테스트 방식이 Android Instrumentation Tests이기 때문이다. 다음과 같은 절차 에 따라 로컬 유닛 테스트로 변경하면 된다.

- IDE 좌측에 Build Variants를 클릭한다. 필자는 맨 처음에 이것을 찾는 데 한참을 애먹었다(<그림 3> 참조).
- Build Variants 화면이 나타나는데, Test Artifact를 Unit Test로 변경한다.







이제 로컬 유닛 테스트를 실행할 수 있다. 내 PC에서 유닛 테스 트를 실행할 수 있으므로 타깃 디바이스의 USB를 뽑자. 클래스의 마우스 오른쪽 버튼을 눌러서 Run을 실행하면 그 결과가 Run 창 에 표시된다.



정상적인 실행을 위해서는 app 모듈의 build.gradle을 <리스트 4>와 같이 수정한다.



<리스트 4> build.gradle에 추가하는 내용 dependencies { // Unit testing dependencies testCompile 'junit:junit:4.12' // Set this dependency if you want to use Mockito testCompile 'org.mockito:mockito-core:1.10.19' // Set this dependency if you want to use Hamcrest matching androidTestCompile 'org.hamcrest:hamcrestlibrary: 1.1' }



원래는 새로운 라이브러리를 선언할 때 compile을 추가하는데 로컬 유닛 테스트를 위해 testCompile에 JUnit4를 선언했고, Mock 객체 사용을 위해 Mockito 라이브러리를 추가했다.

Mockito 라이브러리는 다음 시간에 자세히 설명할 것이다. 안드로 이드 유닛 테스트를 위해서는 androidTestCompile에 필요한 라이 브러리를 선언한다. 여기서는 패턴 매칭 라이브러리인 Hamcrest 를 추가했다.

가장 위에 JUnit4를 추가했다. 기존 안드로이드 유닛 테스트는 JUnit3 기반이다. JUnit4가 2006년에 릴리즈 됐다는 점에서 한참 늦은 것으로 생각한다. 이제 JUnit4에 대해 간략히 알아보자. 참고 로 구글의 공식 개발자 문서인 ‘Building Local Unit Tests’에서도 JUnit4를 권장하고 있다.



알아두면 좋은 JUnit4 특징들

다음은 JUnit4에서 달라진 점들이다. 한빛미디어의 < 테스트 주 도 개발 : TDD 실천법과 도구>라는 책에 정리가 잘 돼 있어 일부 를 발췌했다. 구글에서 검색을 해보니 안드로이드에서 JUnit4는 Android Support Test Library라는 지원 라이브러리로 별도 제공 된다. 추가된 시점은 2014년 12월이다.

- Java5 어노테이션 지원
- test라는 글자로 method 이름을 시작해야 한다는 제약 해소: @Test
- 좀 더 유연한 픽스처: @BeforeClass @AfterClass, @Before, @After
- 예외테스트: @Test(expected=NumberFormatException.class)
- 시간제한테스트: @Test(timeout=1000)
- 테스트무시: @Ignore(“this method isn’t working yet”
- 배열지원: assertArrayEquals([message], expected, actual)
- @RunWith(클래스이름.class) 사용 가능
- @SuiteClasses(Class[])
- 파라미터를 이용한 테스트

JUnit4의 가장 큰 특징은 어노테이션의 활용이다. Test Case의 메소드에는 @Test를 붙여주면 된다. 기존 JUnit3 처럼 Base class 를 상속하거나 testXXX 메소드와 같이 이름을 맞출 필요도 없다. 어노테이션을 넣어주면 된다. JUnit4에서는 assertTrue()나 assertFalse()보다 assertThat() 및 is()를 활용하는 것이 더 자연스 럽다. <리스트 3>의 메소드 이름이 checkUserID_JUnit3Style()인 이유도 이 때문이다.



<리스트 5> JUnit4 스타일로 작성한 유닛 테스트 @Test public void checkUserID() { assertThat("Normal User ID is valid", UserIDPolicy.isUserIdValid("LocalUnitTest s"), is(true)); assertThat("Normal User ID with Underscore is valid", UserIDPolicy.isUserIdValid("Enjoy_ Tests"), is(true)); assertThat("Normal User ID with Dot is valid", UserIDPolicy.isUserIdValid("donghwan. yu"), is(true)); assertThat(UserIDPolicy.isUserIdValid("short"), is(false)); assertThat(UserIDPolicy. isUserIdValid("toooooooooooo_long"), is(false)); assertThat(UserIDPolicy.isUserIdValid("notdash"), is(false)); }



두 번째 특징은 라이프 사이클이 좀 더 풍부해졌다는 점이다. 기 존에는 각 테스트 케이스 실행 전후에 setup() / teardown()을 각 각 1개의 메소드만 사용할 수 있었다. 이제는 @Before, @After, @BeforeClass, @AfterClass를 사용할 수 있고 같은 어노테이션을 붙인 메소드를 여러 개 사용할 수도 있다. 단, 각각의 순서는 지정 할 수 없다. 예를 들어 네트워크에 접속하는 코드가 있었다면 기존 에는 매번 테스트 케이스를 실행할 때마다 서버에 접속하고 접속 을 해제해야 했다. 이제는 @BeforeClass와 @AfterClass에 각 한 번씩 static 메소드만 실행하면 된다. 그 만큼 전체 실행 시간을 절 약할 수 있다.



<리스트 6> @Before를 사용한 예제 private String[] validCases; private String[] wrongCases; @Before public void loadCommonVariable() { validCases = new String[] { "LocalUnitTests", "Enjoy_Tests", "donghwan.yu", }; wrongCases = new String[] { "short", "toooooooooooo_long", "not-dash", }; } @Test public void testUserID_withBefore() { for (String value : validCases) { assertThat(UserIDPolicy.isUserIdValid(value), is(true)); } for (String value : wrongCases) { assertThat(UserIDPolicy.isUserIdValid(value), is(false)); } }



예외 테스트와 시간제한 테스트, 테스트 무시의 경우 기존 JUnit3 환경에서는 주석으로 처리할 수밖에 없었다. 이제는 어노 테이션을 활용해 명시적으로 표현할 수 있다.



<리스트 7> 타임아웃과 @Ignore를 적용한 예제 @Test(timeout=5) public void testUserID_LessThanFiveMillisecond() { for (String value : validCases) { assertThat(UserIDPolicy.isUserIdValid(value), is(true)); } } @Ignore("this method isn't working yet") public void testUserID_Ignored() { //do nothing yet }



<리스트 7>의 메소드는 테스트 케이스의 제한 시간을 5ms로 설 정했다. testUserID_Ignored()는 아직 동작하지 않으므로 테스트 에서 생략했다.



마지막으로 Android Gradle 플러그인은 1.1.0 이상을 사용해야 한다. 구 버전을 사용하고 있다면 최신 버전인 1.2.3으로 업데이트 할 것을 권장한다. 안드로이드 스튜디오도 1.2.2 버전 이상을 사용 하는 것이 성능 면에서 안정적이다. 드라큘라 테마 UI를 지정하면 눈이 조금 더 편해지는 장점도 있다.



<리스트 8> 프로젝트의 build.gradle dependencies { classpath 'com.android.tools.build:gradle:1.2.3' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files }



로컬 유닛 테스트를 위한 코드

어떤 코드를 Local Unit Tests로 만들면 좋을까? 구글 공식 문서 인 Building Local Unit Tests에서는 ① 안드로이드 의존성이 전혀 없는 코드 ② 안드로이드 의존성이 단순한 경우 로컬 유닛 테스트 를 적용하라고 권장하고 있다. 좀 더 구체적으로 보면 ① 코어 비 즈니스 로직 ② 유틸리티 코드 ③양음력 변환과 같이 전체를 한 번 테스트하는 데 오래 걸리는 로직 코드 ④ 외부 라이브러리를 사용 한 네트워크 코드(HTTP, Socket 등)로 정리된다.

아무리 안드로이드와 의존성이 없다고 해도 앱에 들어가는 소스 는 Context, Log 클래스와 같이 최소한 이상의 의존성을 가지게 되는데, 이때는 Mock 객체를 사용해야 한다. 다음 시간에는 Mockito를 활용한 로컬 유닛 테스트에 대해 알아볼 것이다.



출처 : 마이크로소프트웨어 9월호

제공 : 데이터 전문가 지식포털 DBguide.net