티스토리 뷰
💡 넥스트 스탭의 TDD, 클린 코드 with Kotlin 8기 강의를 수강하며 정리한 내용입니다.
목차
배경
최근 넥스트 스탭의 TDD, 클린 코드 with Kotlin 8기를 수강하면서 단위 테스트와 테스트 코드 작성에 새로운 관점을 얻어가고 있다. 덕분에 평소 고민해보지 못했던 주제에 대해서 생각해볼 기회를 얻고 있다. 이번에는 그 중 단위 테스트 작성을 위한 테스트 코드의 가독성에 대한 리뷰 내용이다.
내용
4주차 미션을 진행하면서 테스트 코드와 가독성에 대한 피드백을 받았다. 테스트 코드에 일련의 로직이 들어가면 가독성과 유지보수에 어려움이 발생하고, 다른 개발자가 봤을때 정책과 로직 파악에 어렵다는 것이다.
테스트 코드는 한번에 내려가며 읽고 끝낼 수 있는 것이 좋습니다.
테스트 코드에 일련의 로직이 들어가면 가독성과 유지보수성에 있어 불편한 점이 생겨날 수 있습니다. 따라서 테스트 코드에서 if-else, for-loop와 같은 로직은 제외하고 필요한 부분만 검증할 수 있도록 작성해보시면 좋을것 같습니다.
평소 테스트 코드 작성이 익숙치 않아, 작성 자체에만 급급했는데 리뷰 내용 덕분에 새로운 인사이트를 얻고, 이에 대해 고민해볼 수 있었다.
위와 같은 피드백을 받았던 예시 코드에 대해 살펴보면 다음과 같다.
class MapTest {
@ParameterizedTest
@MethodSource("mapSizes")
fun `지뢰찾기 지도 생성을 테스트한다`(point: Pair<Height, Width>) {
val heightSize = point.first.size
val widthSize = point.second.size
val map = generateTestMap(heightSize, widthSize)
map.grid.points.rows shouldHaveSize heightSize
map.grid.points.rows
.forEach { it.columns shouldHaveSize widthSize }
for (rowIndex in 0 until heightSize) {
val expectedRow =
List(widthSize) { colIndex ->
Point(point = Pair(rowIndex.toIndex(), colIndex.toIndex()), element = Cell)
}
map.grid.points.rows[rowIndex]
.columns shouldContainExactly expectedRow
}
}
private fun generateTestMap(
heightSize: Int,
widthSize: Int,
): Map = Map.create(height = Height(size = heightSize), width = Width(size = widthSize))
companion object {
@JvmStatic
fun mapSizes() =
listOf(
Pair(Height(size = 3), Width(size = 4)),
Pair(Height(size = 5), Width(size = 5)),
Pair(Height(size = 3), Width(size = 2)),
)
}
}
피드백 내용과 같이, 테스트 코드에 복잡한 람다 표현식과 for-loop가 혼재되어있다. 때문에 가독성이 떨어지고, 한 눈에 무엇을 검증하기 위한 테스트 코드인지 파악하기 어렵다.
또한 위 테스트 코드를 살펴보았을때 `지뢰 찾기 지도 생성 테스트()`는 지도의 크기와 원소 초기값을 검증하고 있다. 즉, 한 번에 여러 내용을 검증하고 있고 이는 결국 테스트 코드 로직 파악에 어려움으로 이어진다.
테스트 코드의 가치는 꼭 필요한 내용만 남겼을때 더욱 두드러진다. 한 번에 여러 내용을 검증하려고 하거나 불필요한 내용까지 검증하다보면 오히려 그 가치가 떨어진다.
개선된 코드
위 피드백 내용에 대해 고민해보았을때 위 테스트 코드는 다음과 같이 개선할 수 있다.
class MapTest {
@ParameterizedTest
@MethodSource("mapSizes")
fun `지뢰찾기 지도 생성 크기를 검증한다`(point: Pair<Height, Width>) {
val heightSize = point.first.size
val widthSize = point.second.size
val map = generateTestMap(heightSize, widthSize)
map.grid.points.rows shouldHaveSize heightSize
map.grid.points.rows
.forAll { it.columns shouldHaveSize widthSize }
}
@ParameterizedTest
@MethodSource("mapSizes")
fun `모든 초기 요소가 Cell인지 검증한다`(point: Pair<Height, Width>) {
val heightSize = point.first.size
val widthSize = point.second.size
val map = generateTestMap(heightSize, widthSize)
map.grid.points.rows
.flatMap { it.columns }
.forAll { it.element shouldBe Cell }
}
private fun generateTestMap(
heightSize: Int,
widthSize: Int,
): Map = Map.create(height = Height(size = heightSize), width = Width(size = widthSize))
companion object {
@JvmStatic
fun mapSizes() =
listOf(
Pair(Height(size = 3), Width(size = 4)),
Pair(Height(size = 5), Width(size = 5)),
Pair(Height(size = 3), Width(size = 2)),
)
}
}
각 테스트 메서드는 한 번에 한 가지(꼭 필요한 부분)만 검증하고, for loop 대신에 Stream을 활용하여 가독성을 향상시켰다. 처음 받았던 피드백과 같이 한 눈에 읽으면서 내려갈 수 있도록 개선하였다.
이와 같이 테스트 코드 작성 자체에 급급하지말고, 테스트 코드 작성의 가치를 더욱 활용하기 위해서는 가독성도 꼭 필요한 부분임을 느꼈다. 팀 내 다른 개발자 혹은 신규 입사자를 위해서 테스트 코드 작성시에도 이해하기 쉬운 코드와 꼭 필요한 로직만 남겨두어 테스트 코드의 가치를 십분 활용하는 것이 중요하다.
'테스트코드' 카테고리의 다른 글
[단위테스트] 테스트 코드를 작성하는 이유와 작성 방법 (1) | 2024.11.18 |
---|