1. 시작하기 전에
이전 Codelab에서는 Retrofit을 사용하여 네트워크 요청을 만드는 방법을 배웠습니다. 이 Codelab에서는 네트워크 코드의 단위 테스트 작성 방법을 알아봅니다.
기본 요건
- Android 스튜디오에서 테스트 디렉터리를 만들어 보았습니다.
- Android 스튜디오에서 단위 테스트와 계측 테스트를 작성해 보았습니다.
- Android 프로젝트에 Gradle 종속 항목을 추가해 보았습니다.
학습 내용
- 테스트용 리소스 저장 방법
- 테스트용 API 응답의 모의 처리 방법
- Retrofit API 서비스 테스트 방법
필요한 항목
- Android 스튜디오가 설치된 컴퓨터
- 화성 사진 앱의 솔루션 코드
이 Codelab의 시작 코드 다운로드
이 Codelab에서는 이전 솔루션 코드의 화성 사진 앱에 계측 테스트를 추가합니다.
이 Codelab의 코드를 가져와서 Android 스튜디오에서 열려면 다음을 실행합니다.
코드 가져오기
- 제공된 URL을 클릭합니다. 브라우저에서 프로젝트의 GitHub 페이지가 열립니다.
- 프로젝트의 GitHub 페이지에서 Code 버튼을 클릭하여 대화상자를 엽니다.
- 대화상자에서 Download ZIP 버튼을 클릭하여 컴퓨터에 프로젝트를 저장합니다. 다운로드가 완료될 때까지 기다립니다.
- 컴퓨터에서 파일을 찾습니다(예: Downloads 폴더).
- ZIP 파일을 더블클릭하여 압축을 해제합니다. 프로젝트 파일이 포함된 새 폴더가 만들어집니다.
Android 스튜디오에서 프로젝트 열기
- Android 스튜디오를 시작합니다.
- Welcome to Android Studio 창에서 Open an existing Android Studio project를 클릭합니다.
참고: Android 스튜디오가 이미 열려 있는 경우 File > New > Import Project 메뉴 옵션을 대신 선택합니다.
- Import Project 대화상자에서 압축 해제된 프로젝트 폴더가 있는 위치로 이동합니다(예: Downloads 폴더).
- 프로젝트 폴더를 더블클릭합니다.
- Android 스튜디오가 프로젝트를 열 때까지 기다립니다.
- Run 버튼 을 클릭하여 앱을 빌드하고 실행합니다. 예상대로 작동하는지 확인합니다.
- Project 도구 창에서 프로젝트 파일을 살펴보고 앱이 설정된 방식을 확인합니다.
2. 스타터 앱 개요
화성 사진 앱은 하나의 화면으로 구성되며 네트워크 요청을 통해 가져온 사진 목록을 표시합니다.
이 시작 코드에는 테스트와 관련하여 약간의 차이가 있습니다. 이 부분은 이 Codelab의 범위에서 벗어나므로 지금은 다루지 않지만, 이를 언급하는 것이 중요합니다.
앱이 API에서 데이터를 가져오는 방식을 테스트할 때는 데이터가 어떻게 표시될지 확인할 수 있도록 항상 자체 데이터를 제공하는 것이 가장 좋습니다. API의 데이터는 변경될 수 있으며, 이로 인해 테스트가 중단되고 불필요한 실패가 발생할 수 있습니다. 또한 실제 네트워크 호출을 사용하면 네트워크 연결이나 네트워크 속도로 인해 실패가 발생하여 테스트에 일관성이 없을 수 있습니다. 따라서 테스트에서 일부 데이터를 생성하여 test/res 디렉터리에 JSON 파일로 저장합니다. 이는 기본 앱 코드의 리소스 디렉터리와 유사한 리소스 디렉터리이지만, 테스트 리소스는 test/res에 저장된다는 점이 다릅니다.
test/res
디렉터리를 추가합니다. 프로젝트 뷰에서src
디렉터리를 마우스 오른쪽 버튼으로 클릭하고 드롭다운 메뉴에서 New -> Directory를 선택합니다.
- 결과 팝업에서 아래로 스크롤하여
test/res
를 선택합니다.
- 프로젝트 뷰에서
test/res
디렉터리를 마우스 오른쪽 버튼으로 클릭하고 New -> File을 선택합니다.
- 파일 이름을
mars_photos.json
으로 지정합니다.
- 다음 코드를
mars_photos.json
파일에 복사합니다.
[
{
"id":"424905",
"img_src":"http://mars.jpl.nasa.gov/msl-raw-images/msss/01000/mcam/1000MR0044631300503690E01_DXXX.jpg"
},
{
"id":"424906",
"img_src":"http://mars.jpl.nasa.gov/msl-raw-images/msss/01000/mcam/1000ML0044631300305227E03_DXXX.jpg"
}
]
- 이 리소스 디렉터리에 있는 파일에 액세스할 수 있으려면 테스트 리소스 디렉터리를 빌드 파일에 '소스' 디렉터리로 명시하여 지정해야 합니다. 다음 줄을 추가합니다.
app/build.gradle
android {
...
sourceSets {
test.resources.srcDirs += 'src/test/res'
}
}
이 코드를 사용하면 테스트에서 파일에 액세스할 때마다 코드에 파일의 전체 경로를 입력하지 않고 리소스 파일에 액세스할 수 있습니다. 파일 경로는 컴퓨터와 운영체제에 따라 달라질 수 있으므로 전체 경로를 입력하는 것은 테스트용으로 좋은 방법이 아닙니다.
- 같은 파일에 다음 종속 항목을 추가하고 Gradle을 동기화합니다.
app/build.gradle
dependencies {
...
testImplementation 'junit:junit:4.12'
testImplementation "androidx.arch.core:core-testing:2.1.0"
testImplementation "com.squareup.okhttp3:mockwebserver:4.9.1"
}
- 이제 일반 테스트 디렉터리를 만듭니다.
- 테스트 디렉터리에
com.example.android.marsphotos
라는 새 패키지를 만듭니다. - 패키지에서
BaseTest.kt
라는 새 Kotlin 파일을 만듭니다.
- 다음 코드를 복사하여
BaseTest.kt
파일에 붙여넣습니다. 이 코드가 익숙하지 않아도 괜찮습니다. 아래 코드에서는 'Mock'라는 용어가 반복적으로 등장하며, 이 용어는 이 Codelab 전반에서 언급됩니다. 이 클래스의enqueue()
함수는 나중에 작성할 테스트에서 사용할 수 있도록 개발자가 만든mars_photos.json
파일의 데이터를 파싱합니다.
BaseTest.kt
import okhttp3.mockwebserver.MockResponse
import okhttp3.mockwebserver.MockWebServer
import okio.buffer
import okio.source
open class BaseTest {
val mockWebServer = MockWebServer()
fun enqueue(fileName: String) {
val inputStream = javaClass.classLoader!!.getResourceAsStream(fileName)
val buffer = inputStream.source().buffer()
mockWebServer.enqueue(
MockResponse()
.setResponseCode(200)
.setBody(buffer.readString(Charsets.UTF_8))
)
}
}
3. 데이터 모의 처리
이 Codelab의 테스트는 모의 데이터에 크게 의존합니다. 테스트와 관련하여 모의란 일부 코드의 반환 값을 시뮬레이션하고 있다는 의미입니다. 이전 Codelab에서는 클래스를 모의 처리했습니다. 또한, 함수를 모의 처리하여 지정한 값을 반환하도록 하거나 API를 모의 처리하여 지정한 데이터 일부를 반환할 수 있습니다. 이렇게 하면 테스트용 코드를 구분할 수 있으므로 테스트에 유용합니다. 이 Codelab에서는 API 응답을 모의 처리하는 방법에 중점을 둡니다. 함수 모의 처리는 별도의 Codelab에서 살펴보겠습니다.
4. 단위 테스트 클래스 만들기
이 테스트에서는 API 서비스를 테스트합니다. 먼저 MarsApiServiceTests.kt라는 새 클래스를 만듭니다.
5. 종속 항목
이 Codelab의 시작 코드는 테스트에 필요한 종속 항목과 함께 제공됩니다. 그런데 아직 다루지 않은 중요한 종속 항목이 있습니다.
testImplementation "com.squareup.okhttp3:mockwebserver:4.9.1"
이 종속 항목을 사용하면 모의 서버를 만들 수 있습니다. 기본적으로 모의 서버는 네트워크 요청을 가로챈 후 라우팅 경로를 변경하여 정의된 모의 데이터를 반환합니다. 모의 처리의 의미에 관해서는 이 Codelab의 후반부에서 알아보겠습니다.
6. 권장사항
테스트는 객체 지향 프로그래밍 언어인 Kotlin으로 작성합니다. 즉, 테스트를 위해 객체 지향 코드를 작성할 수 있습니다. 이 테스트를 위해 작성할 코드 양으로 보아 객체 지향 사례를 사용할 필요는 없습니다. 하지만 개념을 설명하기 위해 객체 지향 사례를 구현하겠습니다.
시작 코드에 이미 단위 테스트 디렉터리가 포함되어 있고 그 디렉터리에 BaseTest
라는 클래스가 있음을 알 수 있습니다. 여러 테스트에 사용할 수 있는 코드가 있다면 open class
를 만들고 이 클래스에서 테스트 클래스를 상속할 수 있습니다. 이 BaseTest 클래스에는 enqueue()
라는 하나의 메서드만 있으며 이 Codelab에서는 테스트 클래스를 하나만 작성할 예정입니다. 자체 테스트를 작성할 때는 객체 지향 프로그래밍을 활용할 수 있다는 점을 기억하세요. 그러나 불필요하게 이를 남용하지 않도록 합니다.
7. 네트워크 요청 테스트 작성
먼저, 테스트 클래스가 BaseTest
에서 상속되는지 확인합니다.
MarsApiService
를 직접 테스트하므로 이 클래스의 인스턴스가 필요합니다. 인스턴스는 앱 코드와 동일한 방식으로 설정하지만, 새 서비스의 URL은 달라집니다.
- 테스트 클래스에서
MarsApiService
를 위해lateinit
변수를 만듭니다.
private lateinit var service: MarsApiService
이제 모든 테스트에 앞서 service
변수를 정의하는 함수가 필요합니다.
setup()
이라는 함수를 만들고@Before
주석을 추가합니다.
@Before
fun setup() {}
이 테스트에서는 네트워크 요청을 위한 URL을 사용하지 않습니다. 이러한 경우, MockWebServer
가 유용합니다.
데이터 모의 처리
BaseTest
클래스에는 mockWebServer,
라는 속성이 있으며 이는 MockWebServer
객체의 인스턴스일 뿐입니다. 이 객체는 네트워크 요청을 가로채지만, 테스트에서 먼저 가로채기를 당할 URL로 네트워크 요청을 전달해야 합니다.
MockWebServer
에는 가로채려고 하는 URL을 지정하는 url()
이라는 함수가 있습니다. 이 테스트에서는 실제 네트워크 요청을 만들려고 하는 것이 아니라, 테스트가 자체적으로 제어하는 데이터를 사용하여 네트워크 코드를 테스트하기 위해 네트워크 요청을 만드는 것처럼 가장한다는 것을 기억하세요. url()
함수는 가짜 URL을 나타내는 문자열을 사용하여 HttpUrl
객체를 반환합니다. setup()
함수에 다음 줄을 작성합니다.
val url = mockWebServer.url("/")
가로채려고 하는 URL 엔드포인트를 설정했고 반환된 HttpUrl
객체를 캡처했습니다.
이 함수 내에서 MarsApiService
및 MarsApiClass
(lazy
변수 제외)와 동일한 방식으로 MarsApiService
인스턴스를 만듭니다. 하지만 아직 기본 URL은 포함하지 마세요. 예를 들면 다음과 같습니다.
service = Retrofit.Builder()
.addConverterFactory(MoshiConverterFactory.create(
Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.build()
))
.build()
.create(MarsApiService::class.java)
이제 기본 URL을 설정할 차례입니다. Retrofit.Builder()
의 함수 체인에 다음 코드를 추가합니다.
.baseUrl(url)
이는 요청을 MockWebServer
로 라우팅하려고 한다고 API 서비스에 지정하는 것입니다.
service = Retrofit.Builder()
.baseUrl(url)
.addConverterFactory(MoshiConverterFactory.create(
Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.build()
))
.build()
.create(MarsApiService::class.java)
다시 말하자면, MockWebServer
의 포인트는 실제 API에 실제 네트워크 요청을 하지 않는 것입니다. 기본 아이디어는 실제 네트워크 요청이 있을 때 API가 실패하면 테스트가 실패하는 것입니다. 실제 API를 사용하여 API 자체를 테스트하므로, Android 프로젝트에서 코드를 테스트하는 데에만 초점이 맞춰져 있습니다.
MockWebServer
는 테스트에서 생성했던 데이터를 반환하는 가짜 API라고 간주할 수 있으며, 따라서 요청이 이루어지기 전에 반환할 항목을 MockWebServer
에 명시적으로 알려야 합니다. 이때, BaseTest
의 enqueue()
함수가 사용됩니다. 이 함수의 코드는 이 Codelab의 범위를 벗어나므로 너무 신경 쓰지 않아도 됩니다. 함수가 테스트 리소스로부터 파일을 가져와서 그 파일을 가짜 API 응답으로 변환한다는 사실만 알면 됩니다.
api_service()
라는 테스트 함수를 만듭니다. 테스트 함수에서 다음과 같이enqueue
메서드를 호출합니다.
enqueue("mars_photos.json")
- 다음으로,
MarsApiService
에서getPhotos()
함수를 직접 호출합니다.getPhotos()
는 정지 함수이며 코루틴 범위에서 호출되어야 합니다. 이렇게 하려면 다음과 같이runBlocking
에서 메서드 호출을 래핑해야 합니다.
runBlocking {
val apiResponse = service.getPhotos()
}
- 이제
getPhotos()
응답이null
이 아닌지 확인해 보겠습니다.runBlocking
내부에apiResponse
를 정의했으므로 마찬가지로runBlocking
내부에서 액세스해야 합니다.
runBlocking {
val apiResponse = service.getPhotos()
assertNotNull(apiResponse)
}
getPhotos()
는 MarsPhoto
객체가 포함된 목록을 반환하므로 목록이 예상 크기와 같은지 확인해야 합니다.
- test/res에 있는 mars_photos.json 파일을 확인합니다. 목록이 비어 있지 않도록 또 다른 어설션을 생성합니다. 일부 데이터가 올바른지 확인하는 어설션도 만들어 보겠습니다. test/res/mars_photos.json으로 이동하여 ID 중 하나를 복사합니다. 이 ID의 값이 상응하는 목록 항목의 ID 값과 동일한지 어설션합니다.
runBlocking {
val apiResponse = service.getPhotos()
assertNotNull(apiResponse)
assertTrue("The list was empty", apiResponse.isNotEmpty())
assertEquals("The IDs did not match", "424905", apiResponse[0].id)
}
이제 테스트 코드는 다음과 같습니다.
8. 솔루션 코드
9. 축하합니다
네트워크 요청 테스트는 매우 복잡할 수 있으며 이 Codelab에서는 이 주제에 관해 간단하게만 다룹니다. 개발자가 작성하는 네트워크 테스트 각각은 앱이 데이터를 가져오는 API마다 다릅니다. API로 작업하는 경험이 많아지면 테스트를 확장하여 네트워크 장애와 다양한 API 응답을 시뮬레이션할 수 있습니다. 이는 매우 까다로운 테스트 영역이며, 전문가가 되기 위해 시행착오가 필요하므로 계속 연습해야 합니다.
이 Codelab에서 배운 내용은 다음과 같습니다.
- 테스트에 객체 지향 프로그래밍을 적용하는 방법
- 테스트 디렉터리에 리소스를 저장하는 방법
- 테스트에서 정지 함수를 호출하는 방법
- 테스트에서 API 응답을 모의 처리하는 방법