#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
์ด ๊ฒ์๊ธ์ ์์ฑ๋ ์ฑ์ ๊ฐ์ ธ์์ ํ
์คํธ๋ฅผ ์ํํ๋ค. ํด๋น ์ฑ์ ๊ฐ์ ธ์์ #1-2์ ๋ง๊ฒ ํ๊ฒฝ ์ค์ ์ ์์ ํ๋ค.
#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
'๊นจ์ ๊ฐ๋ ๐ > Android' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
[Android] Dagger2 - Hilt๋ก ๋ง์ด๊ทธ๋ ์ด์ (0) | 2024.07.12 |
---|---|
[Android] Data Binding - View Binding (1) | 2024.07.10 |
[Android] Unit Testing - Room๊ณผ LiveData (1) | 2024.07.08 |
[Android] Unit Testing - ViewModel (0) | 2024.07.08 |
[Android] Unit Testing - ๊ธฐ์ด (0) | 2024.07.08 |