👉 다음과 같은 순서로 테스트를 만들어보자.
@Test
void 대문자_포함_규칙만_충족() {
PasswordStrengthMeter meter = new PasswordStrengthMeter();
PasswordStrength result = meter.meter("abcDef");
assertEquals(PasswordStrength.WEAK, result);
}
public class PasswordStrengthMeter {
public PasswordStrength meter(String s) {
return PasswordStrength.WEAK;
}
}
@Test
void 모든_규칙_충족() {
PasswordStrengthMeter meter = new PasswordStrengthMeter();
PasswordStrength result = meter.meter("abcDef12");
assertEquals(PasswordStrength.STRONG, result);
}
public class PasswordStrengthMeter {
public PasswordStrength meter(String s) {
if("abcDef12".equals(s)) return PasswordStrength.STRONG;
return PasswordStrength.WEAK;
}
}
🤔 이 때, 새로운 테스트 케이스를 추가한다면?
@Test
void 모든_규칙_충족() {
PasswordStrengthMeter meter = new PasswordStrengthMeter();
PasswordStrength result = meter.meter("abcDef12");
assertEquals(PasswordStrength.STRONG, result);
PasswordStrength result2 = meter.meter("aZcDef12");
assertEquals(PasswordStrength.STRONG, result2);**
}
어떻게 해야 테스트를 통과할 수 있을까 ? …
👉 문자열이 "abcDef12" 나 "aZcDef12" 면 STRONG을 리턴하고 나머지는 WEAK를 리턴하게 구현할까?
그러면 케이스를 추가할 때마다 if 절이 늘어나겠네..
😬 모든 케이스를 이렇게 구현할 수는 없다!!
두번째 테스트를 통과시키려면 모든 규칙을 확인하는 코드를 벌써 구현해야 할 것 같다.
→ 막막하다.
이 두 가지 경우는 모두 그냥 해당 값인 STRONG 또는 WEAK를 리턴하면 된다.
아무거나 선택한다.
<테스트 순서>
1. 모든 조건을 충족하는 경우
모든 규칙을 충족하지 않는 경우는 난감하다.
첫 번째 테스트를 모든 조건을 충족하는 경우를 선택했기 때문이다.
나머지 두 경우는 난이도가 비슷할 것 같다.
<테스트 순서>
1. 모든 조건을 충족하는 경우
2. 한 규칙만 충족하는 경우
3. 두 규칙을 충족하는 경우
그리고 마지막에 모든 조건을 충족하지 않는 경우를 테스트 해주면 된다.
<테스트 순서>
1. 모든 조건을 충족하는 경우
2. 한 규칙만 충족하는 경우
3. 두 규칙을 충족하는 경우
4. 모든 조건을 충족하지 않는 경우
코드의 구조를 뒤집거나,
코드 중간에 예외 상황을 처리하기 위해 조건문을 중복해서 추가해야 함.
⇒ 코드를 복잡하게 만들어 버그 발생 가능성 ⬆
~ TDD 시 얻는 이점 ~
한 번에 기능 구현을 시도했는데 잘 안된다면 한발 물러서서 천천히 아래 단계를 밟아가면 된다.
TDD 에서는 테스트를 통과한 뒤에는 리팩토링을 진행한다.
TDD를 진행하는 과정에서 지속적으로 리팩토링을 진행하면 코드 가독성이 높아진다.
코드 가독성이 높아지면 개발자는 더욱 빠르게 코드 분석 가능
수정 요청이 있을 때 변경할 코드 빠르게 찾을 수 있음.
⇒ 코드 변경의 어려움을 줄여주어 향후 유지보수에 도움이 된다.
public String login(String id, String pw) {
User user = getUser(id);
if (!user.matchPassword(pw)) {
throw new IdPwNotMatchException();
}
return "로그인 되었습니다.";
}
입력
id
pw
결과
"로그인 되었습니다."
IdPwNotMatchException
‘결과’ 에는 변경도 포함한다.
스토리보드를 포함한 다양한 형태의 요구사항 문서를 이용해, 기능 명세 구체화
➡ 기능 명세를 구체화하는 동안 입력과 결과를 도출하고, 도출한 기능 명세를 코드에 반영
➡ 기능 명세의 입력과 결과를 코드에 반영하는 과정에서 기능의 이름, 파라미터, 리턴 타입 등이 결정됨.
➡ 이는 곧 기능에 대한 설계 과정과 연결됨.
⇒ 이렇게 TDD에서 테스트를 작성할 때 고민하는 부분은 곧 설계 과정과 유사하다.
💡 즉, TDD 자체가 설계는 아니지만, TDD를 하다 보면 테스트 코드를 작성하는 과정에서 일부 설계를 진행하게 된다!
👉 이를 통해 설계가 불필요하게 복잡해지는 것을 방지할 수 있으며, 요구사항이 바뀌더라도 TDD는 미리 앞서서 코드를 만들지 않으므로 불필요한 구성 요소를 덜 만들게 된다.
⇒ 테스트 코드를 통해 개발자는 기능 명세를 구체화할 수 있다.
4월 1일에 만원을 납부하면 만료일은 4월 30일? 5월 1일?
1월 31일에 만원을 납부하면 만료일은 2월 28일? 3월 2일?
위와 같이 모호한 상황이 생긴다면 기획자와의 소통으로 답을 얻은 뒤, 이를 구체적인 예로 바꾸어 테스트에 구현한다.
이는 예를 이용한 구체적인 명세가 된다.
👉 TDD는 처음 접하는 보드게임을 익히는 과정과 유사하다.
처음 접하는 보드게임을 하려면 게임 규칙을 배운다.
하지만 규칙을 들었다고 해서 바로 완벽하게 이해를 하는 것은 어렵고 실제 게임을 진행하면서 다양한 상황에 규칙을 적용하다 보면 규칙을 점차 이해하게 된다.
TDD도 이와 유사하게 구체적인 예를 이용해서 테스트 코드를 추가하다 보면 기능 명세를 보다 잘 이해하고 모호함을 없앨 수 있다!
이 글을 읽는 모두, 좋은 하루 되세요. 😸