TDD 시작하기 - 1
프롤로그
이전에 부트캠프를 진행하기 전에 짧게 예습 개념으로 TDD 스터디를 짧게 진행했었다.
부트캠프가 시작되고, 내부 프로젝트 및 과제를 진행하다보니 스터디가 흐지부지되어서 책을 다 끝내지 못했었는데, 내용 복습도 하고 그래도 얼마 안되는 양 끝까지 완독하기 위해서 학습을 해보기로 마음먹었다.
- 사용하는 기술스택
Java
JUnit 5
TDD 시작
책에서는 TDD 이전의 개발 방식을 보여주고, 간단한 기능 테스트 코드와 함께 실습해보면서 TDD란 무엇인지 간략하게 설명해주고 있다.
TDD 이전의 개발
- 만들 기능에 대해 설계를 고민
- 과정1을 수행하면서 구현에 대해서도 고민 → 코드 작성
- 기능 테스트 → 문제 발생되면 코드 디버깅
이러한 개발 방식에서는 아래와 같은 문제점이 있다고 지적했다.
- 한 번에 작성한 코드가 많은 경우 디버깅하는 시간이 길어짐
- 코드를 작성하는 개발자와 그 코드를 테스트하는 개발자가 달라서 테스트가 완료되지 않은 상태에서 배포가 이루어져 다른 개발자가 테스트하는 구조가 만들어짐
TDD란?
- 구현을 먼저 하고 나중에 테스트하는것이 아닌, 테스트를 하고 그 다음에 구현하는 것.
public class CalculatorTest {
@Test
void plus() {
int result = Calculator.plus(5,6);
assertEquals(11, result);
}
}
테스트는 작성되었지만 아직 테스트를 수행할 수 없다. 다음과 같은 과정을 수행해야한다.
- Calculator 클래스 생성
- 메서드, 반환 타입 설정
public class Calculator {
public static int plus(int a1, int a2) {
return 11;
}
}
처음엔 단순히 테스트를 성공시키기 위한 코드를 작성하고 이후 점진적으로 발전시킨다.
- 테스트 코드 추가
assertEquals(3, Calculator.plus(1, 2));
- 점진적 구현
public class Calculator {
public static int plus(int a1, int a2) {
if (a1 == 1 && a2 == 2) return 3;
else return 11;
}
}
이런 식으로 TDD는 테스트를 먼저 작성하고 테스트에 실패하면 테스트를 통과시킬 만큼 코드를 추가하는 과정을 반복하면서 점진적으로 기능을 완성해 나간다.
TDD 예시
암호 검사기 구현
- 검사할 규칙
- 길이가 8글자 이상
- 0부터 9 사이의 숫자를 포함
- 대문자 포함
- 세 규칙을 모두 충족하면 암호는 강함이다
- 2개의 규칙을 충족하면 암호는 보통이다
- 1개 이하의 규칙을 충족하면 암호는 약함이다
첫 번째 테스트: 모든 규칙을 충족하는 경우
@Test
void meetsAllCriteria_Then_Strong() {
assertStrength("ab12!@AB", PasswordStrength.STRONG);
assertStrength("abc1!Add", PasswordStrength.STRONG);
}
public class PasswordStrengthMeter {
public PasswordStrength meter(String s) {
return PasswordStrength.STRONG;
}
}
- 각 조건을 검사하는 코드를 만들지 않고 ‘’강함’에 해당하는 값을 리턴하면 일단은 테스트를 통과 가능함.
두 번째 테스트: 길이만 8글자 미만이고 나머지 조건은 충족하는 경우
@Test
void meetsOtherCriteria_except_for_Length_Then_Normal() {
assertStrength("ab12!@A", PasswordStrength.NORMAL);
assertStrength("Ab12!c", PasswordStrength.NORMAL);
}
public class PasswordStrengthMeter {
public PasswordStrength meter(String s) {
if (s.length() < 8) {
return PasswordStrength.NORMAL;
}
return PasswordStrength.STRONG;
}
}
- 이제 앞서 말했던 테스트 케이스를 추가하면서 점진적으로 기능을 완성해 나가면 된다.
세 번째 테스트: 숫자를 포함하지 않고 나머지 조건은 충족하는 경우
@Test
void meetsOtherCriteria_except_for_number_Then_Normal() {
assertStrength("ab!@ABqwer", PasswordStrength.NORMAL);
}
public class PasswordStrengthMeter {
public PasswordStrength meter(String s) {
if (s.length() < 8) {
return PasswordStrength.NORMAL;
}
boolean containsNum = false;
for (char ch : s.toCharArray()) {
if (ch >= '0' && ch <= '9') {
containsNum = true;
break;
}
}
if (!containsNum) return PasswordStrength.NORMAL;
return PasswordStrength.STRONG;
}
}
리팩토링 과정
public class PasswordStrengthMeter {
public PasswordStrength meter(String s) {
if (s.length() < 8) {
return PasswordStrength.NORMAL;
}
boolean containsNum = meetsContainingNumberCriteria(s);
if (!containsNum) return PasswordStrength.NORMAL;
return PasswordStrength.STRONG;
}
}
private boolean meetsContainingNumberCriteria(String s) {
for (char ch : s.toCharArray()) {
if (ch >= '0' && ch <= '9') {
return true;
}
}
return false;
}
- 코드가 다소 길어지므로 해당 코드를 메서드로 추출해서 가독성을 개선하고 메서드 길이도 줄여봄.
네 번째 테스트: 값이 없는 경우
public class PasswordStrengthMeter {
public PasswordStrength meter(String s) {
if (s == null || s.isEmpty()) return PasswordStrength.INVALID;
if (s.length() < 8) {
return PasswordStrength.NORMAL;
}
boolean containsNum = meetsContainingNumberCriteria(s);
if (!containsNum) return PasswordStrength.NORMAL;
return PasswordStrength.STRONG;
}
}
private boolean meetsContainingNumberCriteria(String s) {
for (char ch : s.toCharArray()) {
if (ch >= '0' && ch <= '9') {
return true;
}
}
return false;
}
@Test
void nullInput_Then_Invalid() {
assertStrength(null, PasswordStrength.INVALID);
}
테스트 → 코딩 → 리팩토링
- 기능을 검증하는 테스트 코드 작성
- 테스트 시간이 감소
- 테스트를 통과할 만큼만 코드 작성
- 어떠한 테스트에 대한 코드만 작성하므로 구현 시간이 감소함
- 작은 단위의 코드를 작성해 해당 테스트를 통과하는게 수월해 디버깅 시간이 감소
- 리팩토링
- 작은 단위의 리팩토링이 반복되어 편함(지속적인 코드 정리) - 이 과정을 반복하면서 점진적으로 기능을 완성시키는 것.
Leave a comment