이전 포스트
https://hou27.tistory.com/entry/TDD-TDD%EB%9E%80
실습 환경
- Java 17
- Spring Boot 3.0.1
- wsl 2
테스트 코드를 작성할 때는 원활한 진행을 위한 규칙이 있다.
규칙은 아래와 같다.
규칙
- 쉬운 경우에서 어려운 경우로 진행
- 예외적인 경우에서 정상인 경우로 진행
초반에 복잡한 테스트부터 시작하면 안 되는 이유
초반부터 다양한 조합을 검사하는 복잡한 상황을 테스트로 추가한다면
해당 테스트를 통과시키기 위해 한 번에 구현해야 하는 코드가 많아진다.
예를 들어 암호 강도 측정 기능을 구현하는데,
다음과 같은 순서로 테스트를 만든다고 가정하자
- 대문자 포함 규칙만 충족하는 경우
- 모든 규칙을 충족하는 경우
- 숫자를 포함하지 않고 나머지 규칙은 충족하는 경우
먼저 대문자 규칙을 포함하는 경우의 테스트 코드이다.
public class PasswordStrengthMeterTest {
@Test
void meetsOnlyUpperCriteria_Then_Weak() {
PasswordStrengthMeter meter = new PasswordStrengthMeter();
PasswordStrength result = meter.meter("ABCDE");
assertEquals(PasswordStrength.WEAK, result);
}
}
단순하게 대문자로 이뤄진 문자열을 테스트하는 코드를 작성하고,
규칙 하나만 만족하므로 WEAK를 반환해야 테스트가 통과되도록 하였다.
public class PasswordStrengthMeter {
public PasswordStrength meter(String s) {
return PasswordStrength.WEAK;
}
}
테스트의 통과를 위해 meter method가 WEAK를 반환하도록 구현하였다.
이번엔 모든 규칙을 충족하는 경우의 테스트 코드를 짤 차례이다.
public class PasswordStrengthMeterTest {
...
@Test
void meetsAllCriteria_Then_Strong() {
PasswordStrengthMeter meter = new PasswordStrengthMeter();
PasswordStrength result = meter.meter("123ABCDE456");
assertEquals(PasswordStrength.STRONG, result);
}
}
- 길이가 8글자 이상
- 0부터 9 사이의 숫자 포함
- 대문자 포함
위 조건을 모두 충족하는 문자열을 주고, STRONG을 반환해야 테스트가 통과하도록 작성하였다.
이제, 테스트를 통과시키기 위해 구현해야 하는데,
public class PasswordStrengthMeter {
public PasswordStrength meter(String s) {
if(s == "123ABCDE456") {
return PasswordStrength.STRONG;
}
return PasswordStrength.WEAK;
}
}
가장 먼저 주어진 문자열을 그대로 가져와서 STRONG을 반환하도록 구현하였다.
물론 테스트를 통과시킬 수는 있지만, 테스트 케이스가 추가된다면 바로 또 실패하게 될 것이다.
검증 예가 추가될 때마다 if 절을 추가할 수는 없으므로 보다 범용적인 구현을 해야 한다.
결국, 이제 겨우 두 번째 테스트인데
어떤 코드를 작성해야 모든 조건을 검사하는 코드인 건지 생각해야 하고,
많은 코드를 작성해야 하는 상황이 발생하고 말았다.
물론 보기에 단순하다고 느껴질 수 있지만 매우 단순한 예시이기 때문에 예시가 담고 있는 의미를 생각해야 한다.
한 번에 많은 코드를 작성하게 되면 버그가 발생할 확률이 높아지고, 많은 시간을 허비하게 된다.
또한 코드 작성 시간이 길어지게 되면 집중력도 떨어지고 흐름이 끊기는 등
복잡한 테스트부터 진행해야 하면 안 되는 이유는 넘쳐난다.
구현하기 쉬운 테스트부터 시작하기
그렇다면 복잡하지 않은 테스트는 무엇일까?
쉬운 테스트부터 진행하고 싶기는 하지만 뭐가 쉬운지 판단하는 것도 쉽지 않을 수 있다.
보통 수 분에서 십여 분 이내에 구현을 완료하고 테스트를 통과시킬 수 있는 것을 선택한다고 하는데,
암호 강도 측정 예에서 그 예시를 찾아보겠다.
- 모든 조건을 충족하는 경우
- 모든 조건을 충족하지 않는 경우
위 2개 모두 STRONG 또는 WEAK를 반환하도록 구현하면 끝이기 때문에 매우 쉽다고 할 수 있다.
여기서 중요한 부분은 상황마다 쉬운 테스트가 달라진다는 점이다.
만약 모든 조건을 충족하는 경우의 테스트 코드를 먼저 작성하고 구현했다면,
그다음으로 처음에 떠올렸던 쉬운 테스트인 모든 조건을 충족하지 않는 경우를 진행하려고 할 수 있다.
그러나, 위 두 가지 경우는 정반대의 경우이므로, 이어서 진행하게 된다면 구현해야 할 코드가 너무 많아지게 된다.
때문에 하나의 테스트를 통과시켰다면 그다음으로 구현하기 쉬운 테스트를 잘 생각하고 선택해야 한다.
각 테스트마다 작성한 코드가 적고 들인 시간도 짧았다면 머릿속에 생생하게 내용이 남아 있기 때문에
디버깅해야 하는 경우 등 여러 상황에서 유리하다.
예외 상황을 먼저 테스트해야 하는 이유
예외 상황이 다양하다면 if else 블록이 복잡하게 등장하게 되는 경우가 많다.
예외 상황을 고려하지 않았던 코드에 예외 상황을 반영하고자 하면,
코드를 뒤집거나 엄청 복잡하게 만들어 버리는 경우가 많다 버그가 발생할 확률이 높아진다.
그래서 초반에 예외 상황을 테스트를 진행하고 버그 가능성을 줄이는 게 바람직하다.
TDD를 진행하는 도중에 예외 상황을 발견하면 해당 상황에 대한 테스트 코드를 추가하고 처리해 주어도 된다.
완급 조절
TDD의 어려운 것 중 하나가 바로 한 번에 얼마만큼의 코드를 작성할 것인가이다.
아직 익숙하지 않을 때는 다음 단계를 거치면서 구현하는 것을 추천한다고 한다.
- 정해진 값을 리턴
- 값 비교를 이용해서 정해진 값을 리턴
- 다양한 테스트를 추가하면서 구현을 일반화
간단한 예시와 함께 살펴보자
길이가 8글자 미만이지만, 숫자도 포함하고 대문자도 포함해서
암호 강도 NORMAL에 해당하는 경우를 테스트한다고 가정하자.
public class PasswordStrengthMeterTest {
...
@Test
void meetsOtherCriteria_except_for_Length_Then_Normal() {
PasswordStrengthMeter meter = new PasswordStrengthMeter();
PasswordStrength result = meter.meter("12345A");
assertEquals(PasswordStrength.NORMAL, result);
}
}
먼저 테스트 코드를 작성해 주고, 구현 속도를 최대한 늦춰서 구현해 보도록 하겠다.
1. 정해진 값을 리턴
public class PasswordStrengthMeter {
public PasswordStrength meter(String s) {
return PasswordStrength.NORMAL;
}
}
정직하게 정해진 값을 그대로 반환해 주었다.
2. 값 비교를 이용해서 정해진 값을 리턴
public class PasswordStrengthMeter {
public PasswordStrength meter(String s) {
if("12345A".equals(s)) {
return PasswordStrength.NORMAL;
}
}
}
이번엔 주어진 값을 그대로 비교하고, 맞다면 테스트 통과가 가능한 값을 반환해 주도록 작성하였다.
값 비교로 구현해 둔 경우엔 테스트 케이스가 추가되면
public class PasswordStrengthMeter {
public PasswordStrength meter(String s) {
if("12345A".equals(s) || "AB0327".equals(s)) {
return PasswordStrength.NORMAL;
}
}
}
다음과 같이 값 비교를 추가해 줄 수 있다.
3. 다양한 테스트를 추가하면서 구현을 일반화
이제는 상수를 제거하고 일반화하는 과정을 거쳐야 한다.
public class PasswordStrengthMeter {
public PasswordStrength meter(String s) {
if (s.length() < 8) {
return PasswordStrength.NORMAL;
} else {
return PasswordStrength.STRONG;
}
}
}
익숙해진다면 굳이 단계를 하나하나 거치지 않아도 되지만,
코드가 잘 떠오르지 않는 경우 도움이 되는 방식이다.
지속적인 리팩토링
테스트가 통과하고 나면 리팩토링이 필요한지 확인하고,
필요한 경우 진행해 주게 된다.
코드 중복과 같은 부분을 적절하게 줄여줌으로써 코드의 가독성을 높이고
향후 유지보수에 도움을 주는 것이 중요하다.
테스트 대상 코드에서 상수를 변수로 바꾸거나 변수의 이름을 변경하는 것들은 바로바로 실행하는 게 좋지만,
메서드 추출과 같이 메서드 구조에 영향을 주는 리팩토링은 큰 틀에서 흐름이 눈에 들어온 후 진행하는 것이 좋다.
테스트할 목록 정리하기
TDD를 시작하기 전 테스트할 목록을 미리 정리해 둔다면
어떤 테스트가 구현이 쉬울지,
어떤 테스트가 예외적인지,
등을 고려하기 쉬워진다.
테스트 과정에서 새로운 테스트 사례를 발견하면 목록에 추가해서 놓치지 않도록 하고
목록에 있는 테스트를 한 번에 모두 작성해선 안된다.
수정할 코드가 많을수록 리팩토링에 대한 심리적 저항이 생기기도 하고,
테스트 코드가 많으면 그만큼 구현 전까지 실패하는 테스트가 많게 되어 개발 리듬에 방해되기 때문이다.
시작이 안 될 때는 단언부터 고민
테스트 코드를 작성하다 보면 시작이 어려울 때가 있는데,
검증하는 코드를 먼저 작성하면 도움이 된다고 한다.
구현이 막힐 때
구현이 막히는 경우엔 코드를 지우고 다시
- 쉬운 테스트, 예외적인 테스트
- 완급 조절
을 상기하며 다시 시작하는 것이 도움 될 수 있다고 한다.
참고 도서
http://www.yes24.com/Product/Goods/89145195
'Study > TDD' 카테고리의 다른 글
TDD - 대역 (0) | 2023.02.08 |
---|---|
TDD - 테스트 코드의 구성 (0) | 2023.02.03 |
TDD - JUnit 5 기초 (0) | 2023.01.25 |
TDD - TDD ∙ 기능 명세 ∙ 설계 (2) | 2023.01.16 |
TDD - TDD(Test-Driven Development)란? (0) | 2023.01.06 |