깨알 개념/Android

[Android] Unit Testing - Retrofit

interfacer_han 2024. 7. 8. 12:56

#1 이전 글

#1-1 Unit Testing 개요

 

[Android] Unit Testing - 개요와 환경 설정

#1 안드로이드 앱 테스트#1-1 안드로이드 앱 테스트의 종류먼저, 여기에 있는 구글 공식 문서에서 안드로이드 앱 테스트에 대한 개요를 읽으면 좋다. 해당 구글 공식 문서에서 복사해온 위의 그림

kenel.tistory.com

위 링크에 있는 이전 게시글에 이어서, 실제 안드로이드 프로젝트를 만들어 Retrofit의 Unit Testing을 수행해본다. 본 게시글을 읽기 전에 [Android] Unit Testing - 기초를 보고 오면 이해에 도움이 된다.
 

#1-2 환경 설정 (build.gradle 등)

이전 게시글의 #3을 토대로 본 게시글에 나오는 안드로이드 프로젝트의 Gradle, AGP, JDK의 버전 설정 및 build.gradle 설정을 진행한다. 이전 게시글의 build.gradle과 달리, 본 게시글에서는 Unit Testing에 사용되지 않는 build.gradle의 plugins { ... }, buildFeatures { ... }, dependencies { ... }의 일부 요소를 제거했다. 이는  코드 다이어트를 위한 개인적인 제거이기 때문에, 이 글은 보는 사람은 제거 없이 그냥 복사 및 붙여넣기해도 된다.
 

#2 Unit Testing을 적용할 샘플 앱

#2-1 원본 앱

 

[Android] Retrofit - 기초

#1 이전 글 [Android] Retrofit - 배경과 구조#1 Restrofit의 배경#1-1 REST API REST API (REpresentational State Transfer Application Programming Interface)#1 무엇(What)에 대한 API인가?#1-1 개요REST(REpresentational State Transfer) 또는

kenel.tistory.com

이 게시글의 완성된 앱을 가져와서 테스트를 수행한다. 해당 앱을 가져와서 #2-1에 맞게 환경 설정을 수정했다. 전체 소스 코드는 본 게시글의 #5에 있다.
 

#2-2 MainActivity 일부 수정해서 album_reponse.json 얻기

...

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        ...

        val responseLiveData: LiveData<Response<Albums>> = liveData {
            val response1 = retrofitService.getAlbums()
            val response2 = retrofitService.getSortedAlbums(7)

            logResponseBody(response1)

            emit(response1)
        }

        responseLiveData.observe(this, Observer {
            ...
        })
    }

    private fun logResponseBody(response: Response<Albums>) {
        val responseBody = response.body()
        val gson = GsonBuilder().setPrettyPrinting().create()
        val jsonFormatted : String = gson.toJson(responseBody)

        Log.i("interfacer_han", "$jsonFormatted")
    }

    private fun appendListOnTextView(textView: TextView, list: Response<Albums>) {
        ...
    }
}

해당 앱의 소스 코드 중 MainActivity의 LiveDataBuilder를 일부 수정한다. 바로, logResponseBody() 함수를 추가하는 것이다. 그리고 앱을 한 번 실행해서 얻어진 Log 메시지를 복사한다. 그리고 하나의 파일로 만들자. 해당 파일의 이름은 album_response.json으로 짓는다. 이 작업은 Mock 서버가 응답할 Response를 하드 코딩하려는 목적이다.
 
album_response.json

더보기
[
  {
    "id": 1,
    "title": "quidem molestiae enim",
    "userId": 1
  },
  {
    "id": 2,
    "title": "sunt qui excepturi placeat culpa",
    "userId": 1
  },
  {
    "id": 3,
    "title": "omnis laborum odio",
    "userId": 1
  },
  {
    "id": 4,
    "title": "non esse culpa molestiae omnis sed optio",
    "userId": 1
  },
  {
    "id": 5,
    "title": "eaque aut omnis a",
    "userId": 1
  },
  {
    "id": 6,
    "title": "natus impedit quibusdam illo est",
    "userId": 1
  },
  {
    "id": 7,
    "title": "quibusdam autem aliquid et et quia",
    "userId": 1
  },
  {
    "id": 8,
    "title": "qui fuga est a eum",
    "userId": 1
  },
  {
    "id": 9,
    "title": "saepe unde necessitatibus rem",
    "userId": 1
  },
  {
    "id": 10,
    "title": "distinctio laborum qui",
    "userId": 1
  },
  {
    "id": 11,
    "title": "quam nostrum impedit mollitia quod et dolor",
    "userId": 2
  },
  {
    "id": 12,
    "title": "consequatur autem doloribus natus consectetur",
    "userId": 2
  },
  {
    "id": 13,
    "title": "ab rerum non rerum consequatur ut ea unde",
    "userId": 2
  },
  {
    "id": 14,
    "title": "ducimus molestias eos animi atque nihil",
    "userId": 2
  },
  {
    "id": 15,
    "title": "ut pariatur rerum ipsum natus repellendus praesentium",
    "userId": 2
  },
  {
    "id": 16,
    "title": "voluptatem aut maxime inventore autem magnam atque repellat",
    "userId": 2
  },
  {
    "id": 17,
    "title": "aut minima voluptatem ut velit",
    "userId": 2
  },
  {
    "id": 18,
    "title": "nesciunt quia et doloremque",
    "userId": 2
  },
  {
    "id": 19,
    "title": "velit pariatur quaerat similique libero omnis quia",
    "userId": 2
  },
  {
    "id": 20,
    "title": "voluptas rerum iure ut enim",
    "userId": 2
  },
  {
    "id": 21,
    "title": "repudiandae voluptatem optio est consequatur rem in temporibus et",
    "userId": 3
  },
  {
    "id": 22,
    "title": "et rem non provident vel ut",
    "userId": 3
  },
  {
    "id": 23,
    "title": "incidunt quisquam hic adipisci sequi",
    "userId": 3
  },
  {
    "id": 24,
    "title": "dolores ut et facere placeat",
    "userId": 3
  },
  {
    "id": 25,
    "title": "vero maxime id possimus sunt neque et consequatur",
    "userId": 3
  },
  {
    "id": 26,
    "title": "quibusdam saepe ipsa vel harum",
    "userId": 3
  },
  {
    "id": 27,
    "title": "id non nostrum expedita",
    "userId": 3
  },
  {
    "id": 28,
    "title": "omnis neque exercitationem sed dolor atque maxime aut cum",
    "userId": 3
  },
  {
    "id": 29,
    "title": "inventore ut quasi magnam itaque est fugit",
    "userId": 3
  },
  {
    "id": 30,
    "title": "tempora assumenda et similique odit distinctio error",
    "userId": 3
  },
  {
    "id": 31,
    "title": "adipisci laborum fuga laboriosam",
    "userId": 4
  },
  {
    "id": 32,
    "title": "reiciendis dolores a ut qui debitis non quo labore",
    "userId": 4
  },
  {
    "id": 33,
    "title": "iste eos nostrum",
    "userId": 4
  },
  {
    "id": 34,
    "title": "cumque voluptatibus rerum architecto blanditiis",
    "userId": 4
  },
  {
    "id": 35,
    "title": "et impedit nisi quae magni necessitatibus sed aut pariatur",
    "userId": 4
  },
  {
    "id": 36,
    "title": "nihil cupiditate voluptate neque",
    "userId": 4
  },
  {
    "id": 37,
    "title": "est placeat dicta ut nisi rerum iste",
    "userId": 4
  },
  {
    "id": 38,
    "title": "unde a sequi id",
    "userId": 4
  },
  {
    "id": 39,
    "title": "ratione porro illum labore eum aperiam sed",
    "userId": 4
  },
  {
    "id": 40,
    "title": "voluptas neque et sint aut quo odit",
    "userId": 4
  },
  {
    "id": 41,
    "title": "ea voluptates maiores eos accusantium officiis tempore mollitia consequatur",
    "userId": 5
  },
  {
    "id": 42,
    "title": "tenetur explicabo ea",
    "userId": 5
  },
  {
    "id": 43,
    "title": "aperiam doloremque nihil",
    "userId": 5
  },
  {
    "id": 44,
    "title": "sapiente cum numquam officia consequatur vel natus quos suscipit",
    "userId": 5
  },
  {
    "id": 45,
    "title": "tenetur quos ea unde est enim corrupti qui",
    "userId": 5
  },
  {
    "id": 46,
    "title": "molestiae voluptate non",
    "userId": 5
  },
  {
    "id": 47,
    "title": "temporibus molestiae aut",
    "userId": 5
  },
  {
    "id": 48,
    "title": "modi consequatur culpa aut quam soluta alias perspiciatis laudantium",
    "userId": 5
  },
  {
    "id": 49,
    "title": "ut aut vero repudiandae voluptas ullam voluptas at consequatur",
    "userId": 5
  },
  {
    "id": 50,
    "title": "sed qui sed quas sit ducimus dolor",
    "userId": 5
  },
  {
    "id": 51,
    "title": "odit laboriosam sint quia cupiditate animi quis",
    "userId": 6
  },
  {
    "id": 52,
    "title": "necessitatibus quas et sunt at voluptatem",
    "userId": 6
  },
  {
    "id": 53,
    "title": "est vel sequi voluptatem nemo quam molestiae modi enim",
    "userId": 6
  },
  {
    "id": 54,
    "title": "aut non illo amet perferendis",
    "userId": 6
  },
  {
    "id": 55,
    "title": "qui culpa itaque omnis in nesciunt architecto error",
    "userId": 6
  },
  {
    "id": 56,
    "title": "omnis qui maiores tempora officiis omnis rerum sed repellat",
    "userId": 6
  },
  {
    "id": 57,
    "title": "libero excepturi voluptatem est architecto quae voluptatum officia tempora",
    "userId": 6
  },
  {
    "id": 58,
    "title": "nulla illo consequatur aspernatur veritatis aut error delectus et",
    "userId": 6
  },
  {
    "id": 59,
    "title": "eligendi similique provident nihil",
    "userId": 6
  },
  {
    "id": 60,
    "title": "omnis mollitia sunt aliquid eum consequatur fugit minus laudantium",
    "userId": 6
  },
  {
    "id": 61,
    "title": "delectus iusto et",
    "userId": 7
  },
  {
    "id": 62,
    "title": "eos ea non recusandae iste ut quasi",
    "userId": 7
  },
  {
    "id": 63,
    "title": "velit est quam",
    "userId": 7
  },
  {
    "id": 64,
    "title": "autem voluptatem amet iure quae",
    "userId": 7
  },
  {
    "id": 65,
    "title": "voluptates delectus iure iste qui",
    "userId": 7
  },
  {
    "id": 66,
    "title": "velit sed quia dolor dolores delectus",
    "userId": 7
  },
  {
    "id": 67,
    "title": "ad voluptas nostrum et nihil",
    "userId": 7
  },
  {
    "id": 68,
    "title": "qui quasi nihil aut voluptatum sit dolore minima",
    "userId": 7
  },
  {
    "id": 69,
    "title": "qui aut est",
    "userId": 7
  },
  {
    "id": 70,
    "title": "et deleniti unde",
    "userId": 7
  },
  {
    "id": 71,
    "title": "et vel corporis",
    "userId": 8
  },
  {
    "id": 72,
    "title": "unde exercitationem ut",
    "userId": 8
  },
  {
    "id": 73,
    "title": "quos omnis officia",
    "userId": 8
  },
  {
    "id": 74,
    "title": "quia est eius vitae dolor",
    "userId": 8
  },
  {
    "id": 75,
    "title": "aut quia expedita non",
    "userId": 8
  },
  {
    "id": 76,
    "title": "dolorem magnam facere itaque ut reprehenderit tenetur corrupti",
    "userId": 8
  },
  {
    "id": 77,
    "title": "cupiditate sapiente maiores iusto ducimus cum excepturi veritatis quia",
    "userId": 8
  },
  {
    "id": 78,
    "title": "est minima eius possimus ea ratione velit et",
    "userId": 8
  },
  {
    "id": 79,
    "title": "ipsa quae voluptas natus ut suscipit soluta quia quidem",
    "userId": 8
  },
  {
    "id": 80,
    "title": "id nihil reprehenderit",
    "userId": 8
  },
  {
    "id": 81,
    "title": "quibusdam sapiente et",
    "userId": 9
  },
  {
    "id": 82,
    "title": "recusandae consequatur vel amet unde",
    "userId": 9
  },
  {
    "id": 83,
    "title": "aperiam odio fugiat",
    "userId": 9
  },
  {
    "id": 84,
    "title": "est et at eos expedita",
    "userId": 9
  },
  {
    "id": 85,
    "title": "qui voluptatem consequatur aut ab quis temporibus praesentium",
    "userId": 9
  },
  {
    "id": 86,
    "title": "eligendi mollitia alias aspernatur vel ut iusto",
    "userId": 9
  },
  {
    "id": 87,
    "title": "aut aut architecto",
    "userId": 9
  },
  {
    "id": 88,
    "title": "quas perspiciatis optio",
    "userId": 9
  },
  {
    "id": 89,
    "title": "sit optio id voluptatem est eum et",
    "userId": 9
  },
  {
    "id": 90,
    "title": "est vel dignissimos",
    "userId": 9
  },
  {
    "id": 91,
    "title": "repellendus praesentium debitis officiis",
    "userId": 10
  },
  {
    "id": 92,
    "title": "incidunt et et eligendi assumenda soluta quia recusandae",
    "userId": 10
  },
  {
    "id": 93,
    "title": "nisi qui dolores perspiciatis",
    "userId": 10
  },
  {
    "id": 94,
    "title": "quisquam a dolores et earum vitae",
    "userId": 10
  },
  {
    "id": 95,
    "title": "consectetur vel rerum qui aperiam modi eos aspernatur ipsa",
    "userId": 10
  },
  {
    "id": 96,
    "title": "unde et ut molestiae est molestias voluptatem sint",
    "userId": 10
  },
  {
    "id": 97,
    "title": "est quod aut",
    "userId": 10
  },
  {
    "id": 98,
    "title": "omnis quia possimus nesciunt deleniti assumenda sed autem",
    "userId": 10
  },
  {
    "id": 99,
    "title": "consectetur ut id impedit dolores sit ad ex aut",
    "userId": 10
  },
  {
    "id": 100,
    "title": "enim repellat iste",
    "userId": 10
  }
]

 

#2-3 resources 디렉토리 만들기 및 album_response.json 추가

왼쪽 사이드바에서 보기 모드를 [Project]로 놓고, 모듈_이름/src/main 디렉토리 아래에 resources 라는 디렉토리(폴더)를 만든다. 그리고 방금 만든 album_response.json을 위치시킨다.
 

#3 Unit Testing

#3-1 테스트 클래스 생성

내가 테스트할 클래스는 AlbumService다. 이는 인터페이스이기에 이 게시글의 #3-1에서 진행한 것처럼 테스트 클래스 생성을 할 수는 없다 ([Generate...] 메뉴 다음으로 [Test...] 메뉴가 안 뜸). 하지만, 그냥 직접 수동으로 테스트 클래스를 만들면 그만이다. 안드로이드 에뮬레이터에 의존없이 돌아가는 MockWebServer 라이브러리를 사용하기에 때문에 Local unit test로 진행한다. 따라서, test 디렉토리에 테스트 클래스를 생성한다.
 

#3-2 테스트 클래스의 내용 작성

import com.google.common.truth.Truth
import kotlinx.coroutines.runBlocking
import okhttp3.mockwebserver.MockResponse
import okhttp3.mockwebserver.MockWebServer
import okio.buffer
import okio.source
import org.junit.After
import org.junit.Before
import org.junit.Test
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory

class AlbumServiceTest {

    private lateinit var service: AlbumService
    private lateinit var server: MockWebServer

    @Before
    fun setUp() {
        // Dependency (Test double) 초기화
        server = MockWebServer()

        // Dependent 초기화
        service = Retrofit.Builder()
            .baseUrl(server.url("")) // baseUrl (Mock이라서 baseUrl을 굳이 제대로 넣을 필요 없다. 뭘 넣어도 하드 코딩된 response를 내뱉을 것이다.)
            .addConverterFactory(GsonConverterFactory.create())
            .build()
            .create(AlbumService::class.java)
    }

    @After
    fun tearDown() {
        server.shutdown()
    }

    /* Server에 하드코딩된 JSON 파일 넣기.
     * server.enqueue(mockResponse)된 순간부터,
     * Server에 무슨 요청을 하든 mockResponse를 응답해줌.
     * 물론 Queue이므로 First In First Out이고,
     * n개의 응답이 필요하다면 server.enqueue()를 n번해주어야 한다.
     */
    private fun enqueueMockResponse(fileName: String) {
        val inputStream = javaClass.classLoader!!.getResourceAsStream(fileName)
        val source = inputStream.source().buffer()
        val mockResponse = MockResponse()
        mockResponse.setBody(source.readString(Charsets.UTF_8))
        server.enqueue(mockResponse)
    }

    // 올바른 request를 보내는 지, 응답이 오긴하는 지 확인
    @Test
    fun getAlbums_sentRequest_receivedExpected() { // subjectUnderTest_actionOrInput_resultState 규칙으로 함수 명명함
        runBlocking {
            // Mock 서버에 하드코딩된 데이터 넣고, 곧바로 request 및 response 받기
            enqueueMockResponse("album_response.json")
            val responseBody = service.getAlbums().body()

            /* 서버가 받은 HTTP request 가져오고, 올바른 request였는지 확인.
             * @Query 어노테이션이 존재한다면,
             * 예를 들어 "/albums?userId=7"와 같이 적어야 한다.
             */
            val request = server.takeRequest()
            Truth.assertThat(request.path).isEqualTo(/* expected = */ "/albums")

            // response이 null이 아님을 확인 (Mock 서버가 뭐라도 잘 내뱉는 지 확인)
            Truth.assertThat(responseBody).isNotNull()
        }
    }

    // Albums의 갯수 확인
    @Test
    fun getAlbums_receivedResponse_correctElementSize() {
        runBlocking {
            // Mock 서버에 하드코딩된 데이터 넣고, 곧바로 request 및 response 받기
            enqueueMockResponse("album_response.json")
            val responseBody = service.getAlbums().body()

            Truth.assertThat(responseBody!!.size).isEqualTo(/* expected = */ 100)
        }
    }

    // 모든 Albums를 다 확인하는 건 시간도 오래 걸리고 필요성도 크지 않으니, 첫번째 앨범만 확인
    @Test
    fun getAlbums_receivedResponse_correctContent() {
        runBlocking {
            // Mock 서버에 하드코딩된 데이터 넣고, 곧바로 request 및 response 받기
            enqueueMockResponse("album_response.json")
            val responseBody = service.getAlbums().body()
            val firstAlbum = responseBody!![0]

            Truth.assertThat(firstAlbum.userId).isEqualTo(/* expected = */ 1)
            Truth.assertThat(firstAlbum.id).isEqualTo(/* expected = */ 1)
            Truth.assertThat(firstAlbum.title).isEqualTo(/* expected = */ "quidem molestiae enim")
        }
    }
}

 

#4 작동 확인

 

#5 완성된 앱

 

android-practice/unit-test/TestingRetrofit at master · Kanmanemone/android-practice

Contribute to Kanmanemone/android-practice development by creating an account on GitHub.

github.com