좋은 소프트웨어를 만들기 위해서는 테스트 자동화가 중요합니다. 꼼꼼하고 정확한 자동화 테스트는 QA 비용을 줄이고, 제품의 질을 높입니다. 리멤버의 백엔드는 오래 전부터 TDD를 적용하였습니다. 하지만 클라이언트는 View단과 직접 엮여있기 때문에 겪는 테스트의 어려움과, 빠르게 처리해야 할 비즈니스 요구사항에 밀려, 테스트 자동화를 하지 못하고 있었습니다.
우리는 저번 페이즈 브레이크 때 클라이언트 개발자들끼리 모여 테스트에 대해 논의하였습니다. 그리고 테스트부터 CI까지 이어지는 전략을 정립하고, 각 플랫폼에 맞게 적용하였습니다. 이번 포스팅에서는 안드로이드에서 테스트를 도입하기 위해 했던 구조적인 고민과 적용, 그리고 참고했던 링크들을 나누고자 합니다.
MVC에서 MVP로
MVC는 웹 어플리케이션에서 많이 사용하는 패턴입니다. 안드로이드 또한 MVC로 빠르고 강력하게 개발할 수 있습니다. 하지만 대부분의 기능이 Context라는 화면 속성으로부터 출발하는 안드로이드의 특성 상, Controller과 View가 붙어있게 됩니다. 이것은 테스트를 매우 어렵게 합니다. 테스트는 View와 Model을 나누어야 하는데, Model의 로직을 View와 Controller가 모두 접근하고 있고, Controller와 View는 한 공간에 있기까지 하니, 테스트를 분리하기가 까다롭습니다.
그래서 우리는 테스트를 위해 기본 구조를 MVP로 변경하기로 결정하였습니다. MVP는 View와 Model을 분리하고, 둘 사이 데이터를 주고받는 로직 또한 Presenter에 모아놓기 때문에, 각 레이어에 대해 테스트가 용이합니다. MVC와 MVP에 대해 코드를 포함한 자세한 내용은 아래 링크를 참고해주세요.
http://tosslab.github.io/android/2015/03/01/01.Android-mvc-mvvm-mvp.html
Dagger2
Dagger2는 Square에서 만든 Dagger를 Google이 Fork하여 개선한 DI 프레임워크 입니다. 의존성에 따른 객체생성을 추상화 하여 보일러플레이트 코드를 줄이고, 변화에 유연하게 만듭니다.
MVP는 View에 Presenter 객체, Presenter에 Model 객체가 생성되어야 합니다. Dagger2를 사용하면 여러 클래스에 의존하는 Presenter와 Model 객체를 깔끔하게 생성할 수 있습니다. 의존성을 자유롭게 바꿔가며 유연하게 Model과 Presenter의 Constructer를 바꿀 수 있습니다. 또한 하나의 Presenter를 여러 곳에 사용하던가, 하나의 View에서 여러 Presenter를 갈아 끼우며 사용해야 할 때, DI를 이용한 객체생성 추상화는 큰 힘을 발휘합니다. 저는 Dagger2를 이용하여, MVP를 좀더 깔끔하게 정리하기로 했습니다.
Dagger2는 활용성이 매우 높지만, 학습비용이 높습니다. 하지만 하루정도 시간을 두고 천천히 학습하신다면, 충분히 익히고 프로덕트에 적용할 수 있습니다. Dagger2에 대한 내용은 아래 링크를 참고해주세요.
https://medium.com/@jsuch2362/android-깨알팁-4-dagger2-7f38cd9cb11b
Test
지금까지 했던 모든 작업은 사실 테스트를 위해서 했던 사전 작업입니다. 이제 테스트를 작성합시다. MVP는 Model Layer, View Layer, Presenter Layer 3개의 레이어를 따로 테스트 합니다. Model은 전통적인 JUnit, Presenter는 구글에서 제공하는 Android Test Support Library(ATSL)를 사용합니다. View는 ATSL에 더하여 Espresso를 사용하여 테스트 합니다. 다운로드 및 간단한 사용법은 아래 링크를 참고해주세요.
https://google.github.io/android-testing-support-library/
리멤버는 이제 구조를 바꾸면서 테스트를 추가하고 있기 때문에 아직 Presenter Test까지만 작성하고 있습니다. Presenter의 테스트 코드는 다음과 같습니다.
@RunWith(AndroidJUnit4.class) public class SigninEmailFragmentTest { private static final String GOOD_ID = "test@email.com"; private static final String GOOD_PASSWORD = "123456"; private SigninEmailPresenter.View mView; private AuthHelper mAuthHelper; private SigninEmailPresenter mPresenter; @Before public void setUp() throws Exception { // Mock을 생성한다. this.mView = Mockito.mock(SigninEmailPresenter.View.class); this.mAuthHelper = Mockito.mock(AuthHelper.class); this.mPresenter = new SigninEmailPresenter(mView, mAuthHelper); } // ID가 비어있는지 체크한다. @Test public void testSigninWithBlankId() throws Throwable { // Presenter 메소드에 테스트 목적에 맞는 파라미터를 넘긴다. mPresenter.signin("", GOOD_PASSWORD); // 원하는 메소드가 출력되는가 체크한다. Mockito.verify(mView).inputIdError(R.string.alert_empty_email); } // 비밀번호가 비어있는지 체크한다. @Test public void testSigninWithBlankPassword() throws Throwable { mPresenter.signin(GOOD_ID, ""); Mockito.verify(mView).inputPasswordError(R.string.alert_empty_password); } // 바른 파라미터가 전달되었을 경우 Sign in이 잘 되는지 테스트 한다. @Test public void testSigninWithGoodAccount() throws Throwable { // Presenter에서 실제로 메소드를 호출하는 Helper의 행동을 Mock으로 정의한다. Mockito.when(mAuthHelper.login(GOOD_ID, GOOD_PASSWORD)) .thenReturn(Observable.just(new Response())); // Presenter 메소드를 바른 파라미터로 실행한다. mPresenter.signin(GOOD_ID, GOOD_PASSWORD); // 원하는 결과를 체크한다. Mockito.verify(mView).gotoMainActivity(); } // Sign in이 실패하였을 때 Fail message를 잘 보여주는지 테스트 한다. @Test public void testSigninFail() throws Throwable { // Presenter에서 Helper 메소드가 Fail하도록 Mock을 설정한다. String message = "signinFailMessage"; Mockito.when(mAuthHelper.login(GOOD_ID, GOOD_PASSWORD)) .thenReturn(Observable.error(new RuntimeException(message))); mPresenter.signin(GOOD_ID, GOOD_PASSWORD); Mockito.verify(mView).showFailDialog(message); } }
종합 예시
지금까지 설명드린 MVP, Dagger2, Test Library들을 모두 활용하여 기본적인 기능부터 Model, View, Presenter 테스트까지 구현한 좋은 예시가 있습니다. 아래 링크를 참고해주세요. 위에 말씀드린 것들을 ‘왜 해야 하는가?’ 부터 고민하면서 천천히 학습한다면, 마지막에 이 예시를 보았을 때 많은 도움이 될 것입니다.
https://github.com/ZeroBrain/GDG-ATSL-ON-MVP
Continuous integration (CI)
테스트까지 구현하였지만, 한가지 단계가 더 남아있습니다. 배포 자동화 입니다. 사실 안드로이드는 결국 apk를 뽑아서 플레이 스토어에 직접 올려야 하기 때문에, 모든 배포 과정을 자동화 하기 어렵습니다. 하지만 코드를 작성하고 Pull Request를 날림과 동시에, Test 진행 후 apk까지 뽑게 함으로써, 상당한 부분을 자동화 할 수 있습니다.
배포 자동화를 돕는 툴로는 Jenkins와 Travis가 있습니다. 설치형과 PaaS 형태의 차이점이 있습니다. 배포할 제품이 많고 주기가 짧다면 자체적으로 구축한 Jenkins 서버에서 진행하는 것이 자유도가 높습니다. 하지만 우리는 리멤버 한가지 제품만을 개발하고, 소수의 개발자가 배포를 하기 때문에, 관리비용을 줄일 수 있는 Travis를 선택하였습니다.
Travis를 안드로이드에 적용하기 위해서는 프로젝트 Root에 .yml을 추가하여야 합니다. Travis를 안드로이드에 적용하기 위한 가이드는 아래 링크를 참고해주세요.
https://docs.travis-ci.com/user/languages/android
Travis는 public Repository는 무료고, private Repository는 유료입니다. 그리고 햇갈렸던 부분인데, 유료플랜을 구독하신다면 https://travis-ci.com/ 로 사용하셔야 합니다. https://travis-ci.org/ 로 사용하시면 private Repository가 보이지 않습니다. 가격 정책은 https://travis-ci.com/plans 여기를 참고해주세요.
적절한 .yml로 Travis와 안드로이드를 성공적으로 연동하면, Travis가 git push, pull request 등의 이밴트를 감지하여 자동으로 테스트 코드를 실행합니다. 어떤 이밴트에 따라 배포할지 등의 자세한 옵션은 Travis 콘솔 설정창에서 조정할 수 있습니다. 테스트가 끝나면 결과가 Github 이밴트에 첨부되어 보여집니다. 만약 테스트가 실패할 경우 .yml 설정을 조정하여 따로 노티를 받을 수도 있습니다.
apk까지 뽑기 위해서는 release 설정을 .yml에 더 추가해야 합니다. release를 위한 여러 방법이 있지만, 저는 Github에서 테스트와 함께 release까지 처리하기로 선택하였습니다. Travis 빌드와 함께 Github에 apk를 자동으로 release하기 위한 .yml 설정은 아래 링크를 참고해주세요.
https://docs.travis-ci.com/user/deployment/releases
https://isjang98.github.io/blog/Travis-ci-for-Android
테스트 자동화 합시다
지금까지 테스트를 하기위해 고민하고 적용했던 과정을 소개하였습니다. 아직은 인프라 구축을 이제 시작한 것이기에 개선해야 할 부분이 많이 있습니다. 하지만 테스트를 자동화 하면서 개발 퀄리티가 많이 좋아진 것을 체감하고 있습니다. 테스트가 주는 안정감도 있지만, 테스트를 위해 로직을 분리하며 구조를 개선한 것이 큰 도움이 됩니다. 당장 배포 때마다 하는 QA 테스트에서, 발견되는 버그의 숫자가 절반 이상 줄었습니다.
처음 시작한다면 하나하나 배우는데에 허들이 느껴질 수 있습니다. 하지만 차근차근 학습해 나간다면 높은 생산성을 얻을 수 있습니다. 테스트 자동화 합시다.