본문 바로가기

Android/TDD(Test Driven Development, 테스트 주도 개발)

Test Double(테스트 더블) - TDD(테스트 주도 개발)

반응형

2021.12.10 - [Android+Kotlin+Figma] - Unit Test - TDD(Test Driven Development, 테스트 주도 개발)

 

Unit Test - TDD(Test Driven Development, 테스트 주도 개발)

사담 안드로이드 개발의 기초적인 부분을 훑으면서, 어느 부분에 대해 테스트를 작성해야 하는지 구분이 모호하다고 생각했습니다. 이를테면 "버튼을 누르면 다음 화면으로 넘어가는 기능"은

roomedia.tistory.com

Test Double의 필요성

이전 포스팅에서 단일 클래스를 대상으로 테스트 케이스를 작성하여 Unit Testing(유닛 테스트)을 진행하는 방법에 대해 살펴보았습니다. 이 경우 가장 조심해야 하는 부분은, 적절한 대표군을 선정하여 가능한 모든 입력을 테스트 하는 것이었습니다. 테스트 케이스를 선정한 이후에는 각각의 케이스에서 예상되는 반환값이나 기대되는 기능을 만족하도록 최소한의 코드로 기능을 개발하기만 하면 되었습니다.

그러나 객체 지향 기반으로 프로그램을 작성하다보면 서로 다른 오브젝트가 상호작용하는 경우가 발생하는데요. 이때 시스템에 영향을 줄 수 있는, 실제 구현체를 사용하게 되면 문제가 발생할 수 있고, 이러한 경우에는 Test Double(테스트 더블) 방식으로 테스트를 진행할 수 있습니다. Test Double이 필요한 예시를 들어보겠습니다.

로그인 기능을 제공하는 LoginUseCaseSync 클래스가 다음과 같이 존재할 때, 이를 Unit Testing 하기 위해서는 먼저 파라미터로 제공될 LoginHttpEndpointSync, AuthTokenCache, EventBusPoster 객체를 초기화 하는 과정이 필요합니다. 다른 말로, LoginUseCaseSync는 파라미터에 의존성을 갖고 있습니다. 그러나, 테스트 과정에서 실제 객체를 사용한다면 테스트 속도가 저하될 뿐만 아니라 환경에 따라서도 영향을 받을 수 있습니다. 테스트가 SUCCESS를 반환하는 케이스를 진행하는 컴퓨터가 오프라인이라면 문제가 발생할 수 있겠죠?

Test Double

UnitA를 테스트하고 싶지만 해당 유닛이 UnitB에 의존성을 갖고, UnitB가 테스트에 영향을 미쳐 테스트의 신뢰도를 떨어트릴 때 필요한 개념이 Test Double입니다. Test Double이란, 테스트에 영향을 미치지 않는 UnitB를 구현하는 방식을 말하며, 이는 세 가지 방법으로 이루어질 수 있습니다.

  • Fake: 실제 유닛을 테스트에 적합하게 만든 대체 유닛입니다. 예를 들어, 데이터베이스 라이브러리는 테스트를 위해 in-memory 데이터베이스를 제공하는 데요. in-memory 데이터베이스는 실제 유닛의 모든 기능을 동일하게 수행하지만 데이터를 디스크에 보존하지 않고 메모리에만 저장하였다 삭제하기 때문에 실제 시스템에 영향을 주지 않습니다.
  • Stub: 미리 정의된, 테스트가 필요로 하는 데이터를 반환하는 유닛입니다. 네트워크 요청을 예로 들자면, 실제 네트워크 요청 시 반환하는 값을 네트워크 요청 없이, 직접 반환하는 클래스로, 네트워크 요청이 이루어지지 않기 때문에 테스트 속도도 빠르며 컴퓨터가 오프라인이어도 상관없이 테스트를 진행할 수 있습니다.
  • Mock: 테스트 하는 동안 상호작용을 기록할 수 있으며, 이를 검증할 수 있습니다. 특정 유닛의 특정 메소드가 호출되는지 알고 싶을 때 해당 유닛의 Mock을 생성하면 이를 검증할 수 있습니다.

테스트 케이스

다음과 같이 의존성을 갖는 인터페이스 3개를 모두 구현하면 systemUnderTest를 초기화할 수 있습니다. LoginUseCaseSync의 동작을 세밀하게 살펴보며 테스트 케이스를 선정해볼까요? LoginUseCaseSync는 3개의 파라미터를 갖고 하나의 메소드 loginSync를 수행하는 클래스였죠. usernamepassword를 제공하면 LoginHttpEndpointSync를 거쳐 이를 서버(Endpoint)에 전달하고 서버 응답을 받습니다. 서버 응답에는 토큰이 포함되어 있고, 이를 AuthTokenCache를 활용하여 저장합니다. 로그인에 성공하면 EventBusPoster를 통해 LoggedInEvent가 전달됩니다.

이를 종합해보면 다음과 같은 상황을 테스트해야 합니다.

  • username, password가 LoginHttpEndpointSync에 전달되는지 확인한다.
  • 로그인 성공 시 AuthTokenCache에 토큰이 전달되는지 확인한다.
  • 로그인 실패 시 AuthTokenCache에 저장된 토큰은 변경되지 않는지 확인한다.
  • 로그인 성공 시 EventBusPosterLoggedInEvent가 전달되는지 확인한다.
  • 로그인 실패 시 EventBusPoster에는 어떤 이벤트도 전달되지 않는지 확인한다.
  • 로그인 시 loginSync의 반환값을 확인한다.

하나씩 케이스를 살펴보도록 합시다.

username, password가 LoginHttpEndpointSync에 전달되는지 확인한다.

loginSync 시 전달한 username, passwordLoginHttpEndpointSyncTestDouble에 전달되는지 확인하기 위해 해당 클래스의 멤버변수로 username, password를 선언하고, 이를 검증해보도록 하겠습니다.

이런 식으로 변수를 활용하여 간접적으로 호출을 확인하는 Mock을 생성하게 되는데요. 이후 수업에서는 메소드 호출을 직접 확인할 수 있는 라이브러리 Mockito에 대해 배우기 때문에 이런 방법이 있다~ 정도로 알고 있으면 될 것 같습니다. 위 테스트를 만족하는 최소한의 코드는 다음이 되겠네요.

로그인 성공 시 AuthTokenCache에 토큰이 전달되는지 확인한다. 실패 시 토큰은 변경되지 않는다.

loginhttpendpointsynctestdouble 상태 스샷

로그인에 성공할 시, LoginHttpEndpointSyncTestDoubleloginSync 반환값은 EndpointResult(SUCCESS, token)입니다. 이때 AuthTokenCache 구현체를 사용하여 해당 토큰 값을 저장합니다. LoginHttpEndpointSyncTestDouble 객체는 플래그로 상태를 지정하여 미리 정의된 결과값을 반환하니 Stub 유형으로 볼 수 있고, AuthTokenCacheTestDouble 객체는 토큰을 테스트 용도로 메모리에만 저장하기 때문에 Fake 유형이라고 볼 수 있겠네요.

AuthTokenCache스샷

AuthTokenTest스샷

LoginHttpEndpointSync는 성공, 인증 에러, 서버 에러, 일반적인 에러 네 가지 상태를 가지며, 성공 시에만 토큰을 저장하고 이외에는 초기값을 갖습니다. AUTH_TOKEN, NON_INITIALIZED_AUTH_TOKEN은 임의로 정해놓은 상수입니다.

로그인 성공 시 EventBusPosterLoggedInEvent가 전달되는지 확인한다. 실패 시 EventBusPoster의 메소드는 호출되지 않는다.

로그인에 성공할 시, EventBusPosterpostEventLoggedInEvent가 전달됩니다. 실패 시 postEvent는 호출되어선 안 됩니다. EventBusPosterTestDouble은 이벤트를 전달받되 실제 브로드캐스트 하진 않기 때문에 Fake 유형이며, interactionCount를 통해 postEvent가 호출되었는지 간접 확인하기 때문에 Mock이라고 할 수 있습니다.

eventPoster스샷

테스트 스샷

로그인 시 loginSync의 반환값을 확인한다.

마지막으로 로그인 반환값을 확인합니다.

테스트 스샷

네트워크가 발생하는 상황을 설정하기 위해 LoginHttpEndpointSyncTestDouble에 플래그를 하나 추가하였습니다.

최소한의 코드로 기능 개발

테스트를 모두 만족하도록 작성된 최소한의 코드입니다. 14개의 테스트를 모두 통과하는 모습을 볼 수 있습니다.

요구사항이 복잡해질수록 TestDouble이 많이 사용되는만큼, 개념을 잘 잡아놓으면 좋을 것 같습니다.

반응형