TDD에 대해 들어 보신적 있나요?
TDD(Test-Driven-Development) 테스트 주도 개발은
테스트 코드를 먼저짜고
테스트를 통과하도록 개발하는 개발 방법론입니다
오늘은 Hilt Test와 함께 TDD가 어떻게 진행되는지
간단한 예제를 통해 제대로 보여드리겠습니다!
맞죠. TDD는 고사하고 테스트 코드짜는거 조차 귀찮을 때가 많습니다.
굳이 테스트 코드안짜도 앱은 잘 돌아가잖아요?
단기적으로 봤을땐 테스트 코드를 안짜는게 개발 시간도 단축하고
이득 일 수도 있습니다.
하지만! 그럼에도 불구하고 TDD를 채택하는 이유는
귀찮음을 압도하는 장점들을 가지고 있기 때문입니다.
에러가 발생 할 때 일일히 모듈을 디버깅하는데 시간이 많이든다면?
간단한 기능을 추가했는데 그로 인해 주변기능을 전부 테스트 해야 한다면?
코드 리팩토링을 해야 하는데 잘못 건드려서 에러날까봐 두렵다면?
Wow! TDD를 도입해보세요! 이러한 고민들을 줄이는데 도움을 줄 수 있습니다.
미리짜둔 테스트 코드 실행 한방으로 모든 걱정 Clear!
간단한 예제를 통해 TDD를 맛보시죠.
App Gradle
id ("dagger.hilt.android.plugin")
kotlin("kapt")
testImplementation("com.google.dagger:hilt-android-testing:2.48") kaptTest("com.google.dagger:hilt-android-compiler:2.47") androidTestImplementation("com.google.dagger:hilt-android-testing:2.48") kaptAndroidTest("com.google.dagger:hilt-android-compiler:2.47") implementation ("com.google.dagger:hilt-android:2.48") kapt ("com.google.dagger:hilt-compiler:2.47")
Project Gradle
buildscript {
dependencies {
classpath ("com.google.dagger:hilt-android-gradle-plugin:2.48.1")
}
}
필요한 의존성들을 gradle에 추가해줍시다.
class HiltTestRunner : AndroidJUnitRunner() {
override fun newApplication(
cl: ClassLoader?,
className: String?,
context: Context?
): Application {
return super.newApplication(cl, HiltTestApplication::class.java.name, context)
}
}
AndroidTest폴더에
테스트에 사용할 HiltTestRunner를 만들어줍시다.
그후 App Gradle의 defaultConfig에서 만들어둔
TestRunner를 지정해줍시다.
defaultConfig {
...
testInstrumentationRunner = "com.example.hilttest.HiltTestRunner"
...
}
interface FakeCalculator {
fun add(a: Int, b: Int) : Int
}
class FakeCalculatorImpl @Inject constructor() : FakeCalculator {
override fun add(a: Int, b: Int) = a + b
}
테스트에 사용할 Fake 계산기를 만들어 두겠습니다.
@Module
@InstallIn(SingletonComponent::class)
abstract class CalculatorModule {
//Empty...
}
@Module
@TestInstallIn(
components = [SingletonComponent::class],
replaces = [CalculatorModule::class]
)
abstract class FakeCalculatorModule {
@Binds
abstract fun bindsCalculator(calculatorImpl: FakeCalculatorImpl): FakeCalculator
}
의존성을 주입해줄 테스트 모듈과 임시로 추후에 사용할 진짜 모듈을 만들도록합시다.
테스트에서는 replaces = [CalculatorModule::class] 이부분을 통해
FakeCalculatorModule이 사용됩니다.
@HiltAndroidApp
class Application : Application() {
}
//Manifest
android:name=".Application"
Hilt Application을 App폴더에 만들고 Manifest에 등록해줍시다.
@HiltAndroidTest
class CalculatorTest {
@get:Rule
var hiltRule = HiltAndroidRule(this)
@Inject
lateinit var fakeCalculator: FakeCalculator
@Before
fun setUp() {
hiltRule.inject()
}
@Test
fun `숫자를_더한다`() {
val a = 1
val b = 2
val result = fakeCalculator.add(a, b)
assertEquals(3, result)
}
}
@HiltAndroidTest 을 통해 의존성을 주입받는 클래스라는 것을 명시해줍니다.
HiltAndroidRule로 의존성 주입을 받을 수 있게 해줍니다.
Fake가 잘 돌아가는 것이 확인 되었으니 이 테스트를 통과하도록
진짜 객체를 만들어야겠죠?
interface Calculator {
fun add(a: Int, b: Int) : Int
}
class CalculatorImpl @Inject constructor() : Calculator {
override fun add(a: Int, b: Int) = a + b
}
@Module
@InstallIn(SingletonComponent::class)
abstract class CalculatorModule {
@Binds
abstract fun bindsCalculator(calculatorImpl: CalculatorImpl) : Calculator
}
작성해둔 테스트 모듈을 기반으로 진짜 객체를 만들어 주고
테스트 모듈이 아닌 진짜 모듈을 사용하기 위해 Fake Module을 지우거나 변경해줍시다.
@Inject
lateinit var calculator: Calculator
@Test
fun `숫자를_더한다`() {
val a = 1
val b = 2
val result = calculator.add(a, b)
assertEquals(3, result)
}
아까 작성해둔 Fake만 진짜 객체로 바꿔줬습니다.
만약 제가 테스트 의도 대로 개발했다면 테스트가 통과하겠죠?
무사히 통과했네요!
class CalculatorImpl @Inject constructor() : Calculator {
override fun add(a: Int, b: Int) = a * b
}
저는 힙한 사람이기 때문에 더하라 하면 곱해버립니다.
테스트 코드 작성자의 의도와는 완전 다르죠?
1 + 2 면 3이 나와야하는데 1 * 2로 해서 2가 나왔네요...
이와 같이 테스트 코드에 맞게 개발하는 것이 TDD입니다.
리팩토링 할 때 진짜 객체의 핵심 로직을 바꿔버린다면
테스트가 실패합니다.
그와 반대로 테스트를 통과한다면 의도에 맞게 잘 개발 중이라는 뜻이니
확신을 가지고 개발을 진행할 수 있게 됩니다.
자신이 잘 개발중인지 또는 건드리면 안되는걸 건드렸는지
확인할 수 있는 좋은 수단이 생겼으니 결론적으로 생산성이 올라갑니다.
이 글을 읽으시는 독자 분들도 TDD는 아니더라도
테스트 코드를 습관화 하여 조금 더 유연한 개발을 하셨으면 좋겠습니다.
https://github.com/lyh990517/Android_TDD_Example_Using_HiltTest
https://velog.io/@dhtjdals77/TDD-%EC%A0%95%EC%9D%98-%EB%B0%8F-%EC%9E%A5%EB%8B%A8%EC%A0%90