작성자:시아막 마흐무디
TDD(Test-Driven Development)는 실제 코드가 구현되기 전에 테스트를 작성하는 소프트웨어 개발 접근 방식입니다.
프로젝트/기능의 요구사항에서 "무엇"과 "어떻게"에 대한 명확한 이해가 필요합니다.
TDD는 적은 양이지만 충분한 코드를 작성하는 데 도움이 됩니다. 이는 과도한 엔지니어링, 너무 많은 테스트 범위, 주요 요구 사항 누락, 너무 큰 함수 및 클래스, 너무 많은 복잡한 코드 문과 같은 일반적인 소프트웨어 개발 실수를 방지하는 데 도움이 됩니다.
전반적으로, 이미 단위 테스트가 포함된 간결하고 깔끔한 코드베이스를 갖는 것이 도움이 됩니다. 시간이 지남에 따라 개발 및 코드 유지 관리 비용도 절약됩니다.
이 글에서는 실제 TDD에 대해 논의하겠습니다.
컨텍스트는 Android 개발 환경이므로 샘플 프로젝트와 함께 Kotlin 및 JUnit5를 사용하여 단계를 보여 드리겠습니다.
그러나 여기에 나온 지침과 기술은 다른 프로그래밍 언어에서도 연습할 수 있습니다.
전제조건
- Kotlin 기본 지식
- 단위 테스트 작성에 대한 기본 지식
- 모의 및 주장에 대한 지식
프로그래밍 언어로 Kotlin을 사용하고 단위 테스트를 작성하는 데 JUnit5를 사용하겠습니다.
Mockito는 모의 및 스파이 작업에 사용됩니다.
대상 독자는 자신의 경력에서 새로운 장을 찾고 있는 모든 플랫폼의 모든 소프트웨어 개발자입니다.
컨텍스트가 Android임에도 불구하고 콘텐츠는 플랫폼별 속성에 대해 설명하지 않습니다. 대신 TDD로 개발할 때 기술, 참고 사항 및 과제에 중점을 둡니다.
위 내용이 괜찮으시다면 시작해 보세요.
테스트 주도 개발의 작동 방식
TDD 주기
개발 프로세스는 다음 주기를 따릅니다:
- 실패한 테스트 작성(분홍색 사각형)
- 테스트 통과를 위한 코드 구현(녹색 사각형)
- 테스트가 계속 통과하는지 확인하는 동시에 필요에 따라 코드를 리팩터링(파란색 사각형)합니다(연한 녹색 사각형).
- 새 실패한 테스트 작성(흐름을 다시 시작)
실패하는 테스트 작성(분홍색 사각형)
이 단계에서는 코드에서 수행하려는 작업을 설명하는 것부터 시작합니다.
코드가 올바르게 작동하는지 확인하기 위해 테스트를 제공한다고 상상해 보세요. 이 테스트는 "이 작업을 수행할 수 있습니까?"와 같이 코드에 묻는 질문과 같습니다.
처음에는 코드가 답을 모르기 때문에 코드가 아직 작업 수행 방법을 모르기 때문에 실패해야 하는 테스트를 작성합니다. 이 실패한 테스트는 무언가 잘못되었음을 알려주는 분홍색 경고 표시와 같습니다.
이 단계를 완료하면 JUnit5는 귀하가 작성한 테스트를 바탕으로 포괄적인 보고서를 생성합니다. 이러한 테스트는 귀하의 작업을 가시적으로 표현하는 역할을 합니다.
이제 프로젝트 관리자가 이러한 테스트 사례를 읽고 해당 범위와 기능 또는 제품에 대한 이해의 정확성을 평가하고 있다고 상상해 보십시오. 이러한 관점을 받아들이면 이 개발 단계의 중요성을 더욱 명확하게 이해할 수 있습니다.
기술적 복잡성에서 소프트웨어 동작 자체로 초점을 전환하십시오.
핵심적인 기술적 측면에 얽매이지 말고 소프트웨어가 어떻게 작동하고 사용자 및 기타 구성 요소와 상호 작용하는지에 주의를 기울이십시오.
이러한 관점의 변화를 통해 소프트웨어의 의도된 작업과 결과의 우선순위를 정할 수 있으므로 실제 동작을 정확하게 반영하는 테스트가 가능해집니다.
기술적인 세부 사항보다는 동작에 집중함으로써 테스트가 소프트웨어의 목적 및 사용자 기대와 긴밀하게 일치하는지 확인할 수 있습니다.
어떤 경우에는 구성 요소당 몇 개의 테스트 사례만 남게 될 수도 있으며(목적은 작업은 적지만 대상은 적음) 프로젝트의 모든 동작 요구 사항을 충족하는 한 완전히 괜찮습니다.
팁
- 구체적으로 작성하세요: 코드 동작의 한 측면에 초점을 맞춘 명확하고 구체적인 테스트 사례를 작성하세요.
- 간단하게 시작:필요한 기본 기능을 다루는 가장 간단한 테스트 사례로 시작하세요.
@Test fun `a sum is calculated from two input numbers`() {}
- 의미 있는 이름 사용:테스트 이름을 설명적으로 지정하여 읽는 사람이 테스트에서 확인하는 내용을 알 수 있도록 하세요.
@Test fun `Font Ratio is fetched from data source INITIALLY`() {}
피해야 할 일반적인 실수
- 한 번에 너무 많은 테스트: 단일 테스트에서 여러 항목을 테스트하지 마세요. 이로 인해 무엇이 실패했는지 파악하기가 어려워질 수 있습니다.
// Don't do this
@Test fun `pixelSize fits the standart sizes while fontSize is bigger than minumum supported font size but matches the list of special levels of size`() {}
- 구현 세부정보에 의존: 코드의 내부 작동과 밀접하게 결합된 테스트를 작성하지 마십시오. 테스트는 구현이 아닌 동작에 초점을 맞춰야 합니다.
// Don't do this
@Test fun `pixelSize is Long and Non-Null and fits the standart sizes then calculated font size is non-null and of type Dimention`() {}
테스트 통과를 위한 코드 구현(녹색 사각형)
이제 테스트가 완료되었으므로 작업을 올바르게 수행하는 방법을 코드에 가르칠 차례입니다.
테스트를 통과해야 하는 실제 코드를 작성하면 코드가 질문에 올바르게 대답합니다.
코드가 테스트를 통과하면 "예, 이제 작업을 수행할 수 있습니다!"라고 말하는 녹색 표시등과 같습니다.
이 단계는 코드가 요청한 문제를 이해하고 해결할 수 있는지 확인하는 것입니다.
팁
- 최소 코드 작성:실패한 테스트를 통과시키는 가장 간단한 코드를 작성합니다. 과도한 엔지니어링을 방지하는 방법에 대한 자세한 내용은 여기에서 확인할 수 있습니다.
// Test Case
@Test fun `Storage stores font ratio in key-value`() {
// Given
val fontRatio = 2.0f
val mockEditor = mockk<SharedPreferences.Editor>(relaxed = true)
every { mockSharedPreference.edit() } returns mockEditor
every { mockEditor.putFloat(any(), any()) } returns mockEditor
every { mockEditor.apply() } just Runs
// When
storage.saveFontRatio(fontRatio)
// Then
verify(exactly = 1) {
mockEditor.apply()
}
}
// Correct Implementation - Avoid extra implementation
class SharedPreferenceHelper(
private val sharedPreferences: SharedPreferences
) {
fun saveFontRatio(fontRatio: Float) {
sharedPreferences.edit().putFloat("font-ratio", fontRatio).apply()
}
}
// Wrong Implementation
class SharedPreferenceHelper(
private val sharedPreferences: SharedPreferences
) {
fun saveFontRatio(fontRatio: Float) {
if (fontRatio <= 0.0f)
throw IllegalArgumentException("Font ratio must be greater than 0.0f")
storeValue(key = FONT_RATIO_KEY, value = fontRatio)
}
private fun storeValue(key: String, value: Float){
val editor = sharedPreferences.edit() editor.putFloat(key, value)
editor.apply()
}
fun getFontRatio(): Float {
return sharedPreferences.getFloat("font_ratio", 1.0f) }
}
- 중복 방지: 코드를 반복하지 마세요. 여러 위치에서 유사한 논리를 작성하고 있다면 리팩토링을 고려해보세요. 이러한 기본 코드 개선은 이 단계에서 수행될 수 있지만 수정으로 인해 부작용이 발생할 수 있으면 무시하십시오.
class ... {
override fun getDefaultFontSize(): Float {
val zoomRatio = DEFAULT_SSPEED * DEFAULT_FONT_RATIO / deviceDensity
val fontSize = zoomRatio * standardFontSize
return fontSize
}
override fun getFontSizeBySSpeed(speed: Int): Float {
val zoomRatio = speed * DEFAULT_FONT_RATIO / deviceDensity
val fontSize = zoomRatio * standardFontSize
return fontSize
}
}
class ... {
override fun getDefaultFontSize(): Float = calculate(DEFAULT_AGE)
override fun getFontSizeByAge(age: Int): Float = calculate(age)
private fun calculate(age: Int): Float {
val zoomRatio = age * DEFAULT_FONT_RATIO / deviceDensity
val fontSize = zoomRatio * standardFontSize
return fontSize
}
}
피해야 할 일반적인 실수:
- 앞으로 도약:테스트를 통과하는 데 필요한 것보다 더 많은 코드를 작성하지 마십시오. TDD는 점진적인 개발에 관한 것입니다. TDD는 개발에 대한 점진적이고 단계별 접근 방식을 권장합니다. 앞서 나가면 본질적으로 현재 작업 중인 테스트와 아직 직접적인 관련이 없는 문제를 해결하려고 시도하는 것입니다. 주요 목표는 미래의 기능으로 인해 곁길로 가지 않고 당면한 작업, 즉 현재 테스트를 통과하는 데 집중하는 것입니다.
- 테스트 실패 무시:처음에 테스트가 실패하지 않으면 중요한 사례가 누락되었을 수 있습니다. 언뜻 보기에는 이런 일이 일어날 가능성이 거의 없어 보일 수도 있지만 테스트 구성 요소를 일부 개발한 후에는 논리의 다양한 측면을 테스트하기 위해 단일 메서드에 대한 여러 테스트를 작성하기 시작할 것입니다. 구현되지 않은 논리가 테스트를 통과하면 만족해서는 안 되는 부분입니다. 쉽게 말하면 개발단계에서 버그를 잡는 방법입니다. 그러니 실패가 있어야 할 때 실패를 예상하세요.
코드 리팩토링(파란색 사각형) 및 테스트 성공 보장(연한 녹색 사각형)
코드가 테스트를 통과하면 이제 문제를 정리할 차례입니다.
코드를 더 체계적으로 만들고, 이해하기 쉽게 만들거나, 더 빠르게 만드는 방법을 찾을 수 있습니다. 플레이를 마친 후 방을 정리하고, 플레이를 마친 후 모든 것을 깔끔하고 정리정돈하는 것으로 생각하십시오. 수행하는 작업을 변경하지 않고도 코드를 향상시킬 수 있습니다.
이렇게 하면서 모든 테스트를 계속 실행하여 테스트가 통과하는지 확인합니다. 이 단계에서 테스트가 실패하면 정리한 내용이 실수로 코드를 깨뜨렸을 수 있음을 알려주는 연한 녹색 주의 표시와 같습니다.
이 부분을 별도의 코드 유지 관리 단계로 처리할 수 있습니다.
오래된 코드를 정리하고 그것이 팀 코드 품질 지침과 제품 요구 사항을 준수하는지 확인하는 작업이 주어졌다고 가정합니다.
이 프로세스 전체에서 중요한 것은 테스트가 계속 성공하도록 하면서 균형을 신중하게 유지하는 것입니다.
리팩토링 단계에 대한 몇 가지 아이디어와 전략은 다음과 같습니다.
- 코드 명확성:복잡한 섹션을 단순화하고, 명확하지 않은 변수 이름을 대체하고, 주석을 강화하여 다른 사람(및 미래의 자신)이 코드를 더 쉽게 이해할 수 있도록 합니다.
- 모듈성:큰 기능을 더 작고 집중된 기능으로 나눕니다. 이렇게 하면 코드가 더욱 모듈화되고 유지 관리 및 테스트가 더 쉬워집니다.
- 중복성 제거:중복된 코드를 식별하고 재사용 가능한 함수 또는 클래스로 통합합니다. 이는 반복을 없애고 일관성을 보장하는 데 도움이 됩니다.
- 최적화:성능을 개선할 수 있는 영역을 식별합니다. 그러나 특정 성능 목표가 있고 코드에 병목 현상이 발생한다는 증거가 있는 경우에만 최적화하십시오. 여기서 최적화는 리소스 소모를 방지하는 것이지 코드 성능을 높이는 것이 아닙니다.
- 일관적인 형식:팀이나 프로젝트의 규칙을 준수하면서 일관된 코드 스타일을 유지합니다.
- 사용하지 않는 코드:코드베이스를 어지럽히는 사용하지 않는 변수, 함수 또는 가져오기를 제거합니다.
- 테스트 개선:TDD에 대한 일반적인 인식과 달리 필요할 때마다 테스트를 추가할 수 있습니다. 이전에 해결되지 않은 시나리오를 다루기 위해 새로운 테스트 케이스를 추가하여 테스트 스위트를 강화하십시오. 이는 포괄적인 테스트 범위를 유지하는 데 도움이 됩니다.
- 문서화:코드 목적이 코드 자체에서 즉시 명확하지 않은 경우 문서의 의도와 사용법을 설명하는 문서를 추가하거나 개선하는 것이 좋습니다. 습관이 되지 않도록 하세요. 이는 혼란을 피하기 위해 중요한 사건에 대한 보완적인 설명 역할을 하기 위한 것입니다.
TDD 코드는 자체적으로 표현되어야 하며 문서와 독립적이어야 합니다.
리팩터링하는 동안 모든 테스트를 계속 실행하여 계속해서 통과하는지 확인하는 것이 중요합니다.
팁
- 테스트를 포괄적으로 유지하세요. 리팩터링 중에 의도하지 않은 부작용을 포착하려면 테스트에서 다양한 시나리오를 다루어야 합니다.
- 점진적 리팩터링:코드를 조금씩 변경하고 자주 테스트를 실행하여 회귀를 조기에 포착하세요.
피해야 할 일반적인 실수:
- 테스트 없이 리팩토링: 테스트 없이 리팩토링하면 예상치 못한 동작이 발생할 수 있습니다. 논리의 일부를 놓칠 가능성이 있는 경우 이에 대한 테스트 작성을 고려하십시오.
- 대규모 코드 변경:때로는 테스트를 통과하기 위해 개발한 것보다 더 많은 행을 변경하게 되는 경우도 있습니다. 개발 단계에서 너무 많은 변경을 하는 것보다 항상 별도의 리팩토링 단계를 고려하는 것이 더 안전하고 비용이 적게 드는 옵션입니다.
새 실패한 테스트 작성(흐름 다시 시작)
이제 코드에서 수행하려는 다음 작업을 생각합니다.
코드가 아직 새 작업을 수행하는 방법을 모르기 때문에 실패해야 하는 새 테스트를 작성하는 것부터 시작합니다. 이는 코드에 해결해야 할 새로운 과제를 부여하는 것과 같습니다.
그런 다음 전체 주기를 반복합니다. 코드를 사용하여 테스트를 통과하고(녹색 사각형), 필요한 경우 정리하고(파란색 사각형), 모든 것이 작동하는지 확인하기 위해 계속 테스트합니다(연한 녹색 사각형).
이렇게 하면 항상 앞으로 나아가며 단계별로 코드를 구축할 수 있습니다.
팁
- 증분 단계:명확한 개발 경로를 유지하기 위해 새로운 기능에 대한 새로운 테스트를 조금씩 추가합니다. 복잡한 기능을 한꺼번에 구현하려고 시도하는 대신 이를 더 작고 관리 가능한 부분으로 나누고 각 부분에 대한 테스트를 만듭니다. 이 접근 방식은 명확하고 꾸준한 개발 경로를 유지하여 집중력을 유지하고 위험을 줄이며 소프트웨어에 추가되는 각 항목을 철저히 테스트하는 데 도움이 됩니다.
- 피드백 루프:실패한 테스트 작성에서 얻은 피드백을 사용하여 구현을 안내합니다. 피드백 루프는 TDD의 반복적 특성을 강조합니다. 새로운 테스트를 만들고 실패하는 것을 관찰하면서 구현을 안내하는 귀중한 통찰력을 얻을 수 있습니다.
피드백 루프의 작동 방식은 다음과 같습니다.
- 기대 설정:새 테스트를 작성할 때 코드가 어떻게 작동해야 하는지에 대한 기대를 정의합니다. 이를 통해 새로운 기능을 통해 달성하려는 목표가 명확해집니다.
- 초기 실패:기대치를 충족하는 해당 코드가 누락되었거나 불완전하기 때문에 테스트가 처음에 실패합니다. 이러한 초기 실패는 TDD 프로세스의 자연스러운 부분입니다.
- 구현 안내:테스트 실패에 대한 피드백을 통해 어떤 코드를 작성하거나 수정해야 하는지 방향을 알려줍니다. 이는 새로운 기능의 모습을 개략적으로 설명하는 개발 로드맵이 됩니다.
- 증분적 진행:테스트를 통과하는 데 필요한 코드를 구현하면서 원하는 기능을 점진적으로 구축하게 됩니다. 각 단계는 실패한 테스트에서 제공되는 피드백을 따릅니다.
- 확인:구현이 완료되면 테스트를 다시 실행합니다. 통과하면 새 코드가 처음 설정한 기대치를 충족하는지 확인합니다.
피드백 루프는 개발이 소프트웨어의 의도된 목표와 밀접하게 일치하도록 보장합니다.
피해야 할 일반적인 실수:
- 구현 후 테스트 작성:기능을 구현한 후에는 테스트를 작성하지 마세요. TDD는 테스트를 먼저 작성하는 것입니다. 테스트 코드 앞에 추가된 아주 작은 로직 조각이라도 코드에 리소스 낭비/버그가 있을 수 있음을 의미합니다. 요점은 테스트 스위트에서 필요한 경우가 아니면 로직을 추가하지 않는 것입니다.
- 실패한 테스트 건너뛰기:기능 구현 방법을 알고 있다고 생각하더라도 이 단계를 건너뛰지 마세요.
자신감이 있어도 실패한 테스트 단계를 건너뛰면 안되는 이유는 다음과 같습니다.
- 의도 명확성:실패한 테스트를 작성하면 기능에 대한 의도가 명확해집니다. 구현을 시작하기 전에 목표로 하는 정확한 동작과 결과를 고려해야 합니다.
- 가정 검증:기능을 이해했다고 생각하더라도 테스트를 생성하면 가정이 유효한지 확인할 수 있습니다. 귀하의 이해가 정확할 수도 있지만 테스트를 통해 이를 검증합니다.
- 안전망:실패한 테스트를 작성하여 향후 회귀를 방지하는 안전망을 구축합니다. 이는 기능에 대한 사양 역할을 하며 의도하지 않은 부작용을 포착하는 데 도움이 됩니다.
- 증분 개발:TDD는 점진적 개발을 장려합니다. 각각의 새로운 기능은 테스트 실패부터 실제 구현까지 명확한 진행 과정을 통해 단계별로 구축됩니다. 이 단계를 건너뛰면 진행이 중단됩니다.
- 문서:실패한 테스트는 예상되는 기능 동작을 문서화합니다. 이 문서는 특히 나중에 코드를 다시 방문할 때 귀하와 귀하의 팀에게 유용합니다. 제품 관리자와 QA를 위해 모든 테스트 코드를 나열하여 보고서를 생성하는 시스템이 있다는 점을 항상 기억하세요. 이러한 보고서에는 귀하가 제품에서 발견한 세부 사항이 노출되므로 귀하가 해당 요점을 완전히 이해했다는 점을 설득하도록 노력하십시오.
TDD를 활용한 개발 방법
TDD는 소프트웨어 설계 및 개발을 추진하기 위해 자동화된 테스트 작성의 중요성을 강조합니다. 이로 인해 더 안정적이고 유지 관리가 용이하며 시간이 지남에 따라 변경하기 쉬운 코드가 탄생합니다.
하지만 어떻게 실천할 수 있을까요? 시도해 보고 점차 익숙해지는 것입니다.
실제 세계에서 TDD를 어떻게 사용할 수 있는지 보여주기 위해 새로운 기능을 개발하면서 TDD를 시도해 보겠습니다.
글꼴 크기 자동 설정 기능을 구현할 예정입니다.
뉴스 앱이 있고 사용자는 뉴스 피드의 자동 스크롤 속도를 설정할 수 있습니다.
사용자 프로필 페이지에 설정된 스크롤 속도에 따라 화면 글꼴 크기를 조정하는 기능을 구현하려고 합니다.
사용자가 스크롤 속도를 0에서 1로 설정하면 글꼴 크기가 1.3씩 증가해야 합니다.
스크롤 속도가 1 이상 증가하면 글꼴 크기가 1.2만큼 증가합니다.
이 기능을 사용하면 사용자가 뉴스를 읽는 동안 더 나은 경험을 할 수 있습니다.
또한 이 GitHub 저장소에서 탐색한 코드도 공유했습니다.
자유롭게 복제하여 사용해 보세요.
개발이 진행되는 동안 다음 단계를 따르십시오. 이를 통해 TDD의 맥락에서 기술과 사고방식을 실질적으로 파악하는 데 도움이 됩니다.
그럼 Android Studio를 열고 새 프로젝트를 생성해 보세요.
글꼴 크기 자동 설정 기능 데이터 흐름도
Android 앱 샘플 기능의 DFD
위는 결국 데이터 흐름이 어떤 모습이어야 하는지에 대한 흐름도입니다.
각 매력적인 구성요소의 개요는 다음과 같습니다.
AutoScrollSettingsUseCase 클래스는 FontRatio을 계산하고 저장하는 논리를 처리합니다. 선택한 스크롤 속도를 기준으로 합니다.
이 사용 사례는 UserRepository에 종속됩니다. FontRatio 저장 중 가치.
UserRepository에서 , FontRatio를 저장하고 검색하는 방법이 있습니다. Storage를 사용한 값 메커니즘. 새로운 FontRatio가 나올 때마다 저장소로 전송되면 모든 Observable은 최신 값의 방출을 받게 됩니다.
UserProfileViewModel에서 , AutoScrollSettingsUseCase의 인스턴스가 있습니다. 사용자가 스크롤 속도를 업데이트할 때마다 호출됩니다. 그러면 FontRatio가 다시 계산됩니다. 저장소를 통해 저장됩니다.
사용자가 원하는 스크롤 속도를 입력할 수 있도록 사용자 설정 섹션에 필요한 UI 구성 요소가 있습니다. 이는 NumberPicker과 같은 표준 Android UI 요소를 사용하여 수행할 수 있습니다. 또는 사용자 정의 UI 구성요소(이 부분에 대해서는 논의하지 않습니다.)
이것은 간단한 기능에 대한 분석이며, 이를 거쳐갈 때마다 단계와 최종 결과가 무엇인지 더욱 명확해집니다. 변경 사항을 적용하는 것이 중요합니다.
테스트 작성 방법
첫 번째 단계는 항상 테스트 클래스 자체를 생성하는 것입니다. 이 경우 최소한 다음과 같은 테스트 클래스가 있습니다:
UserRepositoryTestAutoScrollSettingTestUserSettingsViewModelTest
저는 ViewModel 부분부터 시작하는 것을 선호합니다.
ViewModel은 수명 주기 변경(예:전경, 배경, 집중)을 피하는 Android 아키텍처 구성요소입니다. 따라서 상태를 저장하기에 좋은 장소입니다.
unitTest 안에 테스트 파일을 만들어 보겠습니다. 실제 기능 코드와 동일한 패키지 경로를 따르는 소스 코드 디렉터리입니다.
실제로 TDD는 레거시 작업 개발과 유사하지 않습니다.
TDD에서는 IDE를 사용하여 파일 및 속성(필드) 생성 프로세스를 강화합니다. 하지만 우리는 테스트 파일을 수동으로 생성합니다! 몇 번 시도해 보면 IDE의 이러한 측면이 편리해질 것입니다.
코드 구조(패키지)를 생성한 다음 패키지를 마우스 오른쪽 버튼으로 클릭하고 Class를 선택하여 테스트 클래스를 생성합니다. 유형.
설명이 포함된 이름을 선택하십시오. 아직 지정하지 않았다면 규칙을 따라야 할 수도 있습니다.
예:xxx is tested for , 여기서 xxx 테스트된 구성 요소의 이름입니다.
IDE를 사용하여 파일 만들기
이제 빈 테스트를 만들어 보겠습니다. 가능한 한 광범위하게 노력하십시오.
다이어그램에 따르면 이 기능에 대한 논리는 많지 않습니다.
단위 테스트 함수를 작성하는 데는 두 가지 주요 전략이 있습니다:
- AAA
- 주어진/언제/그때
@Test fun `strategy A`(){
// Arrange
// Act
// Assert
}
@Test fun `strategy B`(){
// Given
// When
// Then
}
하나를 선택하여 모든 테스트에 적용해 보세요.
개념은 동일합니다. 테스트 코드를 클러스터링하여 쉽게 읽고 유지 관리할 수 있습니다.
현재 제가 가지고 있는 것은 다음과 같습니다:
class UserProfileViewModel is tested for` {
// Unimplemented Class
val viewModel = UserProfileViewModel()
@Test
fun Font Ratio is fetched from data source`(){}
@Test
fun `Scroll Speed update is called so fontSize calculations are triggered`() {}
@Test
fun `Font Ratio is updated with new emissions from data source`() {}
}
테스트를 진행해 보세요!
테스트 대상 누락으로 인한 테스트 실패
실패하고 있어요. 실제로 빌드가 실패했습니다. 테스트가 아닙니다.
축하합니다! 우리는 방금 TDD 사이클의 첫 번째 단계에 이르렀습니다:
TDD 주기의 첫 번째 단계
ViewModel이 아직 존재하지 않기 때문에 빨간색으로 표시됩니다.
이제 ViewModel의 인스턴스를 만들어 보겠습니다.
그래서 우리는 누락된 클래스나 구현되지 않은 코드를 생성하기 위해 IDE를 사용합니다.
이 대화 상자 팝업을 만들려면 구현되지 않은 부분으로 포인터를 이동하고 Option + Return(macOS의 경우)을 누릅니다.
그런 다음 제공된 옵션을 따르세요:
TDD 작업:UnitTest 파일을 통해 대상 파일 생성
새 파일의 올바른 대상 선택
이제 테스트를 다시 실행해 보겠습니다(마지막 단계):
테스트 통과 표시
예! 통과되었습니다.
이 테스트에는 본문이 비어 있으며 아무것도 테스트하지 않는다는 점에 유의하세요! 맞는 말이고 괜찮습니다.
DFD 다이어그램의 모든 구성 요소에 대해 모든 테스트 클래스(여전히 빈 테스트 본문 포함)를 계속 생성해야 합니다. 이 내용은 기사 시작 부분에서 공유했습니다.
이는 기능을 구현하기 전에 기능을 더 명확하게 이해하는 데 도움이 됩니다.
결국 우리는 다룰 일반 시나리오와 단위 테스트를 포함하는 약 3-4개의 테스트 클래스를 갖게 될 것입니다.
다음과 같이 보일 것입니다:
최소 빈 테스트 케이스
그 중 하나를 예시로 구현해 보겠습니다.
하지만 그 전에 이 기능의 UI 및 도메인 데이터 모델을 사용하여 작업해야 합니다.
따라서 데이터를 이동할 수 있도록 필요한 데이터 클래스를 미리 만들어 보겠습니다.
ProfileViewModel로 돌아가기 테스트 클래스에는 빈 단위 테스트 함수가 있습니다. 그걸 구현해 봅시다.
여기서 핵심은 테스트를 주의 깊게 읽고 어설션이나 추가 구현을 피하는 것입니다.
요구사항만 구현이 허용됩니다.
이 경우 이전에 생성된 데이터 소스(UserRepository)에 연결된 데이터 스트림이 필요합니다. ).
잊지 마세요:먼저 실패한 테스트가 필요합니다.
내부 몸체 구현
테스트 함수 본문 내부에서 구현되지 않은 부분(빨간색 글꼴로 표시)을 확인하세요.
이제 코드를 구현한 다음 리팩토링하여 통과하도록 하겠습니다.
여기서는 MockK 라이브러리를 사용하여 클래스와 객체를 모의하고 Turbine을 사용하여 Flow 스트림을 테스트하고 있습니다.
익숙하지 않더라도 당황하지 마세요! 공식 웹페이지를 확인하고 사용해 보세요.
먼저 종속성을 생성하고 명명된 인수를 사용하여 ViewModel에 추가해 보겠습니다. 명명된 인수는 IDE를 통해 매개변수를 생성하여 테스트 코드를 통해 적절한 이름을 도입하는 데 도움이 됩니다.
IDE 대화 상자를 사용하여 누락된 매개변수 생성
FontRatio에도 동일한 작업을 수행하세요. 저장소 내부의 변수입니다.
결국 최종 테스트 코드는 아래 코드와 같을 수 있습니다.
class `UserProfileViewModel is tested for` {
init {
Dispatchers.setMain(Dispatchers.Unconfined)
}
val mockUserRepository = mockk<UserRepository>()
@Test
fun `Font Ratio is fetched from data source`() = runTest {
// Given
val expectedRatio = 2.0f
every { mockUserRepository.fontRatio } returns flowOf(FontRatioUiModel(expectedRatio))
val viewModel = UserProfileViewModel(userRepository = mockUserRepository)
// When
viewModel.fontRatio.test {
val fromDataSource = expectItem()
// Then
assertEquals(/* expected = */ expectedRatio, /* actual = */ fromDataSource.fontRatio)
}
}
...
}
여기서는 ViewModel 또는 Repository의 내부 부분을 구현하지 않습니다.
테스트 본문에서 오류를 제거하기 위해 누락된 부분을 생성합니다.
우리는 다음 반복에서 이러한 세부 사항을 구현할 것입니다.
지금 테스트를 실행해 보세요.
물론 FontRatio를 구현하지 않았기 때문에 실패할 것입니다. ProfileViewModel 내부 .
이제 ViewModel을 리팩터링하여 테스트를 통과합니다(최소 구현).
이 경우 상태 흐름을 저장소에 연결하기만 하면 됩니다. 이전 반복에서 이미 종속성으로 추가했습니다.
최종 코드는 다음과 같습니다:
class UserProfileViewModel(
userRepository: UserRepository
) : ViewModel() {
val fontRatio: StateFlow<FontRatioUiModel> = userRepository.fontRatio.stateIn(
initialValue = FontRatioUiModel(DEFAULT_FONT_RATIO),
scope = viewModelScope,
started = SharingStarted.Lazily
)
companion object {
private const val DEFAULT_FONT_RATIO = 1.0f
}
}
테스트를 다시 실행하고 Boom! 통과!
메인 코드를 최소한으로 구현한 후 테스트 통과
이 단위 테스트를 통해 UserProfileViewModel의 주요 부분을 구현했습니다. 구성 요소. 하지만 꼭 필요한 부분만. 나머지 테스트 케이스에도 동일한 작업을 수행합니다.
일반 단위 테스트(빠르게 실행 및 통과)처럼 이러한 테스트 사례를 처리하지 마십시오.
먼저 기술 및 제품 요구 사항을 이해하는 데 시간을 투자하십시오. 그런 다음 계획을 롤아웃하고 구현을 시작합니다. 몇 번 시도해 보면 TDD 방식으로 생각하는 것이 더 쉬울 것입니다.
소스 코드
이 프로젝트의 소스 코드와 저장소는 내 GitHub 페이지에서 확인할 수 있습니다.
자유롭게 확인하고 다음 단계를 완료하세요. 여러 분기에서 반복을 분리하여 자신의 구현과 비교할 수 있습니다.
결론
따라서 테스트 주도 개발(TDD)에 대해 자세히 알아보고 그 내용을 살펴본 후, 이것이 게임 체인저라고 말씀드리고 싶습니다!
자세히 설명해 드리겠습니다.
핵심 사항:
- TDD는 실제 코드를 작성하기 전에 테스트를 작성하는 것입니다. 처음에는 조금 이상하게 들릴 수도 있지만 제 말을 믿으세요. 놀라운 효과가 있습니다.
- TDD 프로세스는 간단한 주기를 따릅니다. 즉, 실패한 테스트를 작성하고 해당 테스트를 통과하도록 코드를 구현한 다음 모든 것이 원활하게 실행되도록 필요에 따라 리팩터링합니다.
- 자동화된 테스트를 강조함으로써 TDD는 견고하고 유지 관리가 가능하며 시간이 지남에 따라 적응할 수 있는 소프트웨어를 설계하고 개발하는 데 도움이 됩니다.
테스트 클래스를 생성하고 빈 테스트 함수를 작성하는 것으로 시작했지만 이제 완료하는 것이 여러분의 임무입니다(아니면 공유 저장소로 바로 이동할 수도 있습니다 :)).
아무것도 하지 않는 테스트가 있다는 것이 다소 이상하게 보일 수도 있지만, 이는 모두 계획의 일부입니다.
다음으로 데이터 흐름 차트를 기반으로 기능이 수행해야 할 작업에 대한 명확한 계획을 세웠습니다. 이는 구현을 시작하기 전에 요구 사항을 이해하는 데 도움이 되었습니다.
계획을 세우고 필요한 구성 요소(이 경우 ViewModel)를 구현하기 시작하여 테스트가 먼저 실패했는지 확인했습니다. 맞습니다. 테스트 실패는 실제로 TDD에서는 좋은 일입니다!
점차적으로 우리는 UserAutoScrollSettingsUseCase을 설정하는 것처럼 여러 부분을 서로 연결했습니다. 자동 스크롤 속도에 따라 글꼴 크기 계산을 처리하는 클래스입니다(프로젝트 저장소 확인).
또한 UI 구성 요소를 다루어 사용자가 원하는 스크롤 속도를 입력할 수 있도록 하고 이에 따라 글꼴 크기가 조정되었는지 확인했습니다(프로젝트 저장소 확인).
프로세스 전반에 걸쳐 우리는 테스트를 통과하는 데 필요한 사항에 중점을 두고 코드를 깔끔하고 단순하게 유지했습니다. 불필요한 복잡성은 없습니다!
결국 우리는 "글꼴 크기 자동 설정" 기능을 가동하고 테스트를 성공적으로 통과했습니다.
기억하세요. TDD는 테스트를 서두르거나 미친 듯이 코딩하는 것이 아닙니다. 이는 개발 과정에서 신중하고 사려 깊은 노력을 기울여 장기적으로 큰 성과를 거두는 것입니다.
따라서 소프트웨어 개발 게임의 수준을 높이고 싶다면 TDD를 시도해 보세요! 이는 코드를 더욱 견고하게 만들고 버그를 줄이며 전반적으로 더 나은 개발자가 될 수 있게 해주는 강력한 접근 방식입니다.
제가 공유한 내용은 우리 팀에서 일하는 방식과 그것이 우리에게 도움이 된다는 것입니다. 그러나 이것이 모든 팀/회사에 완벽한 솔루션은 아닙니다. 자신의 것인지 아닌지 확인해야 합니다. 이 솔루션을 개선할 수 있다고 생각하시면 알려주세요.
즐거운 코딩하세요! 🚀
무료로 코딩을 배우세요. freeCodeCamp의 오픈 소스 커리큘럼은 40,000명 이상의 사람들이 개발자로 취업하는 데 도움을 주었습니다. 시작하세요