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

데이터 기술 자료

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

스텝업 안드로이드 개발

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



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



지난 시간에는 로컬 유닛 테스트의 기본 개념과 함께 알아두면 좋은 JUnit4의 특징에 대해 알아봤다. 이번 시간에는 실제로 본인의 프로젝트에서 로컬 유닛 테스트를 적용해보는 것에 관해 알아본다. 타겟 디바이스를 사용할 수 없기 때문에 100% 편리한 환경을 제공하는 것은 아니지만 분명 로컬 유닛 테스트만의 매력을 느낄 수 있을 것이다. 우선 Mock의 개념부터 알아보자.



Mock 객체란 무엇인가?

로컬 유닛 테스트는 스마트폰 등 타겟 디바이스를 사용하지 않는다. 내 PC에는 안드로이드가 없으므로 본인의 프로젝트에서 로컬 유닛 테스트를 처음 작성한 다음 실행하면 <그림 1>과 같은 에러를 출력하고 죽어버릴 것이다. 당연히 유닛 테스트는 FAIL(실패)이다. 왜 FAIL이 됐을까? <리스트 1>의 소스 코드를 다시 보자.



기존 소스에서 로그 한 줄 추가했을 뿐이다. 실제 프로젝트에서는 아무리 단순한 메소드라도 정상적인 동작을 확인하기 위해 간단한 로그 정도는 추가된다. 그런데 그것 조차 로컬 유닛 테스트를 할 수 없다니 황당했다. 에러 메시지를 조금 더 읽어보자.



<리스트 1> 실패한 로컬 유닛 테스트 코드 public static boolean isUserIdValid(String input) { Log.d(TAG, "input= " + input); //로그 한줄 추가했을 뿐인데... if(input != null && USER_ID_PATTERN.matcher(input).matches()) { return true; } return false; }



Android.util.Log가 mock되지 않았다는 내용이다. Gradle을 위한 안드로이드 플러그인에는 모든 메소드에 껍데기만 있고 실행 시에 RuntimeException을 출력하게 돼 있다. 이제 어떻게 해야 할까? 정답은 내가 사용하는 안드로이드 관련 객체 및 메소드를 mock으로 만드는 것이다. Mock 객체는 최종적으로는 필요하지만 테스트할 시점에는 가짜 내용을 적당히 넣어서 만든 객체를 의미한다.

에러 메시지를 보면 구글의 로컬 유닛 테스트 지원 사이트(sites.google.com/a/android.com/tools/tech-docs/unit-testing-support)에서 자세한 내용을 확인하라고 나온다. 지원 사이트로 이동해서 내용을 보면 <그림 2>와 같이 조금은 무책임하게 대처하고 있다. 거의 대부분 상용 앱의 소스 코드에서 사용하고 있는 Log 등의 static method에 대해 로컬 유닛 테스트를 할 수가 없다.



대안으로 build.gradle에 unitTests.returnDefaultValues = true를 추가하라고 권고하고 있으나 역시 부족한 느낌이다. 여기서 주저앉을 것인가? 인터넷에서 검색해보니 ‘PowerMock’이라는 라이브러리를 사용하면 내가 원하는 static method를 mock으로 구현할 수 있었다. PowerMock은 Mockito를 기반으로 작성됐다.



PowerMock과 Mockito를 활용하자

먼저 app의 build.gradle에서 Junit4, Mockito 그리고 PowerMock이 포함돼 있는지 확인한다. 2014년 7월 기준 최신 버전은 각각 Mockito-core 1.10.19과 PowerMock 1.6.2이다. JUnit4는 4.12 버전이면 충분하다.



<리스트 2> PowerMock이 추가됨 // 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 PowerMock testCompile 'org.powermock:powermock-module-junit4:1.6.2' testCompile 'org.powermock:powermock-api-mockito:1.6.2'



이제 Log 클래스를 mock 해보자.

<리스트 3> Log 클래스를 mock @SmallTest @RunWith(PowerMockRunner.class) @PrepareForTest(Log.class) public class UserIDPolicyLocalTests { @BeforeClass public static void mockLogClass() { PowerMockito.mockStatic(Log.class); PowerMockito.when(Log.d(anyString(), anyString())).thenReturn(0); PowerMockito.when(Log.i(anyString(), anyString())).thenReturn(0); PowerMockito.when(Log.e(anyString(), anyString())).thenReturn(0); PowerMockito.when(Log.w(anyString(), anyString())).thenReturn(0); }



Static 메소드를 mock하기 위해서는 PowerMockRunner를 러너로 설정해야 한다. @RunWith 어노테이션을 사용하면 된다. Log 클래스를 mock하기 위해 @PrepareForTest 어노테이션을 지정한다. @PrepareForTest는 여러 번 지정할 수 있다. @Before Class는 매 클래스마다 한 번만 실행된다. JUni3에서 제공하는 setup() 메소드는 매 테스트 케이스마다 실행되므로 비효율적이다. @BeforeClass 어노테이션은 static 메소드에만 지정할 수 있다.

mockLogClass()를 살펴보자. mockStatic() 메소드에서 클래스를 선언하고 Log에서 자주 쓰이는 d(debug), i(info), w(warn), e(error) 메소드를 mock 했다. When()과 thenReturn()은 그냥 상식적으로 이해하기가 더 쉽다. 예를 들어 Log.d(arg1, arg2)라면 arg1과 arg2에 어떤 String이 들어오더라도 0을 리턴하겠다는 의미이다. anyString()은 Mockito에 정의돼 있다. 유사하게 anyInt(), anyChar(), anyObject(), any() 등을 자유롭게 사용할 수 있다. 내가 mock하고자 하는 메소드의 원형에 맞게 타입을 지정해야 한다. 이제 실행을 해보자



Mock을 활용하는 기법은 TDD(테스트 주도 개발)에서도 중급 이상에 속한다. Mock은 단순하게 생각하면 모두 구현하지 않고 일부만 있어도 테스트할 수 있도록 가짜 객체를 주입하는 방법이다. 즉, 테스트 케이스를 외부 환경과 고립시키는 방법이다. 좀 더 세분화하면 대상물에 대한 구현 정도에 따라 더미 객체(Dummy Object), 테스트 스텁(Test Stub), 페이크 객체(Fake Object), 테스트 스파이(Test Spy), Mock 객체(Mock Object) 등으로 구분하기도 한다. 자세한 내용은 TDD 관련 전문 서적을 참고하기 바란다. 여기서는 안드로이드 프로젝트에 로컬 유닛 테스트를 도입할 때 발생할 수 있는 사례에 대해 알아본다.



SharedPreference를 Mock해보자

Mock에 대해 좀 더 알아보자. 이번에는 SharedPreferences를 사용하는 소스 코드에 로컬 유닛 테스트를 적용해본다. Shared Preferences는 안드로이드 코드이기 때문에 mock를 해야 한다. 간단하게 이름을 저장하는 SharedPreferencesHelper 클래스를 작성했다.

<리스트 4> SharedPreferens로 이름을 저장하는 코드 public class SharedPreferencesHelper { SharedPreferences mPref = null; private static final String KEY_NAME = "name"; public SharedPreferencesHelper(Context context) { mPref = context.getSharedPreferences("pref", Context.MODE_PRIVATE); } public boolean saveName(String name) { SharedPreferences.Editor editor = mPref.edit(); editor.putString(KEY_NAME, name); return editor.commit(); } public String getName() { return mPref.getString(KEY_NAME, ""); } }



saveName()과 getName()을 테스트하는 testSaveName()과 testGetName()을 작성할 것이다. 먼저 static method를 mock할 필요는 없으므로 PowerMockRunner 대신 MockitoJUnitRunner 클래스를 러너로 사용한다. SharedPreferences를 생성하기 위해서는 Context가 필요하므로 Context, SharedPreferences, SharedPreferences.Editor 객체를 mock로 생성한다. @Mock를 사용하면 간편하게 mock 객체를 생성할 수 있다. 이것이 JUnit4를 사용해야 하는 이유이기도 하다.



<리스트 5> SharedPreferencesHelper 로컬 유닛 테스트 선언부 @RunWith(MockitoJUnitRunner.class) public class SharedPreferencesHelperLocalTest { private static final String TEST_NAME = "Hello"; @Mock Context mMockContext; @Mock SharedPreferences mMockSharedPreferences; @Mock SharedPreferences.Editor mMockEditor;



다음 바로 testSaveName()을 작성해봤다. Mock 객체도 추가됐고 문제가 없어 보인다.



<리스트 6> testSaveName() 소스 코드 @Test public void testSaveName() { SharedPreferencesHelper pref = new SharedPreferencesHelper(mMockContext); assertThat(pref.saveName(TEST_NAME), is(true)); }



테스트를 간편하게 실행하는 방법은 testSaveName() 메소드에 마우스를 놓고 우 클릭해 <그림 4>과 같이 Run 메뉴를 선택하면 된다. Select 메뉴는 상단 중앙에 빠른 실행을 할 수 있게 등록하는 것이고 Run 메뉴는 로컬 유닛 테스트를 바로 실행하는 메뉴다. 실행해보면 <그림 5>와 같이 Null 예외가 발생한다.







Null 예외가 발생하는 이유는 mPref 객체가 Null이기 때문이다. SharedPreferencesHelper 객체를 생성할 때 mockContext를 넘겨주었는데, mockContext.getSharedPreferences()의 결과는 null이 리턴된다. Null 예외를 방지하기 위해서는 mockShared Preferences()를 추가해야 한다.



<리스트 7> SharedPreference 생성을 mock함 @Before public void mockSharedPreferences() { when(mMockContext.getSharedPreferences(anyString(), anyInt())) .thenReturn(mMockSharedPreferences); when(mMockSharedPreferences.edit()).thenReturn(mMockEditor); when(mMockEditor.commit()).thenReturn(true); }



먼저 mockContext.getSharedPreferences() 메소드 호출의 리턴 값으로 mMockSharedPrefereces를 지정한다. mMock SharedPreferences.edit() 메소드 호출의 리턴 값으로 mMock Editor를 지정한다. 마지막으로 mMockEdit.commit() 메소드 호출의 리턴 값으로 true를 지정한다. 처음에는 잘 이해가 되지 않을 수 있다. 일단은 그대로 따라 해보자. 필자도 처음에는 애를 먹었다. 특히 구글 문서에 공유된 예제의 경우 클래스가 몇 개 더 추가돼 있다. 필자도 처음엔 mock에 대해 확실히 이해하기 어려워 별도로 유사한 코드를 작성하고 차근차근 따라 하면서 이해할 수 있었다. 이제 실행하면 정상적으로 테스트가 실행된다.



<리스트 8> testGetName() 메소드 @Test public void testGetName() { SharedPreferencesHelper pref = new SharedPreferencesHelper(mMockContext); mockNameAs(TEST_NAME); assertThat(pref.getName(), is(TEST_NAME)); } private void mockNameAs(String name) { when(mMockSharedPreferences.getString(eq("name"), anyString())) .thenReturn(name); }



testGetName()은 SharedPreferencesHelper.getName()을 테스트하는 코드다. Get이 정상적으로 되는지 알아보려면 먼저 그 값을 저장해 놓아야 한다. mockNameAs() 메소드에서 “name”의 key에 해당하는 값을 미리 넣어 놓았다. 지금까지 when()과 thenReturn() 메소드를 활용해 mock를 수행할 때 anyString(), anyInt() 등만 사용하고 구체적인 값을 사용하지는 않았다. 위와 같이 구체적인 값을 사용하는 경우 eq(“name”)과 같이 호출해야 한다. 그냥 “name”으로만 입력하면 <그림 6>과 같은 에러 메시지가 출력된다. 정상적으로 실행되면 <그림 7>과 같이 녹색 신호등을 확인할 수 있다.







로컬 유닛 테스트, 어떻게 적용할 것인가?

로컬 유닛 테스트의 경우 mock의 개념에 익숙하지 않으면 실전에서 적용하기 어렵다. UI 코드를 제외한 로직 코드에만 적용한다고 해도 Log 클래스나 SharedPreferences와 같이 UI와 관련 없는 코드에 대해서는 모두 mock을 적용해야 하기 때문이다. 따라서 사전 계획 없이 로컬 유닛 테스트를 실전에 도입하는 것은 주의해야 한다.

추천하고 싶은 방법은 기존에 존재하던 테스트 케이스 중 AndroidTestCase를 상속한 테스트 케이스를 단계적으로 로컬 유닛 테스트로 재작성하는 것이다. 여러 가지 장점이 있는데, 첫째는 JUnit4를 새롭게 적용해볼 수 있다는 점, 그리고 디바이스에 의존하지 않는 테스트 케이스와 UI와 같이 타겟 디바이스에서 반드시 실행돼야 하는 코드를 구별할 수 있다는 점이다. 특히 HTTP나 WebSocket과 같이 네트워크에 관련된 클래스에서는 유닛 테스트를 장기간 테스트해보기도 좋고 테스트 성능을 크게 향상할 수 있다. 며칠씩 수행되는 테스트의 경우 PC에서 실행하는 것이 안정성 면에서 더 유리하다.

Mock에 대해서는 필자도 좀 더 공부해야 할 것 같다. 그동안 현업에서 유닛 테스트를 진행한 경우 Mock 기법은 최대한 배제했다. 그 이유는 개념과 적용하기가 만만치 않기 때문이다. Mock는 어려웠다. 그러나 몇 년 전과 비교하면 어노테이션, Mockito, PowerMock를 함께 사용해 기능이 강력하고 Mock를 적용하는 데 편리한 점도 많았다.

아마도 안드로이드에 Mock를 적용하는 방법은 Context, SharedPreferences, Log 등 정형화된 부분이 많으므로 안드로이드 환경에서 손쉽게 사용할 수 있는 써드파티(3rd-party) 라이브러리가 조만간 등장할 것으로 기대한다. 또한, 구글에서도 Log나 TextUtils에서의 어려움을 알고 있으며 앞으로 좀 더 쉽게 사용할 수 있는 솔루션을 제공할 것으로 기대된다.



로컬 유닛 테스트는 왜 두 번 실행되는가?

로컬 유닛 테스트를 실행하면 테스트가 두 번씩 실행된다. Unit testing support 페이지를 보면 유닛테스트는 Flavor 혹은 build type별로 실행된다고 명시돼 있다. 기본적으로 모든 앱은 debug 모드와 release 모드가 있다. 그 때문에 각각 총 두 번이 실행되는 것이다. Run 창으로 보면 :app:testDebug와 :app:testRelease를 각각 확인할 수 있다.







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

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