TDD란?
Test Driven Development의 약자로 ‘테스트 주도 개발’이라고 한다
반복 테스트를 이용한 소프트웨어 반복론으로 작은 단위의 테스트 케이스를 작성하고 이를 통과하는 코드를 추가하는 단계를 반복하여 구현
애자일 방법론 중 하나인 eXtream Programming(XP)의 ‘Test-First’ 개념에 기반

- { Red } 단계에서는 실패하는 테스트 코드를 먼저 작성한다.
- { Green } 단계에서는 테스트 코드를 성공시키기 위한 최소한의 실제 코드를 작성한다.
- { Blue } 단계에서는 중복 코드 제거, 일반화 등의 리팩토링을 수행한다.
일반 개발 방식 vs TDD 개발 방식
TDD 개발 방식
- 테스트 코드 작성 후 통과한 코드만을 코드 개발함
- 이러한 반복적인 단계가 진행되면서 코드의 버그가 줄고 소스 코드는 간결해짐
- 설계가 자연스럽게 개선되며 재설계 시간 절감
일반 개발 방식
- 초기 코드가 완벽하지 않아 재사용이 어렵고 유지 보수가 어렵다
- 소프트웨어 개발을 느리게 함
- 소스 코드의 품질 저하 및 자체 테스트 비용 증가
TDD 개발 방식의 단점
가장 큰 단점은 생산성의 저하!
- 처음부터 2개의 코드를 짜야한다
- 일반적인 개발 시간 보다 10 ~ 30%정도 증가
TDD 개발 방식의 일반적인 순서
- 기능을 검증하는 테스트 코드 작성
- 테스트 대상이 될 클래스 이름, 메서드 이름, 파리미터 개수 및 타입, 리턴 타입 고민
- 새로운 메서드 생성 할지, 정적 메서드로 구현할지 고민
- 컴파일 오류를 없앨 수 있는 클래스와 메서드 생성
- 테스트 실행 후 테스트 실패
- 실패한 테스트를 통과시킨 후 새로운 테스트 생성 및 반복
TDD 개발 실습 : 암호 검사기
🚀 기능 요구 사항
암호 검사기는 문자열을 검사해서 규칙을 준수하는지에 따라 암호를 '약함', '보통', '강함'으로 구분한다.
- 검사할 규칙은 다음 세 가지이다.
- 길이가 8글자 이상
- 0부터 9 사이의 숫자를 포함
- 대문자 포함
- 세 규칙을 모두 충족하면 암호는 강함이다.
- 2개의 규칙을 충족하면 암호는 보통이다.
- 1개 이하의 규칙을 충족하면 암호는 약함이다.
먼저 길이가 8글자 이상을 테스트 하는 코드를 작성하기 위해 아무 의미 없는 메서드 생성
import org.junit.jupiter.api.Test;
public class PasswordLengthTest {
@Test
void name(){}
}
- [참고] 첫번째 테스트의 중요성
- 모든 규칙을 선정하는 경우
- 각 조건을 검사하는 코드를 만들지 않고 ‘강함’에 해당하는 값을 리턴하면 테스트 통과
- 모든 조건을 충족하지 않는 경우
- 모든 조건을 검사해야 해서 사실 상 모두 구현하고 테스트 하는 방식과 다르지 않음
- 모든 규칙을 선정하는 경우
- 가장 쉽거나 가장 예외적인 경우를 첫번째 테스트로 선정
모든 규칙을 충족하는 테스트 코드 작성
당연히 클래스와 메서드를 생성하지 않았기 때문에 오류가 나온다. 이를 해결하기 위해 클래스와 메서드를 생성하자
public class passwordUnit {
passwordStrength test(String password){
return null;
}
}
public enum passwordStrength {
STRONG
}
당연히 테스트는 실패한다 성공시키기 위해 passwordUnit의 return값을 STRONG으로 지정하면 테스트를 통과한다
public class passwordUnit {
passwordStrength test(String password){
return passwordStrength.STRONG;
}
}
모든 조건을 충족하는 테스트를 만들었으니 길이가 8미만이지만 다른 조건은 충족하는 테스트를 생성해보자
당연히 테스트는 실패한다 그렇다고 password 클래스의 test 메서드의 리턴 값을 NORMAL 값으로 설정하면 위의 테스트와 충돌한다 조율을 해보자
다음과 같은 코드를 추가하면 통과한다
다음은 숫자는 포함하지 않고 나머지 조건은 충족하는 경우의 테스트 코드를 작성해보자
@Test
void meetExceptNumberTest(){
passwordStrength result = password.test("as!@ABBc");
assertEquals(passwordStrength.NORMAL, result);
}
public class passwordUnit {
passwordStrength test(String password){
boolean containNum = false;
if(password.length() < 8){
return passwordStrength.NORMAL;
}
for(char ch : password.toCharArray()){
if('0' <= ch && ch<= '9'){
containNum = true;
break;
}
}
if(!containNum){
return passwordStrength.NORMAL;
}
return passwordStrength.STRONG;
}
}
4번째로는 대문자가 없지만 모든 조건을 충족하는 테스트 코드를 작성해보자
import java.util.Locale;
public class passwordUnit {
passwordStrength test(String password){
boolean containNum = false;
boolean containUpper = false;
if(password.length() < 8){
return passwordStrength.NORMAL;
}
for(char ch : password.toCharArray()){
if('0' <= ch && ch<= '9'){
containNum = true;
break;
}
}
if(!containNum){
return passwordStrength.NORMAL;
}
for(char ch : password.toCharArray()){
if(Character.isUpperCase(ch)){
containUpper = true;
break;
}
}
if(!containUpper){
return passwordStrength.NORMAL;
}
return passwordStrength.STRONG;
}
}
코드 리팩토링
코드를 짜는데 있어서 가장 중요하다고 생각하는 부분은 바로 리팩토링이다
리팩토링을 함으로써 유지 보수가 쉬워지고 가독성이 높아지는 장점이 있다 리팩토링 방법 중 이번에는 OOP의 SOILD 방법 중 SRP 방법을 사용해 코드를 분리해보자
📌 OOP 5대 설계 원칙 - SOLID
SRP - 단일 책임의 원칙
“하나의 메서드는 하나의 책임”
→ 1. 관심사 2. common code, uncommon code, 3. 공통부분
passWordUnit 클래스의 test 메서드를 관심사로 구분해보자
import java.util.Locale;
public class passwordUnit {
passwordStrength test(String password){
boolean containUpper = false;
boolean containNum = false;
boolean containLength = false;
containLength = password.length() > 8;
if(!containLength) return passwordStrength.NORMAL;
containNum = exceptNumTest(password);
if(!containNum) return passwordStrength.NORMAL;
containUpper = exceptUpperTest(password);
if(!containUpper) return passwordStrength.NORMAL;
return passwordStrength.STRONG;
}
public boolean exceptNumTest(String password){
for(char ch : password.toCharArray()){
if('0' <= ch && ch<= '9'){
return true;
}
}
return false;
}
public boolean exceptUpperTest(String password){
for(char ch : password.toCharArray()){
if(Character.isUpperCase(ch)){
return true;
}
}
return false;
}
}
훨씬 코드가 깔끔해진걸 볼 수 있다! 이에 이어 테스트 코드도 코드이기 때문에 리팩토링 해보자
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class passwordTest {
passwordUnit password = new passwordUnit();
@Test
void meetAllTest(){
assertStrength("as12!@AB", passwordStrength.STRONG);
}
@Test
void meetExceptLengthTest(){
assertStrength("a12!@A", passwordStrength.NORMAL);
}
@Test
void meetExceptNumberTest(){
assertStrength("as!@ABBc", passwordStrength.NORMAL);
}
@Test
void meetExceptUpperTest(){
assertStrength("ass12!@df", passwordStrength.NORMAL);
}
private void assertStrength(String passwordExp, passwordStrength exp){
passwordStrength result = password.test(passwordExp);
assertEquals(exp, result);
}
}
공통 생성 객체인 password를 클래스 객체로 빼고 assertStrength 메소드를 만들어서 공통 부분을 제거하여 훨씬 깔끔해진 모습을 볼 수 있다!
이제 강함과 보통 규칙을 작성하였고 이번에는 남은 강도가 약함과 예외처리를 해서 마무리 하겠다! 먼저 1개 이하의 규칙을 충족하는 암호를 짤 때 두 가지 정도 고민 해본다. 먼저 하나의 조건이 맞을 때 나머지 둘은 충족하지 않는 코드를 작성할 것인가, 두 조건이 충족하지 않고 그 안에서 하나의 조건이 충족하는 코드를 작성할 것인가. 이미 하나의 조건이 충족하지 않을 경우를 작성했기 때문에 조건 하나를 더 충족하지 않았을 때를 작성해보자 (후자를 선택) 먼저 테스트 코드를 작성한다.

당연히 WEEK가 eunm에 없기 때문에 오류가 나온다 passwordStrength에 WEEK를 추가하고 테스트를 실행하면 NORMAL이라서 오류가 나온다

근데 구현하다 보니 WEEK는 조건들이 엮여있는 느낌이였다. 확통의 조합 같은 느낌으로 풀다 보니 그런건가…? 그래서 구현을 다하고 테스트 코드를 후에 작성했다.
import java.util.Locale;
public class passwordUnit {
passwordStrength test(String password){
boolean containUpper = false;
boolean containNum = false;
boolean containLength = false;
containLength = password.length() >= 8;
containNum = exceptNumTest(password);
containUpper = exceptUpperTest(password);
if(!containLength){
if(!containNum) return passwordStrength.WEEK;
if(!containUpper) return passwordStrength.WEEK;
return passwordStrength.NORMAL;
}
if(!containNum){
if(!containUpper) return passwordStrength.WEEK;
return passwordStrength.NORMAL;
}
if(!containUpper) return passwordStrength.NORMAL;
return passwordStrength.STRONG;
}
public boolean exceptNumTest(String password){
for(char ch : password.toCharArray()){
if('0' <= ch && ch<= '9'){
return true;
}
}
return false;
}
public boolean exceptUpperTest(String password){
for(char ch : password.toCharArray()){
if(Character.isUpperCase(ch)){
return true;
}
}
return false;
}
}
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class passwordTest {
passwordUnit password = new passwordUnit();
@Test
void meetAllTest(){
assertStrength("as12!@ABa", passwordStrength.STRONG);
}
@Test
void meetExceptLengthTest(){
assertStrength("a12!@A", passwordStrength.NORMAL);
}
@Test
void meetExceptNumberTest(){
assertStrength("as!@ABBc", passwordStrength.NORMAL);
}
@Test
void meetExceptUpperTest(){
assertStrength("ass12!@df", passwordStrength.NORMAL);
}
@Test
void meetExceptNum_UpperTest(){
assertStrength("aaaaaaaa", passwordStrength.WEEK);
}
@Test
void meetExceptNum_LengthTest(){
assertStrength("AAAAAA", passwordStrength.WEEK);
}
@Test
void meetExceptLength_UpperTest(){
assertStrength("aaabbb", passwordStrength.WEEK);
}
@Test
void allExceptTest(){
assertStrength("aa", passwordStrength.WEEK);
}
private void assertStrength(String passwordExp, passwordStrength exp){
passwordStrength result = password.test(passwordExp);
assertEquals(exp, result);
}
}
마지막으로 예외처리에 대해 테스트 코드를 작성하고 구현 하였다. 아무것도 쓰지 않았을 때를 예외라고 생각하고 작성하였는데, String은 null과 empty를 다르게 보기 때문에 두 가지 경우를 다 생각하고 테스트 코드를 작성하였다.

ERROR를 생성하고 아래와 같이 구현하였다.


물론 이렇게 예외 처리의 방법은 다양하다. 이것만이 정답이 아닌데, passwordUnit클래스에서 nullpointException을 throw하고 main에서 받아서 try-catch문을 사용할 수 도 있고 main에서도 던진 후 JVM이 처리하도록 놔둘 수 도 있다.
Resource Links
'Test Code' 카테고리의 다른 글
| Controller 계층에 관한 테스트 (0) | 2026.01.18 |
|---|---|
| Junit5란? (0) | 2026.01.18 |