엔지니어링

Sendbird Chat Android SDK의 테스트 코드 개선하기

Share
Sendbird Chat Android SDK의 테스트 코드 개선하기

상담을 요청해보세요

Sendbird의 제품에 대해 더 자세한 정보를 얻고 문의사항에 대한 답변을 받으세요.

상담 요청하기

센드버드 SDK에서는 제품의 퀄리티를 올리기 위해서 테스트를 적극적으로 만들고 있습니다

하지만 테스트 개수가 증가하면서 테스트의 유지보수도 점점 어려워지고 있습니다.

SDK 팀에서는 그러한 문제를 해결하기 위해 여러 가지 방면에서 꾸준하게 테스트 코드를 개선하고 있습니다.

이 글에서 몇 가지의 개선사항을 공유하겠습니다.

코드 예시는 kotlin으로 작성했으나, 대부분 다른 언어와 플랫폼에도 적용될 수 있는 항목들입니다.

 

Assumptions vs Assertions

센드버드에서는 통합 테스트(integration test)를 수행하고 있습니다. 

통합 테스트는 유저와 가까운 환경에서 동작을 검증해 볼 수 있다는 장점이 있지만, 어려운 점들도 있습니다.

그중 하나는, 여러 가지 컴포넌트를 통합해서 테스트하기 때문에 실패할 수 있는 지점이 많다는 점입니다. SDK logic과 관련 없이도 테스트가 실패할 수도 있습니다. 

센드버드 SDK의 기능을 예시로 들어보겠습니다.

센드버드 SDK에는 메시지를 다른 채널로 복사할 수 있는 기능이 있습니다.

해당 기능을 통합 테스트하는 시나리오입니다.

  1. 로그인
  2. “Channel1” 이름을 가진 채널 생성
  3. “Channel1” 에 메시지 전송
  4. “Channel2” 이름을 가진 채널 생성
  5. “Channel1” 에서 “Channel2” 로 메시지 복사

이 중에서, 최종적으로 테스트하고 싶은 단계는 5번 단계입니다.

나머지 단계는 5번 단계를 수행하기 위한 부수적인 단계입니다.

만약 5번 단계에서 테스트가 실패한다면, 확실하게 복사 기능에 문제가 있다고 생각할 수 있습니다.

그렇지만 3번 단계에서 테스트가 실패한다면 어떨까요? 테스트의 목적인 메시지 복사를 실행하지도 않았는데 이 테스트를 실패라고 간주할 수 있을까요?

3번 단계의 실패와 5번 단계의 실패는 다른 형태의 실패라는 것은 확실합니다.

그래서 assumption과 assertion을 나누는 방식을 도입했습니다.

Assumption은 테스트가 정상적으로 실행되기 위해서 만족해야 하는 조건들을 의미합니다.

Assertion은 테스트의 목적이 되는 부분을 의미합니다.

위의 시나리오에서는, 1~4번 단계가 assumption이고 5번 단계가 assertion입니다.

Assumption과 assertion을 구분을 잘 지어둔다면 다음과 같은 장점이 있습니다.

  1. 테스트 코드의 목적을 빠르고 정확하게 파악할 수 있습니다.
  2. 테스트가 실패했을 때, 핵심 로직에서 문제가 있는지 혹은 configuration, 외부 환경에 문제가 있는지, SDK 외의 다른 컴포넌트에 문제가 있는지 등을 빠르게 파악할 수 있습니다.

일반적으로, assumption 성격의 작업은 setUp 단계에서 이루어집니다. 

setUp 단계는 각 테스트 실행 이전에 실행되는 단계이고 Junit에서는 @Before Annotation을 통해 구현할 수 있습니다. 이 단계에서 주로 필요한 resource들을 설정하게 됩니다. 이 setUp 단계를 적절하게 구성하는 것만으로도 assumption을 잘 구분할 수 있습니다.

SDK 팀에서는 setUp 단계 구성 말고도 다른 방식으로도 assumption을 구분하고 싶었는데요.

다음과 같은 이유가 있었습니다.

  • setUp 단계에서 공유되지 않는 assumption을 표현하고 싶을 때
  • 기존에 작성된 테스트에서 setUp 단계를 수정하지 않으면서 빠르게 assumption을 표현하고 싶을 때

Assumption 부분에서 검증에 실패하면, AssumptionFailedException 을 발생시키는 방식을 채택해 적용했습니다. 다음과 같은 함수를 활용했습니다.

이를 위에 시나리오에 적용해본 테스트 코드입니다.

테스트 코드상으로도 어떤 부분이 핵심 테스트 로직인지 알아볼 수 있습니다.

실패했을 때의 메시지는 다음과 같이 차이가 있습니다.

언뜻 보기에는 큰 차이가 없어 보이는데요,테스트의 개수가 늘어나면서 각 엔지니어가 모든 테스트의 내용을 전부 알 수 없게 되었습니다. 또한 기능이 추가되고 고도화되면서 시나리오들은 점점 더 복잡해졌습니다. 따라서 “메시지 전송에 실패했습니다” 의 실패 문구만 보고서는 로직 자체에 문제가 있는지, 부수적인 부분에서 문제가 있는지 파악하기가 어려웠습니다.

따라서 위와 같이 assumption 단계에서 실패했다는 정보가 추가로 들어갔을 때 시나리오의 의미와 실패 원인을 파악하는 것이 용이해졌습니다.

 

유용한 실패 메시지

테스트가 실패했을 때 적절한 실패 메시지를 주는 것은 중요합니다. 

특히, 통합 테스트에서는 더 중요한데 여러 컴포넌트를 함께 테스트하기 때문에 비확정적인 경우가 있고 테스트를 재실행 했을 때 다르게 동작할 수 있기 때문입니다.

그런 경우 실패 메시지에 많은 정보가 담겨있다면, 원인을 빠르게 추적할 수 있을 겁니다.

다음은 리스트의 크기를 검증하는 테스트입니다.

Numbers의 크기가 4이기 때문에 테스트가 실패 했습니다. 하지만, 실패 메시지만 봐서는 `numbers의 크기가 5가 아니다`라는 것만 알 수 있습니다.

다음과 같이 다른 형태의 assertion을 사용해 보겠습니다.


이제 실패 메시지를 보고 실제 list의 size 값이 무엇이었는지 알 수 있습니다.

여기서 더 나아가서 assertion library에서 제공하는 assertion을 사용해보겠습니다.


이제 실패 메시지만 보아도, 다음 정보를 알 수 있습니다.

  1. 어떠한 조건 때문에 실패했는지 (size가 5여야 했는데 실제로는 4였다)
  2. 실패를 파악하기 위한 부수적인 정보 (numbers의 값들)

유용한 실패 메시지를 주기 위해서 다양한 방법을 사용할 수 있겠지만, 제가 추천하는 방법은 assertion library(kotest-assertion, assertj 등)를 활용하는 것입니다. 상황에 맞는 적절한 assertion을 사용할 수 있다면 테스트 코드의 readability 또한 같이 개선될 것입니다. 

 

콜백 함수에서 검증하지 않기

다음은 콜백 함수를 받는 함수를 테스트하는 코드입니다.

위 테스트 코드의 접근법에는 다음 문제가 있습니다.

  1. Assertion이 실패한 경우 문제가 생길 수 있습니다. 콜백 함수가 호출되는 쓰레드가 테스트 프레임워크의 main thread가 아닐 수 있고, 이 경우 테스트 프레임워크에서 assertion error를 감지하지 못할 수 있습니다.
  2. 타이밍이 안 맞아서, 다른 테스트가 실행되는 동안 콜백 함수가 호출되는 경우 다른 테스트에 영향을 줄 수 있습니다. Test isolation을 저해하는 요소가 됩니다.
  3. 가독성이 좋지 않습니다. 만약 연속적으로 위 함수를 테스트해야 한다고 했을 때, 콜백 함수 안에서 다시 함수를 호출하게 되면 indent가 계속 늘어나게 되고 가독성이 더욱 나빠지게 됩니다.

이를 해결하기 위한 해결책들은 다양하게 있습니다. 다음과 같은 예시가 있습니다.

  1. 콜백 함수를 blocking 호출로 변환해 테스트
  2. 콜백 함수를 coroutine으로 변환해 테스트
  3. Mockito 등의 mock 라이브러리를 사용하여 콜백 함수의 결과를 기록하고 verify

이 중에서 라이브러리 dependency 없이 가장 간단하게 적용할 수 있는 1번을 예시로 보여드리겠습니다.

먼저 sendMessge 함수로 blocking으로 변환하는 함수를 extension function으로 작성했습니다.

그러면 다음과 같이 테스트 코드를 수정할 수 있습니다.

수정된 테스트 코드가 이전 테스트 코드보다 간결해졌습니다. 위에 제시된 문제점도 같이 해결되었습니다.

하지만 blocking 함수를 따로 작성해줘야 하고 콜백 함수가 불리지 않았을 경우의 처리도 필요하다는 등 번거로운 점들도 있습니다.

Coroutine 사용 여부 등 현재 프로젝트 구성에 따라서 다른 방식으로 callback function을 대체하는 것이 더 좋은 방법일 수 있습니다. 

 

결론

소개해드린 세 항목을 정리해 보겠습니다.

  1. Assumptions vs Assertions –  assumption과 assertion을 구분해서 readability와 실패 메시지 개선
  2. 유용한 실패 메시지 – assertion library를 사용해서 readability와 실패 메시지 개선
  3. 콜백 함수에서 검증하지 않기 – blocking 호출로 변환해 readability, test isolation 보장 등을 개선

테스트를 개선하면서 느낀 것은, 테스트의 readability가 정말 중요하다는 것입니다.

코드 리뷰 시에도 로직을 빠르게 파악할 수 있고, 테스트가 깨졌을 때도 원인을 빠르게 파악할 수 있는 등 테스트의 퀄리티에 영향을 많이 주는 요소라고 느꼈습니다.

위의 세 가지 개선점도 직간접적으로 readability을 개선해주었고 테스트 작성에 도움이 많이 되었습니다.

 

이 글을 쓰는 데 제일 도움이 많이 되었던 블로그를 소개해 드립니다.

이 글이 여러분들의 테스트도 개선하는 데 도움이 되기를 바라며 글을 마칩니다.

Categories: 엔지니어링