개발 방법론
전통적인 개발 방법론은 디자인 -> 코드 -> 테스트 순으로 진행하며, 필요시에 테스트를 진행한다.
코드가 잘 동작하는지 검증하고 보장할 수 있는 테스트는 적은 코드에서는 큰 의미가 없을 수도 있으나,
여러명이 같이 코드를 수정하거나 기존에 있던 코드를 수정할 때는 굉장히 중요해졌다.
이러한 이유로 개발 이후 테스트의 커버리지를 높이기 위해 힘써왔으며, 아예 테스트를 먼저 작성함으로써 테스트에 코드를 맞추는 새로운 방법론이 나오게 되었다.
TDD(Test Driven Development)
테스트를 작성 후에 이를 참으로 만드는 코드를 작성하는 과정을 통해 개발하는 방법론으로
Test -> Code -> Refactor 단계의 반복으로 이루어져있다.
TDD의 장점으로는 다음과 같다.
- 테스트와 개발 해야할 목록을 명확히 할 수 있음
- 점진적으로 코드를 증가시키면서 개발이 가능함
- 테스트 패스를 통해 코드에 대한 객관적인 검증이 가능함
TDD를 통해 FizzBuzz 문제를 코딩해보자.
FizzBuzz 문제란?
- 1~100 사이의 수 중 3의 배수이면 Fizz, 5의 배수이면 Buzz, 3 & 5 의 배수이면 FizzBuzz 출력
개발 프로세스
- 수행할 테스트에 대해서 주석(자연어)로 미리 작성
- 실패 테스트 작성
- 테스트를 통과하는 코드 작성
- 코드 리팩토링
- 2번으로 이동
위의 개발 프로세스를 따라서 FizzBuzz 문제를 코딩해본다.
첫번째로, 수행해야할 테스트에 대해서 주석으로 작성한다.
의도적으로 기능을 동작하는지 뿐만아니라, 원하는 값이 아닐때에도 동작하는 예외 테스트까지 포함하여 작성해야한다.
package com.luv2code.tdd;
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
class FizzBuzzTest {
// IF number is divisible by 3, print Fizz
// IF number is divisible by 5, print Buzz
// IF number is divisible by 3 and 5, print FizzBuzz
// IF number is NOT divisible by 3 or 5, then print the number
}
두 번째로, 주석 매칭되는 테스트 코드를 작성 후 실패를 확인한다.
FizzBuzz 클래스의 compute 메소드는 아직 작성되지 않은 값으로, 입력 값 3에 대해서 아무런 동작을 하지 않기 때문에 에러가 발생된다.
package com.luv2code.tdd;
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
class FizzBuzzTest {
// IF number is divisible by 3, print Fizz
@DisplayName("Divisible by Three")
@Test
void testForDivisibleByThree() {
String expected = "Fizz";
assertEquals(expected, FizzBuzz.compute(3), "Should return Fizz");
}
// IF number is divisible by 5, print Buzz
// IF number is divisible by 3 and 5, print FizzBuzz
// IF number is NOT divisible by 3 or 5, then print the number
}
세 번째로, 테스트를 통하는 코드를 작성한다.
입력값이 3으로 나누어진다면, Fizz를 출력하는 코드를 작성한다.
메소드의 return 값이 String이기 때문에 3의 배수가 아니라면 숫자를 String으로 출력하였다.
코드 작성 후에는 개별 테스트와 이전 테스트들이 모두 돌아가는지 확인해야한다.
package com.luv2code.tdd;
class FizzBuzz {
public static String compute(int i) {
if (i % 3 == 0) {
return "Fizz";
}
return Integer.toString(i);
}
}
네번째로는, 코드를 리팩토링 한다.
현재 작성된 코드가 너무 짧고, 첫 부분이기 때문에 리팩토링을 할 수 있는 부분이 없어서 생략한다.
다섯번째는, 다시 주석에 매칭되는 테스트 코드를 작성하여 두번째 과정부터 다시 진행한다.
위의 과정을 반복하면 FizzBuzz 문제에 대해서 다음과 같은 소스 코드와 테스트 코드를 작성할 수 있다.
소스코드
package com.luv2code.tdd;
class FizzBuzz {
// IF number is divisible by 3, print Fizz
// IF number is divisible by 5, print Buzz
// IF number is divisible by 3 and 5, print FizzBuzz
// IF number is NOT divisible by 3 or 5, then print the number
public static String compute(int i) {
StringBuilder result = new StringBuilder();
if(i % 3 == 0){
result.append("Fizz");
}
if(i % 5 == 0) {
result.append("Buzz");
}
if(result.length()==0) {
result.append(i);
}
return result.toString();
}
/*
public static String compute(int i) {
if( (i % 3 == 0) && (i % 5 == 0)) {
return "FizzBuzz";
}
else if (i % 3 == 0) {
return "Fizz";
}
else if (i % 5 == 0) {
return "Buzz";
}
else {
return Integer.toString(i);
}
}
*/
}
테스트 코드
package com.luv2code.tdd;
import static org.junit.jupiter.api.Assertions.*;
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
class FizzBuzzTest {
// IF number is divisible by 3, print Fizz
@DisplayName("Divisible by Three")
@Test
@Order(1)
void testForDivisibleByThree() {
String expected = "Fizz";
assertEquals(expected, FizzBuzz.compute(3), "Should return Fizz");
}
// IF number is divisible by 5, print Buzz
@DisplayName("Divisible by Five")
@Test
@Order(2)
void testForDivisibleByFive() {
String expected = "Buzz";
assertEquals(expected, FizzBuzz.compute(5), "Should return Buzz");
}
// IF number is divisible by 3 and 5, print FizzBuzz
@DisplayName("Divisible by Three and Five")
@Test
@Order(3)
void testForDivisibleByThreeAndFive() {
String expected = "FizzBuzz";
assertEquals(expected, FizzBuzz.compute(15), "Should return FizzBuzz");
}
// IF number is NOT divisible by 3 or 5, then print the number
@DisplayName("Not Divisible by Three or Five")
@Test
@Order(4)
void testForNotDivisibleByThreeOrFive() {
String expected = "1";
assertEquals(expected, FizzBuzz.compute(1), "Should return 1");
}
}
추가적으로 이러한 테스트를 수행할 때 매개변수를 일일이 테스트 메서드에 넣지 않고,
@ParameterizedTest 에너테이션을 통해 매개화하여 전달할 수 있다.
@ParameterizedTest
매개화된 테스트를 지원하기 위해서 JUnit에서 제공하는 에너테이션으로 다음 에너테이션과 함께 사용하여 매개화된 테스트 값을 전달할 수 있다.
에너테이션 | 설명 | 사용 방법 |
@ValueSource | 배열 값을 사용하여 값을 전달 (자료형 : Strings,ints,doubles,floats) |
@ParameterizedTest @ValueSource(ints = {1, 2, 4, 6, 7, 8, 11}) |
@CsvSource | CSV String 배열을 사용 | @ParameterizedTest @CsvSource(value = {"1,1", "2,2", "3,Fizz", "4,4", "5,Buzz"}) |
@CsvFileSource | CSV File을 읽어서 사용 | @ParameterizedTest(name = "value={0}, expected={1}") @CsvFileSource(resources = "/small-test-data.csv") |
@MethodSource | 값을 제공하는 Custom method 사용 (return 타입 - Stream<Arguments>) |
@ParameterizedTest @MethodSource("generateData") |
MethodSource를 사용하는 경우 커스텀 메소드를 생성해야하며, 아래와 같이 작성하여 사용한다.
static Stream<Arguments> generateData() {
return Stream.of(
Arguments.of(1,"1"),
Arguments.of(2,"2"),
Arguments.of(3,"Fizz")
...
)
}