일레클 Android TDD 도입기 (3/3)

최정인
elecle
Published in
9 min readMar 11, 2022

일레클 Android TDD 도입기 (3/3)

안녕하세요, 일레클 서비스를 운영하는 나인투원 Mobile 팀에서 안드로이드 개발을 하고 있는 최정인입니다.

일레클 Android TDD 도입기 시리즈, 그 마지막 포스팅입니다. 본 포스팅에서는 테스트 주도 개발 방식을 적용해서 스프린트를 진행한 방식을 소개합니다. 데이터 분석용 이벤트 심기 작업을 예시로 코드와 함께 설명하겠습니다.

Sample Project — 데이터 분석용 이벤트 심기

저희는 일레클 유저들의 이용 경험을 보다 면밀히 추적하고 분석하기 위해 여러가지 이벤트들을 심고 있습니다.

유저는 일레클 전기 자전거를 다양한 방식으로 대여할 수 있다

이를테면 일레클 전기자전거를 유저가 어떻게 대여했는지 — QR코드를 스캔했는지, 일련 번호를 직접 입력했는지, 예약 기능을 사용했는지, 바로탑승 기능을 사용했는지, NFC 태그를 사용했는지 — 를 분석하기 위해서인데요. 새로운 피쳐가 출시될 때마다 그에 관련된 분석용 이벤트들도 추가되어 왔습니다.

일레클 앱이 진화함에 따라 이벤트들의 관리도 점점 복잡해졌고, 아래와 같이 문제점들이 산더미처럼 누적되었습니다.

Firebase analytics로 심어진 이벤트와 amplitude로 심어진 이벤트가 혼재되어 있음. / 더 이상 활용되지 않는 레거시 이벤트가 계속 날아옴. / 이벤트가 잘못된 위치에 심어짐. / 파라미터가 부분적으로 잘못됨. / 값이 이상함. / 어떤 이벤트는 안드로이드에만 심어져 있고, ios에는 심어져 있지 않음. (vice versa)

그래서 저희는 이번 기회에 이벤트를 심는 함수들을 관리하는 파일을 생성하고 컨벤션을 맞추고, 에러 처리하는 로직을 인터페이스화 시키는 등의 대규모 작업을 진행했습니다.

예시 Task — “riding fail” 이벤트를 심어보자!

“riding fail” 이벤트를 예시로, Red → Green → Refactor 싸이클 과정을 서술해보겠습니다. “riding fail” 이벤트는 유저가 일레클 라이딩을 성공적으로 시작하지 못했을 때, 자전거 기기에 대한 정보 및 발생한 에러 상황 등을 파악하기 위한 이벤트입니다. 이벤트 명세는 아래와 같습니다.

riding fail / 라이딩을 시도했는데 성공적으로 시작하지 못했을 때

  • exception_slug (string): {scan_ble_not_found, riding_lock_already_opened, riding_lock_opened, riding_start_fail}
  • region (string): 이용자 현재 위치가 있는 서비스구역 지역 코드 11, 36, 411. 서비스구역이 아니면 null
  • service_group (string): 이용자 현재 위치가 있는 서비스구역 id {1,2,3,...}
  • organization (string): 이용자가 스캔한 바이크의 organization 코드. organization이 없으면 null {1,2,3...}
  • inflow (string): 라이딩을 시도한 방법 {nfc, autoride, qrcode, input}

[ 🔴 Red ] > Green > Refactor

Red 단계에서는 어떤 테스트가 필요한지, 어디까지 테스트할 것인지 스코프를 정하는 과정이 필요합니다. 예를 들어, 이벤트 함수의 경우 아래와 같이 크게 세 가지의 테스트가 필요한데요.

이벤트 이름이 명세와 일치하는가?
이벤트 파라미터의 key가 명세와 일치하는가?
이벤트 파라미터의 value가 명세와 일치하는가?

이를테면 “riding fail”라는 이름의 이벤트가 들어오는지 확인하기 위해 아래와 같은 테스트케이스를 추가할 수 있습니다.

첫번째 테스트 케이스 : sendRidingFail_event_type()

Multiline 주석으로 테스트케이스에 대한 설명을 간략히 적어주고, inline 주석으로는 given, when, then을 추가해주었는데요. 해석하자면 이렇습니다.

given — 테스팅을 위한 설정들과 초기값들이 주어졌을 때,

when — 이벤트 함수 ‘sendRidingFailEvent’가 실행되면,

then — “riding fail” 라는 이름의 이벤트가 전송된다. (=앰플리튜드에 이벤트를 전송하는 함수의 첫번째 파라미터가 “riding fail”이다.)

이렇게 주석을 달아주면 GREEN 단계에서 함수를 실제로 구현할 때에도 용이하고, 리뷰어 입장에서도 각 테스트케이스가 무엇을 테스트하는 것이며 타겟 함수가 어떤 일을 수행하는지 파악하기 좋습니다.

그리고 이벤트 파라미터들의 key가 잘 들어오는지는 아래와 같이 테스트할 수 있습니다.

두번째 테스트 케이스 : sendRidingFail_event_parameter_key()

mockK의 ‘verify’ 구문을 통해 특정 함수가 실행되었는지 여부를 확인할 수 있습니다. ‘exactly=1’는 말 그대로 한 번 실행 되었는지를 확인하는 구문입니다. slot, every, withArg 또한 빈번하게 사용하는 구문들인데요. 자세한 활용법은 mockK 공식문서에서 확인하실 수 있습니다 :)

이벤트 파라미터들의 value에 대한 테스팅도 key 테스팅과 유사하게 수행할 수 있습니다. 다만 한 가지 유의할만한 점은 nullable한 변수들의 값으로 null이 주어지는 경우도 테스트해야한다는 것입니다.

세번째 테스트케이스 : sendRidingFail_event_parameter_value_null_case()

이렇게 null case에 대한 테스팅도 모두 수행해야, 커버리지를 추출할 때 모든 분기가 잘 테스트 되었음을 확인할 수 있습니다.

Red > [ 🟢 Green ]> Refactor

Green 단계에서는 테스트 케이스들이 통과할 수 있도록, 실제 함수를 구현합니다. “riding fail” 이벤트의 이름, 파라미터의 key와 value가 명세와 일치할 수 있도록 함수를 구현해보면 아래와 같습니다.

일단 line 9에서 “riding fail”라는 이름을 인자로 넘겨주었으니, 앞서 소개된 첫번째 테스트케이스를 통과할 것입니다.

그리고 line 2에서 7을 보면 이벤트 파라미터의 key와 value를 명세에 맞춰 지정해주고 있는데요. 이렇게 하면 두번째와 세번째 테스트케이스도 통과를 하게 됩니다.

단, line3과 line7에서 “exception_slug”와 “inflow”를 둘 다 String 파라미터로 넘기고 있다는 점을 주목해주세요! (곧 이어 소개될 refactor 단계에서 이 부분을 수정할 예정입니다. 😉)

Red > Green > [ 🟡 Refactor ]

Refactor 단계에서는 말 그대로 리팩토링을 해야 합니다. 이제까지 테스트케이스를 통과했으니, “riding fail” 이벤트 함수를 실제로 어딘가에 심어볼 차례인데요. 그러다보면 새로운 문제들 — 파라미터를 계속 타고 타고 전달해줘야 한다거나, 잘못된 값이 들어와도 이를 미리 캐치할 방법이 없다거나 — 이 보이게 됩니다.

앞서 Green 단계에서는 “inflow”를 String으로 넘겨주었습니다. 하지만 다시 이벤트 명세를 살펴보면, enum으로 관리하는 것이 훨씬 편리하고 안전해보입니다.

  • inflow (string): 라이딩을 시도한 방법 {nfc, autoride, qrcode, input}

String으로 전달해준다면, inflow로 어떤 값이 주어질 수 있는지 확인하기 위해 이벤트 함수의 모든 usage를 클릭해봐야 합니다. 또한, 오타가 나더라도 캐치하기 어렵고, 서로 다른 목적의 String들(ex. 이벤트용, 웹뷰 endpoint용 등)과 헷갈릴 여지도 높습니다.

위와 같이 RidingInflow 라는 enum class를 생성해주고, 파라미터를 String 대신 RidingInflow 타입으로 교체해준다면 아래와 같습니다.

이렇게 enum을 활용할 경우 팀원/후임자가 “가능한 라이딩 시도 방식에는 무엇이 있는지” 파악하기도 좋고, 코드 레벨에서도 type safety가 보장되어 좋습니다.

“exception_slug” 또한 이벤트 명세를 보아하니 String으로 넘겨주는 것보다 더 나은 관리 방식이 존재할 것 같은데요.

  • exception_slug (string): {scan_ble_not_found, riding_lock_already_opened, riding_lock_opened, riding_start_fail}

이 경우 enum보다 sealed class를 사용하는 것이 더 낫겠다고 판단했습니다. RidingFailError 라는 sealed class를 생성하였고, 세부적인 에러 케이스들을 sub class로 생성하고 inflow를 넘겨주었습니다.

그리하여 최종적으로 완성된 이벤트 함수 sendRidingFailEvent 는 아래와 같습니다!

일레클 Android TDD 도입기 시리즈를 마무리하며

세 개의 포스팅을 작성하는 내내 많은 고민들을 했습니다. “ ‘mockK를 활용한 테스트코드 작성법’ 이런 식으로 튜토리얼성 글을 썼으면 좀 더 나았으려나..” 라는 생각도 들었습니다. 하지만 그런 부분은 라이브러리 공식 문서를 읽는 것이 훨씬 정확하고 유익할 것이기에, 오히려 medium 포스팅으로는 저희가 밟아온 시행착오들을 소개하는 것이 낫겠다고 판단했습니다.

본 포스팅에서는 Red → Green → Refactor 싸이클을 코드와 함게 설명드렸는데요. 최대한 쉽고 단순한 예시를 들려고 했지만, 아무래도 일레클 특수한 상황을 설명하려다 보니 100% 전달되지 않을 것 같아 아쉬움이 남습니다.

혹시나 팀에 TDD를 도입하고 싶지만 뭐부터 어떻게 할지 모르겠어서 막막하시는 분들께 이 시리즈가 도움될 수 있다면 좋겠습니다. 🙂

시리즈 전체 보기

일레클 Android TDD 도입기 (1/3) → TDD를 도입하기까지의 배경, 본격적으로 도입하기까지의 기나긴 과정
일레클 Android TDD 도입기 (2/3) → 리팩토링 세션을 통한 테스트 코드 작성 연습, 컨벤션 맞추기
일레클 Android TDD 도입기 (3/3) → 본격적으로 테스트 주도 개발 방식을 적용해서 스프린트를 진행한 과정

--

--