프로젝트를 진행할 때 해당 프로젝트가 어떠한 프로젝트이며, 어떤 기능을 담고 있는지 기술한다.
너무 세세한 부분까지 정리하기보다 구현해야 할 기능 목록을 정리하는 데 집중한다.
정상적인 경우도 중요하지만, 예외적인 상황도 기능 목록에 정리한다.
기능을 구현하면서 문서를 계속 업데이트한다. 죽은 문서가 아니라 살아있는 문서를 만들기 위해 노력한다.
문자열, 숫자 등의 값을 하드 코딩하지 마라. 상수를 만들고 이름을 부여해 이 변수의 역할이 무엇인지 의도를 드러내라.
companion object{
const val CONST_VAL = 100
}
코틀린에서의 상수는 대문자 스네이크 케이스로 작성하며 일반적으로 companion object 안에 상수로 선언하게 된다.
여기서 const
는 컴파일을 진행하며 결정되는 상수이며, 런타임에 할당되는 val 와 달리 컴파일 시간 동안 할당이 되어야 한다.
즉, const 는 함수나 어떤 클래스의 생성자에게도 결코 할당 될 수 없고 오직 문자열이나 기본 자료형으로 할당되어야 한다.
클래스는 프로퍼티, init 블록, 부 생성자, 메서드, 동반 객체 순으로 작성한다.
class StudentClass(private val name: String) {
val students: Int // 프로퍼티
init {
} // init 블록
StudentClass(name: String, subname: String) {
} // 부 생성자
private fun addStudents(): Unit { } // 메서드
companion object {
// ..
} // 동반 객체
}
변수 이름에 자료형, 자료 구조 등을 사용하지 마라.
val carNameList: String = Console.readLine()
val listString: List<String> = carNameList.split(",")
함수 길이가 길어진다면 한 함수에서 여러 일을 하려고 하는 경우일 가능성이 높다. 아래와 같이 한 함수에서 안내 문구 출력, 사용자 입력, 유효값 검증 등 여러 일을 하고 있다면 이를 적절하게 분리한다.
fun userInput(): List<Int> {
println("숫자를 입력해 주세요: ")
val userInput = Console.readLine().trim()
val user = mutableListOf<Int>()
for (c in userInput) {
user.add(c.digitToInt())
}
require(user.size == 3) { "[ERROR] 숫자가 잘못된 형식입니다." }
return user
}
단지 기능을 점검하기 위한 목적으로 테스트를 작성하는 것은 아니다.
테스트를 작성하는 과정을 통해서 나의 코드에 대해 빠르게 피드백을 받을 수 있을 뿐만 아니라 학습 도구로도 활용할 수 있다.
이런 경험을 통해 테스트에 대해 어떤 유용함을 느꼈는지 알아본다.
테스트의 중요한 목적 중 하나는 내가 작성하는 코드에 대해 빠르게 피드백을 받는 것이다. 시작부터 큰 단위의 테스트를 만들게 된다면 작성한 코드에 대한 피드백을 받기까지 많은 시간이 걸린다.
그래서 문제를 작게 나누고, 그 중 핵심 기능에 가까운 부분부터 작게 테스트를 만들어 나간다.
큰 단위의 테스트
숫자 야구 게임을 시작해서 사용자가 숫자를 입력하면, 컴퓨터 숫자와 비교하여 그 결과를 알려준다.
작은 단위의 테스트
사용자의 숫자가 컴퓨터의 숫자와 하나도 일치하지 않으면 낫싱을 출력한다.
사용자의 숫자가 컴퓨터의 숫자와 1개는 일치하고, 위치가 다르면 1볼을 출력한다.
테스트를 작성하기 위해서는 @Test
어노테이션을 붙여 테스트 함수로 만들어야 한다.
@Test
fun `"1,2"을 , 로 split 했을 때 1과 2로 잘 분리되는지 확인하는 학습 테스트를 구현한다`() {
val input = "1,2"
assertThat(input.split(',')).containsAll(listOf("1", "2"))
}
@Test
fun `"1"을 , 로 split 했을 때 1만을 포함하는 배열이 반환되는지에 대한 학습 테스트를 구현한다`() {
val input = "1"
assertThat(input.split(',')).containsExactly("1")
}
@Test
fun `(1,2) 값이 주어졌을 때 String의 substring() 메소드를 활용해 () 을 제거하고 "1,2"를 반환하도록 구현한다`() {
val input = "(1,2)"
assertThat(input.substring(1, 4)).isEqualTo("1,2")
}
@DisplayName
어노테이션을 활용하여 보여지는 이름을 수정할 수 있다.
@BeforeEach
,@AfterEach
어노테이션을 통해 해당 테스트가 진행되기 전, 후에 작업을 정의할 수 있다.
@ParameterizedTest
를 통해 해당 파라미터를 통해 해당 값들을 테스트할 수 있다.
class SetTest {
private lateinit var numbers: MutableSet<Int>
@BeforeEach
fun setUp() {
numbers = HashSet()
numbers.add(1)
numbers.add(1)
numbers.add(2)
numbers.add(3)
}
@Test
@DisplayName("Set의 size() 메소드를 활용해 Set의 크기를 확인하는 학습테스트를 구현한다.")
fun setSizeTest() {
assertThat(numbers.size).isEqualTo(4)
}
@ParameterizedTest
@ValueSource(ints = [1, 2, 3])
@DisplayName("1, 2, 3의 값이 존재하는지를 확인한다.")
fun containsTest(value: Int) {
assertThat(numbers).contains(value)
// or
assertThat(numbers.contains(value)).isEqualTo(true)
// or
assertTrue(numbers.contains(value))
}
}
여러개의 인자를 통해 테스트하고 싶다면 @CsvSource
를 활용할 수 있다.
@ParameterizedTest
@CsvSource({"test,TEST", "tEst,TEST", "Java,JAVA"})
void toUpperCase_ShouldGenerateTheExpectedUppercaseValue(String input, String expected) {
String actualValue = input.toUpperCase();
assertEquals(expected, actualValue);
}
자주 발생하는 오류에 대해서는 다음과 같은 메서드를 활용할 수 있다.
// 다음과 동일
assertThrows<IllegalArgumentException> { }
assertThat()의 확장함수로 정말로 많은 테스트를 제공하고 있다. 이를 활용하여 가독성 높은 테스트를 만들어보자.
야구 게임이라는 간단한 동작을 1주일이나 미션으로 진행하라는 것이 어떤 의미일까 생각다. 그 의미를 추가된 요구 사항에서 찾아 볼 수 있었다.
요구사항
- indent(인덴트, 들여쓰기) depth를 3이 넘지 않도록 구현한다. 2까지만 허용한다. 함수(또는 메서드)가 한 가지 일만 하도록 최대한 작게 만들어라
- JUnit 5와 AssertJ를 이용하여 본인이 정리한 기능 목록이 정상 동작함을 테스트 코드로 확인한다.
첫 번째
함수를 작게 만들어서 각각의 함수 즉 기능에 대한 책임을 줄이는 것
이러한 소스는 가독성을 증가시키고, 나중에 함수를 리팩토링할 때나 기능을 변경할 때 유용하기 때문이다.
두 번째
단위테스트의 작성에 있어 메소드의 기능이 한 가지 일만 수행하는 것이 유리하다.
테스트 코드를 만들면서 테스트케이스를 작성할 때 함수가 여러 가지 일을 동시에 한다면 테스트가 특정 상황에서만 작동하며, 후에 코드가 디벨롭됬을 때 작성된 이런 테스트는 의미가 없어 질 것이다.
간단한 야구 게임이었지만 익숙하지 않은 테스트코드의 작성에 대해 잘 알 수 있었다.