λ Έλ¨Έμ€ λͺ¨λ°μΌνμ΄ TDDλ₯Ό νμ©νλ λ²
- #TDD
- #BDD
- #Android
- #iOS
λ€μ΄κ°λ©°
μλ νμΈμ! μ λ λ Έλ¨Έμ€ λͺ¨λ°μΌνμ κ°λ° 리λ μ΅λμμ λλ€. νμ¬λ frommμ΄λΌλ μλΉμ€λ₯Ό κ°λ°νλ©°, λ€μν κΈ°μ μ λμ κ³Όμ λ₯Ό ν΄κ²°νκ³ μμ΅λλ€.
κ°λ°μλΌλ©΄ λꡬλ ν λ²μ―€μ TDD(Test-Driven Development)λΌλ μ©μ΄λ₯Ό μ νκ±°λ, μ΄ λ°©λ²λ‘ μ λμ νλ € μλν΄λ³Έ κ²½νμ΄ μμ κ²μ λλ€. TDDλ λ§μ κ°λ° μμ κ³Ό κ°μ°μμ λμ μ½λ νμ§, μμ μ±, μ μ§λ³΄μμ±μ 보μ₯νλ νμ μ μΈ λ°©λ²λ‘ μΌλ‘ μκ°λ©λλ€. κ·Έλ¬λ μ΄λ₯Ό μ€λ¬΄μμ μ€μ λ‘ μ μ©νκ³ , νΉν μ΄μ μ€μΈ μλΉμ€μ κ°λ° κ³Όμ μμ 체κ³μ μΌλ‘ νμ©ν κ²½νμ κ°μ§ κ°λ°μλ λ§μ§ μμ κ²μ λλ€.
νΉν, frommμ²λΌ λ€μν κΈ°λ₯μ ν¬ν¨νκ³ μμΌλ©°, 볡μ‘ν UI/UX μꡬμ¬νκ³Ό μ€ννΈμ νΉμ μ λΉ λ₯Έ κ°λ° μ¬μ΄ν΄μ μΆ©μ‘±ν΄μΌ νλ λͺ¨λ°μΌ μ± μλΉμ€μ κ²½μ° TDDλ₯Ό μ μ©νλ κ³Όμ μ λμ± κΉλ€λ‘μ΅λλ€. μ΄λ‘ μ μΌλ‘λ μ΄μμ μ΄μ§λ§, νμ€μ μΌλ‘λ μ νλ 리μμ€μ λΉ λ₯Έ μμ¬κ²°μ μμμ TDDλ₯Ό λμ νκ³ μ μ§νκΈ°λ μ½μ§ μμ΅λλ€.
κ·ΈλΌμλ λΆκ΅¬νκ³ , λ Έλ¨Έμ€ λͺ¨λ°μΌνμ TDDλ₯Ό ν΅ν΄ μ½λ νμ§κ³Ό κ°λ° μμ°μ±μ λμ΄κΈ° μν΄ κΎΈμ€ν λ Έλ ₯ν΄μμ΅λλ€. μ΄ κΈμμλ μ ν¬ νμ΄ μ€λ¬΄μμ TDDλ₯Ό μ΄λ»κ² μ μ©νλμ§, κ²ͺμ μ΄λ €μκ³Ό μ΄λ₯Ό 극볡νκΈ° μν κ³Όμ μμμ μνμ°©μ€λ₯Ό 곡μ νκ³ μ ν©λλ€. μ΄ κΈμ΄ μ¬λ¬λΆμκ² μ€μ§μ μΈ λμκ³Ό μκ°μ μ€ μ μκΈ°λ₯Ό λ°λλλ€.
TDDλ?
TDD(Test-Driven Development)λ μΌνΈ 벑(Kent Beck)μ΄ κ³ μν μννΈμ¨μ΄ κ°λ° λ°©λ²λ‘ μΌλ‘, βν μ€νΈ μ£Όλ κ°λ°βμ΄λΌλ μ΄λ¦ κ·Έλλ‘ ν μ€νΈλ₯Ό λ¨Όμ μμ±νκ³ μ΄λ₯Ό κΈ°λ°μΌλ‘ μ½λλ₯Ό μμ±νλ λ°©μμ λλ€. TDDλ λ€μκ³Ό κ°μ λ°λ³΅μ κ³Όμ μ ν΅ν΄ μ΄λ£¨μ΄μ§λλ€.
- Red: μ€ν¨νλ ν
μ€νΈ μμ±
- λ¨Όμ μμ±ν ν μ€νΈ μΌμ΄μ€κ° μ€ν¨νλλ‘ μ€κ³ν©λλ€. μ΄λ ꡬνλμ§ μμ κΈ°λ₯μ λͺ νν μ μνλ λ¨κ³μ λλ€.
- Green: ν
μ€νΈλ₯Ό ν΅κ³Όνλ μ΅μνμ μ½λ μμ±
- ν μ€νΈκ° μ±κ³΅νλλ‘ κ°λ¨νκ² μ½λλ₯Ό ꡬνν©λλ€. μ΄ λ¨κ³μμλ μ΅μνμ κΈ°λ₯ ꡬνμ μ§μ€ν©λλ€.
- Refactor: μ½λ κ°μ
- ν μ€νΈκ° ν΅κ³Όν ν, μ½λλ₯Ό 리ν©ν°λ§νμ¬ κ°λ μ±κ³Ό μ μ§λ³΄μμ±μ κ°μ ν©λλ€. μ΄λ ν μ€νΈκ° λ€μ μ€ν¨νμ§ μλλ‘ μ£Όμν©λλ€.
TDDμ κΈ°λ ν¨κ³Ό
TDDμ μ£Όμ μ₯μ μ λ€μκ³Ό κ°μ΅λλ€.
- λ²κ·Έ κ°μ: ν μ€νΈλ₯Ό κΈ°λ°μΌλ‘ μ½λλ₯Ό μμ±νκΈ° λλ¬Έμ μ½λμ μμ μ±μ΄ λμμ§λλ€.
- 리ν©ν°λ§ μμ μ±: μ½λ λ³κ²½ μ ν μ€νΈκ° μ¬λ°λ₯΄κ² λμνλμ§ λ³΄μ₯ν©λλ€.
- λͺ νν μ€κ³: ν μ€νΈ μμ± κ³Όμ μμ μΈν°νμ΄μ€μ λ‘μ§μ΄ μμ°μ€λ½κ² μ€κ³λ©λλ€.
νμ§λ§ TDDλ λ¨μν μ½λ© κΈ°λ²μ λμ΄, μννΈμ¨μ΄ κ°λ°μ μ² νμ΄μ μ¬κ³ λ°©μμΌλ‘ μ΄ν΄λμ΄μΌ ν©λλ€. μ€μ λ‘ μ΄λ₯Ό μ€λ¬΄μ μ±κ³΅μ μΌλ‘ μ μ©νκΈ° μν΄μλ μλΉμ€ νΉμ±κ³Ό νμ μν©μ λ§λ μ λ΅μ μ κ·Όμ΄ νμν©λλ€.
λͺ¨λ°μΌνμμμ TDD μ μ©
TDD vs BDD: μ€λ¬΄μμμ κ³ λ―Ό
λͺ¨λ°μΌ μ± κ°λ° νκ²½μμλ TDDμ μ ν΅μ μΈ λ°©μλ§μΌλ‘λ ν΄κ²°ν μ μλ λ¬Έμ λ€μ΄ μμ£Ό λ°μν©λλ€. μ΄λ₯Ό 극볡νκΈ° μν΄ μ ν¬ νμ TDDμ ν¨κ» BDD(Behavior-Driven Development) μ€νμΌμ μ μ ν κ²°ν©νμ¬ νμ©νμ΅λλ€.
μ ν΅μ TDDμ νκ³
μΌνΈ 벑μ μ ν΅μ μΈ TDD λ°©μμ μμ λ¨μμ ν μ€νΈλ₯Ό κΈ°λ°μΌλ‘ μ½λλ₯Ό μμ±νλ λ° μ€μ μ λ‘λλ€. νμ§λ§ λͺ¨λ°μΌ μ± κ°λ°μμλ λ€μκ³Ό κ°μ νκ³μ μ΄ μμμ΅λλ€.
- ν μ€νΈ μΌμ΄μ€μ νλ°μ μ¦κ° : UI/UXμ κ΄λ ¨λ λ€μν μλ리μ€λ₯Ό λͺ¨λ ν μ€νΈνλ €λ©΄ μΌμ΄μ€κ° κ³Όλνκ² λ§μμ§λλ€.
- λ³νμ λ°λ₯Έ μ μ§λ³΄μ λΆλ΄ : λͺ¨λ°μΌ μ±μ UI λ³κ²½μ΄ λΉλ²νλ©°, λΉμ¦λμ€ λ‘μ§λ μμ£Ό μμ λ©λλ€. λͺ¨λ λ³νλ§λ€ ν μ€νΈλ₯Ό μμ νλ κ²μ νμ€μ μΌλ‘ μ΄λ ΅μ΅λλ€.
- λλ©μΈ μ΄ν΄μ μ΄λ €μ : μ§λμΉκ² μΈλΆνλ ν μ€νΈλ μ€νλ € λλ©μΈ λ‘μ§μ νμ νκΈ° μ΄λ ΅κ² λ§λλλ€.
BDDλ‘μ μ ν: μ¬μ©μ μ€μ¬ ν μ€νΈ
μ΄λ¬ν λ¬Έμ λ₯Ό ν΄κ²°νκΈ° μν΄, μ ν¬ νμ Given-When-Then ν¨ν΄μ κΈ°λ°μΌλ‘ ν BDD λ°©μμ λ³ννμ΅λλ€.
μλ₯Ό λ€μ΄, βνλ‘νμ ν΄λ¦νμ λ ν΄λΉ νλ‘νμ΄ μ‘΄μ¬νλ©΄ νλ‘ν μ΄λ―Έμ§ νλ©΄μΌλ‘ μ΄λνλ€.βλΌλ μ¬μ©μ μλ리μ€λ₯Ό κΈ°λ°μΌλ‘ μμ±ν ν μ€νΈλ λ€μκ³Ό κ°μ΅λλ€.
fun `OnClickProfile_Profile exist_navigate ToProfileImage`() = runTest {
//test code
}
BDD μ€νμΌμ ν΅ν΄ μμ±λ ν μ€νΈλ λ€μκ³Ό κ°μ μ₯μ μ κ°μ§λλ€.
- λΉμ¦λμ€ μꡬμ¬νμ λͺ νν λ°μ : ν μ€νΈ μΌμ΄μ€λ₯Ό ν΅ν΄ μ€μ μꡬμ¬νμ΄ μ½λμ λͺ νν λ Ήμλ€ μ μμ΅λλ€.
- λ³νμ μ μ°ν μ€κ³ κ°λ₯ : λΉλ²ν λ³νμλ λλ©μΈ λ‘μ§μ μ€μ¬μΌλ‘ μμ μ μΈ ν μ€νΈλ₯Ό μ μ§ν μ μμ΅λλ€.
νμμ λ°λΌ, λλ©μΈ λ΄λΆ λ‘μ§μ΄λ νΉμ μ νΈλ¦¬ν° ν¨μ κ²μ¦μ μν΄ μ ν΅μ μΈ TDD λ°©μλ μ μ ν λ³ννμ΅λλ€.
TDDμ λͺ©μ μ μ€κ³μ νμ§ ν₯μ
TDDλ λ¨μν λ²κ·Έλ₯Ό λ°©μ§νλ λꡬ, μ£μ§ μΌμ΄μ€λ₯Ό μ κ²νκΈ° μν λκ΅¬κ° μλλΌ, μ€κ³ νμ§μ λμ΄λ μ² νμΌλ‘ μ΄ν΄ν΄μΌ κ·Έ κ°μΉλ₯Ό μ λλ‘ νμ© ν μ μμ΅λλ€. μ ν¬ νμ μ€κ³ νμ§μ ν₯μμ μ£Ό λͺ©μ μΌλ‘ νμ¬ TDDλ₯Ό νμ©νμμΌλ©° λ€μκ³Ό κ°μ μ€κ³ κ°μ ν¨κ³Όλ₯Ό κ²½ννμ΅λλ€.
- μΈν°νμ΄μ€ μ€κ³ : ν μ€νΈλ₯Ό λ¨Όμ μμ±νλ κ³Όμ μμ μμ°μ€λ½κ² μΈν°νμ΄μ€κ° ꡬ쑰νλ©λλ€.
- μ¬μ 리뷰 κ°λ₯ : ν μ€νΈ μΌμ΄μ€λ₯Ό μμ±νλ©΄μ μ€κ³ λ¨κ³μμλΆν° 리뷰λ₯Ό λ°μ μ μμ΅λλ€.
- νμ§κ³Ό μμ°μ± ν₯μ : μ μ€κ³λ μΈν°νμ΄μ€λ μ½λ νμ§μ λμ΄κ³ , λΆνμν μμ λΉμ©μ μ€μ λλ€.
TDDμ μ§μ°©νμ§ μλ μ μ°ν¨
μ ν¬ νμ TDDλ₯Ό μ격ν μ μ©νλ λμ , ν¨μ¨μ μΈ μ€κ³μ λΉ λ₯Έ κ°λ° μ£ΌκΈ° μ μ§λ₯Ό λͺ©νλ‘ νκ³ μμ΅λλ€. λͺ¨λ μμμ TDDλ₯Ό μ μ©ν기보λ€λ, ν΅μ¬ λΉμ¦λμ€ λ‘μ§μ μ§μ€νμ¬ ν¨κ³Όλ₯Ό κ·Ήλννκ³ μμ΅λλ€.
μ ν¬λ βμλ²½ν ν μ€νΈ 컀λ²λ¦¬μ§βκ° λͺ©νκ° μλλΌ, ν¨μ¨μ μΈ μ€κ³μ λΉ λ₯Έ κ°λ° μ£ΌκΈ°λ₯Ό μ μ§νλ κ²μ λͺ©νλ‘ μΌκ³ μμ΅λλ€. μλΉμ€λ μμ£Ό λ³κ²½λκΈ° λλ¬Έμ, λͺ¨λ ν μ€νΈλ₯Ό μμ νκ³ μ μ§λ³΄μνλ κ²μ λΆκ°λ₯ν©λλ€. TDDμ κ³Όλνκ² μ§μ°©ν κ²½μ°, μ€νλ € μλκ° λλ €μ§κ³ ν μ€νΈ μ½λ μμ²΄κ° μ μ§λ³΄μμ λμμ΄ λ μνμ΄ μμ΅λλ€.
μ ν¬λ TDDλ₯Ό λͺ¨λ λ²μμ μ μ©νμ§ μκ³ , ν΅μ¬ μμμλ§ μ νμ μΌλ‘ λμ νμ΅λλ€. μλ₯Ό λ€μ΄, Clean Architectureλ₯Ό κΈ°λ°μΌλ‘ ν μλΉμ€ μ€κ³μμ, Usecase λ μ΄μ΄μλ§ TDDλ₯Ό μ§μ€μ μΌλ‘ μ μ©νκ³ , Presentation λ° Data λ μ΄μ΄μμλ νμμ λ°λΌ μ νμ μΌλ‘ ν μ€νΈλ₯Ό μμ±ν©λλ€. μ΄λ₯Ό ν΅ν΄ TDDμ μ΄μ κ³Ό λΉ λ₯Έ κ°λ° μ£ΌκΈ° μ¬μ΄μ κ· νμ μ μ§ν μ μμμ΅λλ€.
λͺ¨λ°μΌνμμμ TDD μμ μ¬λ‘
κ°λ¨ν μμλ₯Ό ν΅ν΄μ λͺ¨λ°μΌνμμ TDDλ₯Ό μ΄λ»κ² μ μ©νκ³ μλμ§ μκ°νκ² μ΅λλ€. λ¨Όμ λ€μκ³Ό κ°μ μꡬμ¬νμ ꡬνν΄μΌνλ€κ³ κ°μ νκ² μ΅λλ€.
νλ‘ν νλ©΄μ§μ
μ νλ‘ν λ°μ΄ν°λ₯Ό λΆλ¬μμ μ¬μ©μμκ² λ
ΈμΆνλ€.
νμ€μμλ μμ κ°μ λͺ νν μꡬμ¬νλ³΄λ€ λ³΄λ€ λ³΅μ‘νκ³ , νλ©΄κΈ°ν, μ μ± λ±μ΄ ν¬ν¨λ κΈ°νμκ° μ λ¬ λ κ²μ λλ€. μΌλ¨ μμ μ¬λ‘μμλ λ¨μν νλ‘ν νλ©΄ μ§μ μ μ¬μ©μμ νλ‘νμ λ ΈμΆνλ κΈ°λ₯μ ꡬννλ€κ³ κ°μ νκ² μ΅λλ€. μꡬμ¬νμ μ λ¬ λ°μΌλ©΄ λ΄λΉμλ Usecaseλ₯Ό μ€κ³νκ² λ©λλ€. μ΄λ Usecase μ€κ³κ° TDDμ ν¨κ» μ§νλ©λλ€.
μμ μλ νλ‘ν λ°μ΄ν°λ₯Ό κ°μ Έμ€λ FetchProfileUsecase λ₯Ό λ§λ€κ³ μ νκ³ μλμ κ°μ΄ ν μ€νΈ μΌμ΄μ€λ₯Ό μκ°ν΄ λ³Ό μ μμ΅λλ€.
@Test
fun `launch profile screen_given profile id_return profile data`() = runTest {
}
ν΄λΉ ν μ€νΈ μ½λλ νλ‘ν νλ©΄μ μ§μ νμ λ, μ£Όμ΄μ§ νλ‘νIDλ₯Ό κ°μ§κ³ profile data λ₯Ό 리ν΄ν΄μΌνλ ν μ€νΈ μΌμ΄μ€κ° λ©λλ€. λ¬Όλ‘ μ΄μΈμλ λ€μν κΈ°νμ μΈ μμΈμν© (ex. νλ‘ν μμ΄λκ° μμ λλ μ€λ₯ νμ μ λ ΈμΆνλ€ λ±)μ λ°μν ν μ€νΈ μΌμ΄μ€λ μκ² μ§λ§ ν΄λΉ λΆλΆλ€μ μλ΅νκ² μ΅λλ€. μ΄μ ν μ€νΈ μ½λλ₯Ό μμ±ν΄μΌνλλ° μ΄λ Repository Interface μ€κ³κ° ν¨κ» μ΄λ£¨μ΄μ§λλ€. μμ μλ μλ²μμ νλ‘ν λ°μ΄ν°λ₯Ό κ°μ ΈμμΌ νκΈ° λλ¬Έμ repository interface λ₯Ό ꡬμ±νκ³ λ€μκ³Ό κ°μ΄ ν μ€νΈ μ½λλ₯Ό μμ± ν κ²μ λλ€. λλ΅μ μΌλ‘λ μλμ κ°μ ν μ€νΈ μ½λλ₯Ό μμ± ν μ μμ΅λλ€. (μλ μ½λλ κ°λ΅νκ² μμ± λ μμμ λλ€.)
@Test
fun `launch profile screen_given profile id_return profile data`() = runTest {
val mockRepository = mockk<ProfileRepository>()
val profile = Profile("User123", "user123@example.com")
coEvery { mockRepository.getProfile(any()) } returns profile
val useCase = FetchProfileUseCase(mockRepository)
val result = useCase("user123")
assertEquals(profile, result)
}
μ΄ κ³Όμ μ ν΅ν΄ Repository μΈν°νμ΄μ€μ Profile μ΄λΌλ λλ©μΈμ΄ μ€κ³κ° λ©λλ€. ν΄λΉ μ½λμ μΈν°νμ΄μ€, λλ©μΈ λͺ¨λΈμ μ½λ리뷰λ₯Ό ν΅ν΄μ νλ² λ κ²μ¦λκ³ μ΄ν Dataλ μ΄μ΄, Presentationλ μ΄μ΄ μμ μ΄ μ§νλ©λλ€.
μ μμμμλ λ§€μ° κ°λ΅ν μΌμ΄μ€λ‘ μΆμ½ν΄μ μ€λͺ μ λλ Έλλ°μ, μ€μ 볡μ‘ν λ‘μ§μΌ μλ‘ TDDμ ν¨κ³Όλ₯Ό λ§μ΄ λλ μ μμ΅λλ€. μλ μ½λ μ ν¬κ° μ¬μ©νκ³ μλ ν μ€νΈ μΌμ΄μ€μ μ€ μμμ λλ€.
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class ExampleUseCaseTest {
private lateinit var useCase: ExampleUseCase
@MockK
private lateinit var repositoryA: RepositoryA
@MockK
private lateinit var repositoryB: RepositoryB
@MockK
private lateinit var handler: Handler
private val fixture = kotlinFixture()
@BeforeAll
fun setup() {
MockKAnnotations.init(this)
useCase = ExampleUseCase(
repositoryA = repositoryA,
repositoryB = repositoryB,
handler = handler
)
}
@Test
fun `Given repositoryB throws NotFoundException When useCase is invoked Then result is Failure`() = runTest {
val testData: DataType = fixture()
coEvery { repositoryA.getData(any()) } returns testData
coEvery { repositoryB.connect(any(), any()) } throws CustomException.NotFoundException
coEvery { handler.cleanup() } returns Unit
val result = useCase.invoke(ExampleUseCase.Param(id = fixture(), key = fixture()))
assertThat(result).isEqualTo(ExampleUseCase.Result.Failure)
}
@Test
fun `Given repositoryA throws DataNotFound When useCase is invoked Then result is Failure`() = runTest {
coEvery { repositoryA.getData(any()) } throws CustomException.DataNotFound
coEvery { repositoryB.connect(any(), any()) } returns Unit
coEvery { handler.cleanup() } returns Unit
val result = useCase.invoke(ExampleUseCase.Param(id = fixture(), key = fixture()))
assertThat(result).isEqualTo(ExampleUseCase.Result.Failure)
}
@Test
fun `Given valid inputs When all operations succeed Then result is Success`() = runTest {
val data: DataType = fixture()
val metadata: MetadataType = fixture()
val additionalInfo: AdditionalInfo = fixture()
coEvery { repositoryA.getData(any()) } returns data
coEvery { repositoryB.connect(any(), any()) } returns Unit
coEvery { repositoryB.getMetadata() } returns metadata
coEvery { repositoryB.getAdditionalInfo() } returns additionalInfo
every { repositoryB.isConditionMet() } returns true
val result = useCase.invoke(ExampleUseCase.Param(id = fixture(), key = fixture()))
assertThat(result).isEqualTo(
ExampleUseCase.Result.Success(
data = data,
metadata = metadata,
additionalInfo = additionalInfo,
conditionMet = true
)
)
}
@Test
fun `Given metadata indicates termination When useCase is invoked Then result is Failure`() = runTest {
val data: DataType = fixture()
coEvery { repositoryA.getData(any()) } returns data
coEvery { repositoryB.connect(any(), any()) } returns Unit
coEvery { repositoryB.getMetadata() } returns MetadataType.TERMINATED
val result = useCase.invoke(ExampleUseCase.Param(id = fixture(), key = fixture()))
assertThat(result).isEqualTo(ExampleUseCase.Result.Failure)
}
}
λ§μΉλ©°
TDDλ λͺ¨λ νκ²½μμ λμΌνκ² μ μ©λ μ μμ΅λλ€. νμ μλ, μλΉμ€μ νΉμ±, νμ¬μ μν©μ λ°λΌ κ·Έ λ°©μμ λ¬λΌμ ΈμΌ ν©λλ€. μ ν¬ λ Έλ¨Έμ€ λͺ¨λ°μΌνμ TDDμ BDDλ₯Ό μ μ ν κ²°ν©νμ¬ ν΅μ¬μ μΈ μμμ μ§μ€ν¨μΌλ‘μ¨, ν¨μ¨μ μΈ μ€κ³μ μ½λ νμ§μ λμμ λ¬μ±νκ³ μ λ Έλ ₯ν΄μμ΅λλ€.
μ΄ κΈμ΄ TDD λμ μ κ³ λ―Όνκ³ κ³μ λΆλ€μκ² μ‘°κΈμ΄λλ§ λμμ΄ λκΈΈ λ°λΌλ©°, κ°μμ νκ²½μ λ§λ μ΅μ μ λ°©λ²μ μ°Ύμκ°μκΈΈ μμν©λλ€. κ°μ¬ν©λλ€! π