플라워로드 기술 블로그 : http://blog.flowerroad.ai
이글은 2020년 12월 25일에 작성된 글입니다.
Unit Test?
일단 유닛테스트란 구현하는 method를 테스트하는 또 다른 method의 집합체 이다. 하나의 유닛 테스트 method는 테스트 하려고 하는 method의 특정 루틴을 검사한다. 이말은 하나의 method를 테스트 하기 위해 기본적으로 성공, 실패, 예외 등 여러 유닛 테스트 method가 필요하다.
스타트업에서 바쁜데 굳이 이걸??
소프트웨어 엔즈니어라면 한번쯤은 들어봤거나 당연하게 사용하고 있을것이다. 하지만, 아직 국내 작은 기업 혹은 스타트업에서는 바쁘다는 이유만으로 이를 무시하는 경우도 많다고 한다. 사실 나도 15년정도 개발자로 일하면서 TDD적용해서 개발할때를 제외하고 이를 엄격하게 지킨 경우는 그리 많지 않은것 같다. 하지만, 이를 적용했을때와 적용하지 않았을때의 소프트웨어 품질 유지 격차는 매우 컷다.
기본적으로 유닛 테스트를 통해서 method단위의 여러 경우의 수를 검증하고 릴리즈 전에 basic 한 문제의 발견해서 수정 할 수 있다.(적용후 기본적인 null point access exception 문제는 거의 사라진다...) 또한, 잘 만들어진 유닛테스트는 코드의 리팩토링에 대한 부담에서 자유롭게 해준다.
초기 MVP(Minimum Viable Product)를 target 으로 만드는게 아니라면 가급적 적용하는것이 좋다. 제품 코드라는것이 처음 시작할때에는 뭐.. 기능이 그리 많지 않으니 괜찮겠지 하고 시작하지만 1년만 지나도 눈덩이 처럼 불어난 코드와 기능들 때문에 테스트를 감당할 수 없고, 릴리즈 할때마다 메일박스에 쌓여 있는 exception 메일을 받아보고 자괴감에 빠지는 나를 발견할수도있다.
일단 한번 조그만것 부터 적용해 보고 판단합시다!
Setup
Android 에서 Unit Test를 적용 하는 방법은 많은데 이 글에서는 아래 나열된 라이브러리를 gradle dependency에 추가해서 진행할 예정이다.
dependencies{
testImplementation 'junit:junit:4.12'
testImplementation "androidx.arch.core:core-testing:2.1.0"
testImplementation 'org.mockito:mockito-core:2.19.0'
testImplementation 'org.powermock:powermock-module-junit4:2.0.4'
testImplementation 'org.powermock:powermock-api-mockito2:2.0.4'
}
뭔가 많다.... 간단하게 설명하면
- JUnit
- 기본적인 Java Unit Test Framework이다. test 어노테이션(@Test)을 간편하게 test method 작성/실행 할 수 있다.
- Mockito
- Unit Test를 위한 Java Mocking Framework 이다.
- Unit Test를 하려고 하는 class, method에서 다른 class를 사용하는 경우 해당 class를 mocking 해서 테스트 할때 원하는 동작을 지정 해준다.
- PowerMockito
- 간단한 수준의 유닛 테스트는 Mockito로도 충분하지만 코드의 구조가 복잡한 경우 부족한 부분이 있다.
- Log의 mocking이 필요 할때 유용하게 사용된다.
이정도면 일단 기본적인 Unit Test는 가능하다.
코드 위치
프로젝트를 생성하면 아래와 같이 Unit Test 코드를 위치시킬 수 있는 폴더 및 default code를 확인 할 수 있다. 아래 위치에 앱의 구현코드와 동일한 package 구조를 만들고 각각의 class 마다 하나의 Unit Test Code파일을 만들면 구분/관리하기 쉽다.
이 블로그에서는 예제를 위해서 아래와 같은 folder 구조와 예제 소스 파일들을 추가 하고, 이중 Unit Test를 진행할 구현 class 가 있는 소스 파일은 WebSocketManager.java이고, 해당 구현 class 를 테스트할 Unit Test가 구현된 소스 파일은 testWebSocketManager.java를 사용해서 진행한다.
진행전 미리 알아 두면 좋을 내용
이 블로그의 예제에서는 org.java_websocket.client.WebSocketClient을 warapping한 WebSocketManager class를 대상으로 하고, 파일 이름으로 유추 할 수 있듯이 WebSocket Module 이다.
테스트할 코드
public class WebSocketManager implements SocketManager{
private static final String TAG = "WebSocketManager";
private URI mURI = null;
public WebSocketManager(){
}
/*************************************************************************
* Private Method
*************************************************************************/
private void setConnectionStatus(boolean status){
this.connectionStatus = status;
}
private void checkConnection() throws Exception{
if(!this.isConnected()){
Log.d(TAG, "checkConnection: Is not connected");
throw new Exception();
}
}
/*************************************************************************
* public Method
*************************************************************************/
public URI getURI(){
return this.mURI;
}
public WebSocketManager setURI(String uri) {
try{
this.mURI = new URI(uri);
Log.d(TAG, "setURI: " + this.mURI);
} catch(Exception e){
e.printStackTrace();
this.mURI = null;
}
return this;
}
public WebSocketManager initSocket(){
try{
WebSocketManager self = this;
this.mSocket = new WebSocketClient(this.getURI()){
@Override
public void onOpen(ServerHandshake handshake) {
Log.d(TAG, "onOpen: " + handshake);
self.mListener.onConnect();
self.setConnectionStatus(true);
}
};
return this;
}catch(Exception ex){
ex.printStackTrace();
throw ex;
}
}
}
위와 같은 class를 간단하게 작성하고 해당 class의 setURI method를 Unit Test로 확인해본다.
간단한 Unit Test 코드를 만들어 보자
일단 Unit Test를 위한 class를 만들고, @RunWith(PowerMockRunner.class) 어노테이션을 이용해서 powermock을 사용하도록 설정 하자. 그리고, static method나 생성자를 mocking할때 사용되는 @PrepareForTest({A.class, B.class}) 어노테이션을 이용해서 Log.class를 mocking해주자.
@RunWith(PowerMockRunner.class)
@PrepareForTest({Log.class})
public class testWebSocketManager {
}
이제 기본 class는 준비가 되었으니 아래와 같이 get/setURI를 테스트 하는 Unit Test 코드를 본격적으로 작성해 보자
Unit Test Method는 @Test 어노테이션을 이용해서 작성하면 된다. method의 내용은 간단하게 객체를 생성하고, setURI()후 getURI() 을 해서 서로 같은지 비교하는 구문이다.
@RunWith(PowerMockRunner.class)
@PrepareForTest({Log.class})
public class testWebSocketManager {
@Test
public void testGetSetURI(){
WebSocketManager inst = new WebSocketManager();
inst.setURI("<HTTPS://custom.unit.test.com>");
System.out.println("URI : " + inst.getURI());
assertEquals("<HTTPS://custom.unit.test.com>", inst.getURI().toString());
}
}
작성후 단축키 Ctrl+Shift+F10 혹은 메뉴바 아래의 run Icon을 이용해서 테스트를 실행해 보자.
java.lang.RuntimeException: Method d in android.util.Log not mocked. See <http://g.co/androidstudio/not-mocked> for details.
at android.util.Log.d(Log.java)
at com.example.example_unittest.lib.socketmanager.WebSocketManager.setURI(WebSocketManager.java:52)
at com.example.example_unittest.lib.testWebSocketManager.testGetSetURI(testWebSocketManager.java:44)
.
.
응?...응??
뭔가 에러가 발생했다. "Method d in android.util.Log not mocked" 문구로 보아하니 Log class를 mocking하지 않아서 발생했다.
위의 testWebSocketManager Class 를 확인 해보면 Log를 mocking하기 위해서 powermock을 동작시키고 준비만 시켰지 실제로 mocking하는 구문을 넣지 않은 것을 발견 할 수 있다.
아래와 같이 Log class의 mocking이 모든 Unit Test method들에게 적용될 수 있도록 @Before 어노테이션하부에 setup() method를 만들고 powermockito를 이용해서 Log class를 mocking해주자.
여기서 @Before 어노테이션이 하는 역활은 Unit Class 내부의 @Test 어노테이션으로 정의된 모든 Unit Test Method를 실행하기 전에 먼저 실행 해서 Unit Test Method에 필요한 공통 선행 작업을 정의해 둘 수 있다.
@RunWith(PowerMockRunner.class)
@PrepareForTest({Log.class})
public class testWebSocketManager {
@Before
public void setup() {
PowerMockito.mockStatic(Log.class);
}
@Test
public void testGetSetURI(){
WebSocketManager inst = new WebSocketManager();
inst.setURI("<HTTPS://custom.unit.test.com>");
System.out.println("URI : " + inst.getURI());
assertEquals("<HTTPS://custom.unit.test.com>", inst.getURI().toString());
}
}
짜잔! 아래와 같이 테스트에 성공해서 Unit Test Method이름 왼쪽에 녹색 체크 표시가 붙은것을 확인 할 수 있다.
Mocking과 Mocking된 class를 주입해보자(Mock, InjectMocks)
위에서 간단하게 get/set method를 Unit Test로 테스트해 봤다. 이제 외부 class를 사용하는 method를 Unit Test로 테스트 해보자.
기본적으로 Log를 mocking과 비슷한 내용이나 powermockito를 사용하지 않고 @Mock, @InjectMocks 어노테이션을 사용해서 Unit Test Method를 동작시키는 방법을 살펴 보자.
아래와 같이 테스트 할 코드 method중 initSocket() 를 테스트 할 예정인데 이 method는 내부에서 WebSocketClient class의 instance를 생성하고 있다. 이 경우 Unit Test에서 WebSocketClient 자체는 테스트의 대상이 아니기 때문에 mocking한 class를 만들어서 해당 class를 제외한 나머지 루틴들을 검증해야한다.
public WebSocketManager initSocket(){
try{
WebSocketManager self = this;
this.mSocket = new **WebSocketClient**(this.getURI()){
@Override
public void onOpen(ServerHandshake handshake) {
Log.d(TAG, "onOpen: " + handshake);
self.mListener.onConnect();
self.setConnectionStatus(true);
}
};
return this;
}catch(Exception ex){
ex.printStackTrace();
throw ex;
}
}
아래 Unit Test Method 코드 처럼 일단 @Mock어노테이션을 이용해서 WebSocketClient class의 mock 객체를 만들고, 해당 mocking된 객체를 @InjectMocks 어노테이션을 이용해서 WebSocketManager 객체에 주입하자. 그리고, Unit Test Method시작할 때 MockitoAnnotations.initMocks(this) 를 이용해서 초기화 하는것도 잊지 말자.
@Mock
WebSocketClient webSocketClient;
@InjectMocks
WebSocketManager webSocketManager;
@Test
public void testInitSocket(){
**MockitoAnnotations.initMocks(this);
webSocketManager.initSocket();
System.out.println("socket: " + webSocketManager.getSocket());
System.out.println("instance socket: " + webSocketClient);
assertNotEquals(webSocketManager.getSocket(), webSocketClient);
}
이렇게 간단하게 외부 library class를 사용하는 method도 해당 class의 동작을 배제하고 테스트 하려고 하는 method만 Unit Test로 확인 할 수 있는 방법을 알아 보았다.
주요 Error Case
- Log.class를 mocking 하지 않아서 발생하는 에러
마치며...
뭔가 글이 많이 길어진 느낌이 있는데 실제로 해보면 내용이 그리 어렵지않다. 특히 Javascript의 Mocha 를 사용해본 경험이 있는 개발자라면 매우 쉽게 적응 할 수 있을거라 생각된다.
Project를 시작하는 단계나 전체 규모가 크지 않은 경우 빠르게 적용해서 주기적인 빌드(CI/CD)와 함께 Unit Test도 적용하면 어이없는 exception mail을 받아보는 일이 많이 줄어들것이다.
복잡한 class구조의 경우나 listener 가 있는 구조의 class등 좀 까다로운 경우 powermockito의 사용법을 찾아보면서 진행해야 하는 경우도 있을 수 있으나, 왠만해서는 reference할 수 있는 자료를 인터넷상에서 쉽게 찾을 수 있어서 어렵지는 않다.
물론 rare 한 class구조 이거나 프로젝트가 design pattern(MVP, MVVM 등)이 적용 되어 있지 않은 경우(쉽게 Fist99 방식으로 짜여진 코드의 경우)는 적용하기 매우 힘들 수 있다.
대부분 Unit Test가 적용되어 있지 않은 코드는 design pattern또한 적용되어 있지 않은 경우가 많은데, 일단 단계적인 refactorying을 통해서 design pattern 적용을 진행하고 이때 Unit Test도 함께 적용해서 코드의 품질 유지를 하기 바란다.
참고 사이트
- https://developer.android.com/training/testing/unit-testing/local-unit-tests?hl=ko
- https://junit.org/junit4/
- https://site.mockito.org/
- https://github.com/powermock/powermock
- https://jdm.kr/blog/222
각주 : Fist99는 주먹구구를 뜻한다 .....
'개발 > android' 카테고리의 다른 글
[Android] DI(Dependency Inversion, 의존성 역전) 적용하기 (0) | 2022.01.28 |
---|