Kotest로 해보는 안드로이드 테스트 (하)
728x90
반응형

이미지 출처 : https://ramadandev.medium.com/test-driven-development-for-android-developers-ceb55ae3ea16

 

본 글에서는 RxJava 혹은 RxKotlin의 기본 지식이 필요하다. 그리고 만약 이전 글이 궁금하다면 아래 링크를 접속해보자.

 

 

Kotest로 해보는 안드로이드 테스트 (상)

테스트 코드를 만들어 본 적은 있지만 안드로이드 프로젝트 내에 테스트 코드를 적용해 본 적이 없었다. 그래서 TDD같은 단어는 그림의 떡. 남들이 말하는 유닛 테스트는 "우와 멋있는 말"이라고

devvkkid.tistory.com

 


 

앞에서 Kotest를 사용하기 위한 준비를 마쳤다. 이제 사용을 해 볼것이다. 이미 만들어진 프로젝트가 있다. (해당 링크는 필자의 Github repository이다.) 이 프로젝트에는 REST API 형태로 통신하는 모듈이 들어있다. 이 API를 테스트해 볼 것이다.

 

1. 테스트 클래스 생성

먼저 test 폴더 아래에 단위테스트를 할 테스트 클래스를 만들자. 필자의 경우, ExampleUnitTest라고 지었다.

 

2. 통신할 ApiService 가지고 오기

이미 API와 통신 모듈은 준비가 된 상황. 모듈은 싱글톤으로 되어 있기 때문에 따로 가지고 올 필요는 없고 ApiService만 준비하면 된다. ApiService도 바로 선언해서 사용할 것은 아니기 때문에 lateinit을 사용한다. (늦은 초기화)

private lateinit var apiService: ApiService

 

 

 

3. beforeSpec과 beforeTest

Kotest에는 여러 콜백 메소드를 제공한다. 그 중 TestListener에는 다음과 같은 두가지 Callback도 제공한다.

beforeSpec : 해당 테스트 클래스에서 1회만 실행되는데 여러개의 테스트 중 1번째 테스트가 실행되기 전에 수행된다.
beforeTest : 개별 테스트가 수행하기 직전에 호출된다.

이를 시험해보고 싶으면 아래 코드를 입력해보자. 또 2번의 ApiService는 테스트하기 전에 1회의 초기화를 해야하므로 beforeSpec에서 초기화를 하도록 하자.

class ExampleUnitTest : FunSpec() {
    private lateinit var apiService: ApiService

    // 본 코드 기준으로 FunSpec형 Test를 시작하기 전에 1회만 실행
    override fun beforeSpec(spec: Spec) {
        super.beforeSpec(spec)
        println("beforeSpec #####")
        apiService = ApiClient().getApiService()
    }

    // 개별 test()전마다 1회씩 실행
    override fun beforeTest(testCase: TestCase) {
        super.beforeTest(testCase)
        println("beforeTest >>>>>")
    }
}

 

 

 

4. 딴 짓을 한다.

깔깔깔. 이 유튜브 재밌네!!! 아. 이런 젠X

 

아 아... 이게 아니지. 자~ 돌아가자. (😓머쓱타드)

 

 

 

5. 본격 테스트 코드 작성

Spec 소스들은 (본 코드에서는 FunSpec) init 내부에 테스트를 작성하도록 되어 있다. 다수의 테스트를 진행하더라도 init 내부에 넣기만 하면 된다. 필자는 3가지 API를 테스트를 할 것이고 그 중 조회용 테스트를 작성해보려고 한다.

init {
    test("getUsers API") {
        // hmmm....
    }
}

getUsers API는 RxJava의 Observable을 반환한다. 그리고 test 메소드를 사용하면 Observable을 TestObserver로 변환한다. TestObserver는 이벤트를 감지하고, 연결된 테스트 체인에따라 성공여부를 알려주는 옵저버로 onSubscribe, onNext, onError, onSuccess등을 오버라이드하여 결과값을 테스트하는 용도로 사용할 수 있다.

 

이 상태에서 에러가 없는지(assertNoErrors) 제대로 Complete가 되었는지(assertComplete) 확인해보자.

val testObserver = apiService.getUsers()
    .test()
    
testObserver.assertNoErrors()
    .assertComplete()

// Result
java.lang.AssertionError: Not completed (latch = 1, values = 0, errors = 0, completions = 0)

 

이렇게 Error가 출력된 이유는 API call하는 과정이 메인스레드가 아닌 워커스레드(백그라운드)를 통해 수행되기 때문이다. 이 워커스레드가 완전히 수행을 끝내고 종료하기 전에 테스트 프로그램이 종료가 되면서 Not completed가 출력된 것이다. TestObserver 클래스의 awaitDone() 함수를 사용하면 비동기 코드를 테스트 할 수 있다. awaitDone()에서 설정된 시간내에 처리가 완료되지 않으면 TestObserver는 취소(dispose)된다. 만약 처리가 완료되면 awaitDone의 남은 시간만큼 기다리지 않고 바로 다음 처리로 넘어간다.

await 함수 : onNext된 값이 모두 발행될때까지 기다림
awaitDone 함수 : interval() 함수처럼 비동기로 동작하는 Observable 코드를 검증
test("getUsers API") {
    // awaitDone() 함수가 편리한 이유 :
    // test() 함수가 실행되는 스레드에서 onComplete() 함수를 호출할 때까지 기다려주기 때문
    val testObserver = apiService.getUsers()
        .test()
        .awaitDone(10000, TimeUnit.MILLISECONDS)

    testObserver.assertNoErrors()
        .assertComplete()
}

 

 

 

6. 남은 API도 테스트하기

같은 방식으로 남은 2개의 API도 테스트를 하고 테스트 클래스를 Run하면 아래와 같은 결과를 얻을 수 있다.

 

 

 

Kotest를 어떻게 쓰는지 어느 정도 감을 잡은 상태다. 이제는 HTTP 통신 테스트를 해보고 싶다. 보통 클라이언트 개발자는 서버 개발자가 API를 준비해줘야 연결과 응답을 테스트해볼 수 있다. 하지만 API 설계는 서버개발자와 클라이언트 개발자가 같이 하기에 미리 모델을 만들수는 있다. 모델이 준비는 되었는데 서버가 준비되지 않아서 Unit test를 할 수 없다면? 이럴 때 유용한 것이 OkHttp에서 제공하는 MockWebServer이다.

 

 

 

가상의 웹서버를 만들어보자

mock [mɑːk]
1. (특히 흉내를 내며) 놀리다
2. 무시하다
3. 거짓된, 가짜의

MockWebServer는 놀려도 되는 웹서버를 말한다. ... 농담이다. 세번째 뜻인 가짜의 웹서버를 뜻하고 마치 실제 서버가 응답을 주는 것처럼 보이기에 테스트에 사용하기에 적합하다. 실습을 해보기 위해선 build.gradle에 먼저 MockWebServer를 추가해야 한다.

// build.gradle(:app)
dependencies {
    // 일부 생략
    
    testImplementation "com.squareup.okhttp3:mockwebserver:4.9.0"
}

 

 

그 다음부터는 위의 테스트 방식과 비슷하다. 테스트 클래스를 만들고 beforeSpec과 afterSpec을 오버라이드 한다. MockWebServer를 사용할 것이기에 선언 후 beforeSpec에서 초기화(Mock 서버 초기화)를 한다. 서버를 열었던 것처럼 테스트가 끝날 때 닫아주기 위한 shutdown도 해준다.

class MockWebServerTest : FunSpec() {

    private lateinit var server: MockWebServer

    override fun beforeSpec(spec: Spec) {
        server = MockWebServer()
        server.start()
    }

    override fun afterSpec(spec: Spec) {
        server.shutdown()
    }
}

 

 

이제 테스트를 만들어보자. 서버는 준비되었으니 응답을 받을 HttpClient 모듈을 만들 차례. 완성된 Retrofit 모듈이 API 인터페이스를 받아서 인터페이스 구현체를 만든다.

init {
    test("Test MockServer") {
        // baseUrl : http://localhost:port번호/v1/
        val baseUrl = server.url("/v1/")

        val client = OkHttpClient.Builder().connectTimeout(1, TimeUnit.MINUTES)
            .writeTimeout(1, TimeUnit.MINUTES)
            .readTimeout(1, TimeUnit.MINUTES)
            .build()

        val retrofit = Retrofit.Builder()
            .baseUrl(baseUrl)
            .addConverterFactory(GsonConverterFactory.create())
            .addCallAdapterFactory(RxJava3CallAdapterFactory.create())
            .client(client)
            .build()

        // 인터페이스 구현
        val service = retrofit.create(ApiService::class.java)
}

 

 

이제 모의 응답을 만들 차례다. Mock 서버가 이 모의 응답을 받아서 준비하고 클라이언트에서 요청을 보내면 준비된 모의 응답을 출력한다. 마지막으로 test 메서드를 통해 반환된 TestObserver로 원하는 테스트를 수행한다.

test("Test MockServer") {

    // 인터페이스 구현
    val service = retrofit.create(ApiService::class.java)

    ////////////////////
    // 일부 코드 생략
    ////////////////////

    val response = MockResponse()
        .addHeader("Content-Type", "application/json; charset=utf-8")
        .setResponseCode(200)
        .setBody(Gson().toJson(res))

    server.enqueue(response)

    service.getUsers()
        .test()
        .awaitDone(2000, TimeUnit.MILLISECONDS)
        // 에러가 없는지 확인 테스트
        .assertNoErrors()
        // 원하는 값을 가지고 있는지 테스트
        .assertValue {
            it.data[0].firstName == "Test1"
        }
}

 

 

이렇게해서 HTTP 통신도 Unit test로 확인할 수 있다. 이 전체 테스트 코드를 알고 싶다면 필자의 Github를 참고하길 바란다.

 


 

Kotest와 MockWebServer는 내가 원하는 맛(?)의 자료를 찾기 쉽지 않았다. 그래서 더욱 많은 자료를 찾아보고 나름 쉬운 정리를 해보려고 노력했다. 하지만 여전히 충분한 설명이 부족할 것 같아서 내가 본 핵심 참고자료를 아래에 기재해둔다. 많은 분들의 도움이 되기를.

 

 

Kotest 참고 자료
- How to write beforeEach and beforeClass in kotlintest
- MVP & Rx unit test
- RxJava 테스팅과 Flowable-3(비동기 테스트)
- How to unit test your RxJava code in Kotlin
- TestObserver을 이용한 Retrofit 테스트
- Retrofit + Rx + JUnit 으로 api test 하기

MockWebServer 참고자료
- [번역] 단위테스트를 위한 안드로이드 모의 서버 (추천👍)
- Github : elye/demo_android_mock_web_service (추천👍)
- MockWebServer를 이용해 외부API 호출 메서드 테스트하기
- Mock Responses with OkHttp & Retrofit
- Unit Test Retrofit API calls with MockWebServer
- Integrating Retrofit with RxJava
- Testing your Network logic

언젠가 차분히 보고 싶은 자료
- 테스트 코드, 안드로이드에서는 어떻게 작성해야 할까?
- Best Practices for Unit Testing in Kotlin

본 포스팅의 실습 코드가 궁금하신 분들은 필자의 Github를 참고해주세요.

 

 

뿌듯하구만 (으쓱)

728x90
반응형