#1 이전 글
#1-1 Unit Testing 개요
위 링크에 있는 이전 게시글에 이어서, 실제 안드로이드 프로젝트를 만들어 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 원본 앱
이 게시글의 완성된 앱을 가져와서 테스트를 수행한다. 해당 앱을 가져와서 #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' 카테고리의 다른 글
[Android] Dagger2 - Hilt로 마이그레이션 (0) | 2024.07.12 |
---|---|
[Android] Data Binding - View Binding (0) | 2024.07.10 |
[Android] Unit Testing - Room과 LiveData (0) | 2024.07.08 |
[Android] Unit Testing - ViewModel (0) | 2024.07.08 |
[Android] Unit Testing - 기초 (0) | 2024.07.08 |