프롤로그

이전에 부트캠프를 진행하기 전에 짧게 예습 개념으로 TDD 스터디를 짧게 진행했었다.

부트캠프가 시작되고, 내부 프로젝트 및 과제를 진행하다보니 스터디가 흐지부지되어서 책을 다 끝내지 못했었는데, 내용 복습도 하고 그래도 얼마 안되는 양 끝까지 완독하기 위해서 학습을 해보기로 마음먹었다.

- 사용하는 기술스택
Java
JUnit 5

TDD 시작

책에서는 TDD 이전의 개발 방식을 보여주고, 간단한 기능 테스트 코드와 함께 실습해보면서 TDD란 무엇인지 간략하게 설명해주고 있다.

TDD 이전의 개발

  1. 만들 기능에 대해 설계를 고민
  2. 과정1을 수행하면서 구현에 대해서도 고민 → 코드 작성
  3. 기능 테스트 → 문제 발생되면 코드 디버깅

이러한 개발 방식에서는 아래와 같은 문제점이 있다고 지적했다.

  • 한 번에 작성한 코드가 많은 경우 디버깅하는 시간이 길어짐
  • 코드를 작성하는 개발자와 그 코드를 테스트하는 개발자가 달라서 테스트가 완료되지 않은 상태에서 배포가 이루어져 다른 개발자가 테스트하는 구조가 만들어짐

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);
}

테스트 → 코딩 → 리팩토링

  1. 기능을 검증하는 테스트 코드 작성
    • 테스트 시간이 감소
  2. 테스트를 통과할 만큼만 코드 작성
    • 어떠한 테스트에 대한 코드만 작성하므로 구현 시간이 감소함
    • 작은 단위의 코드를 작성해 해당 테스트를 통과하는게 수월해 디버깅 시간이 감소
  3. 리팩토링
    • 작은 단위의 리팩토링이 반복되어 편함(지속적인 코드 정리) - 이 과정을 반복하면서 점진적으로 기능을 완성시키는 것.

Categories:

Updated:

Leave a comment