이번 시간은 테스트 우선 개발에 대한 이야기다
예제 코드는 이전 시간에 했단 단위 테스트를 개선하면서 진행된다.
강의에서 실습을 자바스크립트로 하지만 이번에도 자바로 재해석하면서 진행하겠다.
📖 원본이 궁금하면 이규원 님의 TDD 수강하는 걸 추천드립니다. 👍
테스트 우선 개발
이전과는 반대로 테스트 코드를 먼저 작성하고 운영 코드를 구현하는 개발 절차로
코딩의 수단보다 목적에 집중하게 도와준다.
테스트 코드
어떤 조건에 대하여 만족하는 지 검증하는 것으로 이전 시간에 알아봤던 코드 기능 명세와 비슷하다
정리하자면 다음과 같다.
- 가시적이고 구체적인 목표 : 프로그래머에게 코드를 작성하는 과정에 앞서서 어떤 목표를 도달하기 위한 명확한 이해를 강요
- 자가검증 : 목표가 달성되었는지 검증이 가능하다. 언제든지 쉽게 확인할 수 있다.
- 반복 실행 : 새로운 테스트를 추가해도 이전 테스트에 대한 결과를 확인할 수 있다.
- 클라이언트 : 운영코드 API의 클라이언트가 된다.
테스트 우선 개발의 장점
- 명확하고 검증 가능한 목표를 설정한 후 목표를 달성 : 코드를 작성하는 목표를 명확하고 검증하게 된다. 자가검증을 달성하는 행위다.
- 프로세스가 코딩에 앞선 목표 설정을 강요 : 어떤 목표로 작성해야 하는지 이해를 강요한다.
- 프로그래머는 자신이 풀어야 할 문제를 구체적으로 이해 : 작성하는 테스트 코드를 통해 어떤 문제를 풀어야 할지 이해하게 된다.
실습
이전 시간에 했던 단위 테스트 refineTest로 계속 진행하겠다.
public String refineTest(String s) {
return s.replace(" ", " ").replace(" ", " ");
}
@ParameterizedTest
@CsvSource(value = {"hello world", "hello world", "hello world"})
@DisplayName("배열로 테스트")
public void blankArrayTest(String s) throws Exception {
boolean result = blankReplace.refineTest(s).equals("hello world");
assertEquals(result, true);
}
이제 실패한 코드를 수정하여 통과시켜보자.
public String refineTest(String s) {
return s.replace(" ", " ")
.replace(" ", " ")
.replace(" ", " ");
}
단순하게 replace .replace(" ", " ")를 추가했다.
테스트를 실행해보자.
단순하지만 통과했다. 코드를 보지 않고 테스트 결과만 보면 목표를 만족했다고 볼 수 있다.
하지만 그 외의 테스트 케이스를 추가해보면 어떨까?
@ParameterizedTest
@CsvSource(value = {..., "hello world"}) // 공백 5개다
...
공백 5개를 추가했다
성공했다.
공백 6개 케이스를 추가해보자
@ParameterizedTest
@CsvSource(value = {..., "hello world"}) // 공백 6개다
...
이번에도 성공했다.
공백을 11개로 추가해보자
@ParameterizedTest
@CsvSource(value = {..., "hello world"}) // 공백 11개다
...
실패했다.
기능을 수정하자.
public String refineTest(String s) {
return s.replace(" ", " ")
.replace(" ", " ")
.replace(" ", " ")
.replace(" ", " ");
}
테스트를 통과했지만 코드는 찝찝하다.
하지만 알고 봤더니 고객은 모든 공백에 대해서 줄이는 기능을 원했다.
지금까지 space 공백에 대해 처리를 했었는데 tab으로 공백을 줄 경우 통과가 될까?
tab 테스트 코드를 먼저 작성하고 결과를 보자
@ParameterizedTest
@CsvSource(value = {"hello\t world"})
@DisplayName("tab 공백 테스트")
public void tabTest(String s) throws Exception {
boolean result = blankReplace.refineTest(s).equals("hello world");
assertEquals(result, true);
}
탭 문자를 가진 공백을 줄이진 못했다.
replace 기능을 수정해보자.
public String refineTest(String s) {
return s.replace(" ", " ")
...
.replace("\t ", " ");
}
구현한 코드가 찝찝하지만 성공했다.
테스트 케이스로 탭과 공백 위치를 변경하고 실행해보자
@ParameterizedTest
@CsvSource(value = {..., "hello \tworld"})
실패했다. 이걸 통과하려면 기능을 또 수정해야 한다.
public String refineTest(String s) {
return s.replace(" ", " ")
.replace("\t", " ")
...;
}
요구가 추가되었다. 특정 금지어(mockist, purist)가 있으면 마스킹 처리해야 한다.
테스트 케이스를 먼저 추가하자.
@ParameterizedTest
@CsvSource(value = {"hello mockist,*******"})
@DisplayName("금지어 마스킹 테스트")
public void maskingTest(String s, String masking) throws Exception {
boolean result = blankReplace.refineTest(s).equals("hello " + masking);
assertEquals(result, true);
}
아직 기능은 수정이 안되어서 실패하는 테스트가 되었다.
이제 수정하자.
public String refineTest(String s) {
return s.replace(" ", " ")
...
.replace("mockist", "*******");
}
통과되었다.
다른 금지어에 대한 테스트 케이스를 추가해보자.
@ParameterizedTest
@CsvSource(value = {"hello mockist,*******", "hello purist,******"})
반복되는 상황으로 실패를 예측할 수 있다.
기능을 수정하자.
public String refineTest(String s) {
return s.replace(" ", " ")
...
.replace("mockist", "*******")
.replace("purist", "******");
}
성공적으로 통과했다.
지금까지 기능을 보면 코드가 많이 찝찝하다.
현재 테스트 케이스에만 비효율적으로 통과하는 코드이며,
이외에 테스트 케이스가 생길 때마다 통과하기 위해 기능을 수정해야 한다.
위의 마스킹 요구사항을 재정리해보자.
특정 단어가 금지어로 등록되면 단어의 글자 개수만큼 * 로 변환해야 한다.
이를 기반으로 알 수 없는 금지어가 들어온다는 가정하에 테스트 케이스를 추가해보자.
@ParameterizedTest
@CsvSource(value = {"dkne", "dkjksd", "bmndkf"})
@DisplayName("금지어 마스킹 테스트")
public void randomWordsMaskingTest(String word) throws Exception {
List<String> bannedWords = Arrays.asList(new String[]{"dkne", "dkjksd", "bmndkf"});
String s = "hello " + word;
String actual = "hello " + blankReplace.repeat(word.length());
boolean result = blankReplace.refineTest(s, bannedWords).equals(actual);
assertEquals(result, true);
}
실패하는 테스트 케이스다. 통과하기 위해서는 기능에 금지어 리스트를 추가로 받게 하고 결과를 반환하게 수정해보자.
여기서 주의할 점은 이전 케이스들은 금지어를 사용하지 않는데 기능에 금지어 리스트를 받게 수정해서 컴파일 에러가 발생한다.
이는 변경에 따른 기존 테스트가 실패한다는 의미이며, 이를 방지하기 위해 같은 이름으로 다른 인자를 받는 메서드를 추가해보자.
public class BlankReplace {
public String refineTest(String s) {
return refineTest(s, new ArrayList<>());
}
public String refineTest(String s, List<String> bannedWords) {
s = s.replace(" ", " ")
.replace("\t", " ")
.replace(" ", " ")
.replace(" ", " ")
.replace(" ", " ")
.replace("mockist", "*******")
.replace("purist", "******");
for (String bannedWord : bannedWords) {
s = s.replace(bannedWord, repeat(bannedWord.length()));
}
return s;
}
public String repeat(int length) {
StringBuilder builder = new StringBuilder();
IntStream.range(0, length)
.forEach(i -> builder.append("*"));
return builder.toString();
}
}
기존 테스트 케이스와 새로운 테스트 케이스가 정상적으로 작동하는지 확인해보자.
공백 문자만 지우는 테스트와 금지어 마스킹하는 테스트 모두 통과했다.
다시 보니 처음에 작성한 금지어 mockist, purist와 랜덤 금지어가 겹친다.
하나로 합치고 테스트를 해보자
@ParameterizedTest
@CsvSource(value = {"dkne", "dkjksd", "bmndkf", "mockist", "purist"})
@DisplayName("금지어 마스킹 테스트")
public void randomWordsMaskingTest(String word) throws Exception {
List<String> bannedWords = Arrays.asList(new String[]{"dkne", "dkjksd", "bmndkf", "mockist", "purist"});
String s = "hello " + word;
String actual = "hello " + blankReplace.repeat(word.length());
boolean result = blankReplace.refineTest(s, bannedWords).equals(actual);
assertEquals(result, true);
}
잘 작동한다.
다음 시간에는 리팩토링에 대해 알아보겠다.
'교육 및 인강 > 이규원의 현실 세상의 TDD' 카테고리의 다른 글
이규원님의 현실 세상의 TDD 기초, 7편 : 테스트 주도 개발 (0) | 2021.04.16 |
---|---|
이규원님의 현실 세상의 TDD 기초, 6편 : 정리된 코드(리팩토링) (0) | 2021.04.14 |
이규원님의 현실 세상의 TDD 기초, 4편 : 단위테스트 (2) | 2021.04.13 |
이규원님의 현실 세상의 TDD 기초, 3편 : 코드 분해 (0) | 2021.03.10 |
이규원님의 현실 세상의 TDD 기초, 2편 : 테스트 기법 (0) | 2021.03.10 |