테스트 코드 작성 순서, TDD · 기능 명세 · 설계

이채은·2023년 11월 20일
0

테스트 코드 작성 순서

초반에 복잡한 테스트부터 시작하면 안되는 이유

  • 초반부터 다양한 조합을 검사하는 복잡한 상황을 테스트로 추가하면 해당 테스트를 통과시키기 위해 한 번에 구현해야 할 코드가 많아진다.

e.g. 비밀번호 검사 테스트

👉 다음과 같은 순서로 테스트를 만들어보자.

  1. 대문자 포함 규칙만 충족하는 경우
  2. 모든 규칙을 충족하는 경우
  3. 숫자를 포함하지 않고 나머지 규칙은 충족하는 경우

1. 대문자 포함 규칙만 충족하는 경우 테스트

  • 테스트 코드 작성
@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;
	}
}

2. 모든 규칙을 충족하는 경우 테스트

  • 테스트 코드
@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가 익숙해지면 상황에 따라 구현 속도를 조절할 수 있게 된다.

한 번에 기능 구현을 시도했는데 잘 안된다면 한발 물러서서 천천히 아래 단계를 밟아가면 된다.

  1. 정해진 값을 리턴
  2. 값 비교를 이용해서 정해진 값을 리턴
  3. 다양한 테스트를 추가하면서 구현을 일반화

지속적인 리팩토링

  • TDD 에서는 테스트를 통과한 뒤에는 리팩토링을 진행한다.

    • 매번 해야 하는 것은 아님.
    • 적당한 후보가 보이면 진행
  • TDD를 진행하는 과정에서 지속적으로 리팩토링을 진행하면 코드 가독성이 높아진다.

    • 코드 가독성이 높아지면 개발자는 더욱 빠르게 코드 분석 가능

    • 수정 요청이 있을 때 변경할 코드 빠르게 찾을 수 있음.

      ⇒ 코드 변경의 어려움을 줄여주어 향후 유지보수에 도움이 된다.


TDD · 기능 명세 · 설계

기능 명세

  • 크게 입력결과로 나눌 수 있다.
    • 입력은 기능을 실행하는데 필요한 값
    • 결과는 기능을 실행했을 때 얻는 값

e.g) 로그인 기능

  • 입력
    • 아이디와 암호
  • 결과
    • 아이디와 암호가 일치하면 성공
    • 아이디와 암호가 일치하지 않으면 실패
public String login(String id, String pw) {
	User user = getUser(id);
	if (!user.matchPassword(pw)) {
		throw new IdPwNotMatchException();
	}
	return "로그인 되었습니다.";
}
  • 입력

    • 입력은 보통 메서드의 파라미터로 전달한다.
      • id
      • pw
  • 결과

    • 결과 형식은 여러 형식으로 정의할 수 있다.
    • 리턴값과 익셉션을 결과로 사용했다.
      • "로그인 되었습니다."
      • IdPwNotMatchException
  • ‘결과’ 에는 변경도 포함한다.

    • e.g) 회원 가입 기능
      • 실행 결과 : DB에 회원 정보 추가 ← 리턴값이 없는 ‘변경’

기능 명세로부터 설계가 시작되는 과정

스토리보드를 포함한 다양한 형태의 요구사항 문서를 이용해, 기능 명세 구체화

➡ 기능 명세를 구체화하는 동안 입력과 결과를 도출하고, 도출한 기능 명세를 코드에 반영

➡ 기능 명세의 입력과 결과를 코드에 반영하는 과정에서 기능의 이름, 파라미터, 리턴 타입 등이 결정됨.

➡ 이는 곧 기능에 대한 설계 과정과 연결됨.

설계 과정을 지원하는 TDD

  • TDD는 테스트를 만드는 것부터 시작.
  • 테스트 코드를 먼저 만들기 위해 필요한 것
    • 테스트할 기능을 실행
    • 실행 결과를 검증

테스트할 기능을 실행

  • 기능을 실행할 수 없으면 테스트를 할 수 없다.
    • 즉, 테스트에서 실행할 수 있는 객체나 함수가 존재해야 한다.
      • e.g) 테스트 대상이 되는 클래스와 메서드 이름 결정
      • e.g) 메서드를 실행할 때 사용할 인자의 타입, 개수 결정

실행 결과를 검증

  • 실행 결과를 어떻게 검증할지 고민한다.
    • e.g) 리턴 값을 이용해서 검증하는 테스트
      • 리턴 타입을 결정

⇒ 이렇게 TDD에서 테스트를 작성할 때 고민하는 부분은 곧 설계 과정과 유사하다.

💡 즉, TDD 자체가 설계는 아니지만, TDD를 하다 보면 테스트 코드를 작성하는 과정에서 일부 설계를 진행하게 된다!

필요한 만큼 설계하기

  • TDD는 테스트를 통과할 만큼만 코드를 작성한다.
    • 즉, TDD로 개발을 진행하면 현시점에서 테스트를 통과시키는데 필요한 만큼의 코드만 만들게 된다.
  • 필요할 것으로 예측해서 미리 설계를 유연하게 만들지 않는다.

👉 이를 통해 설계가 불필요하게 복잡해지는 것을 방지할 수 있으며, 요구사항이 바뀌더라도 TDD는 미리 앞서서 코드를 만들지 않으므로 불필요한 구성 요소를 덜 만들게 된다.


기능 명세 구체화

  • 개발자는 보통 기획자가 작성한 스토리보드나 와이어프레임과 같은 형태로 요구사항 명세를 전달 받는다.
    • 이런 문서는 사용자나 기획자가 보기에는 적당할지 모르나 개발자가 기능을 구현하기에는 생략된 내용이 많다.
  • 테스트 코드를 작성하려면 파라미터와 결과 값을 정해야 하므로 개발자는 요구사항 문서에서 기능의 입력과 결과를 도출해야 한다.

⇒ 테스트 코드를 통해 개발자는 기능 명세를 구체화할 수 있다.

  • 모호한 상황을 만나면 이를 구체적인 예로 바꾸어 테스트 코드에 반영한다.
  • 요구사항 : 서비스를 사용하려면 매달 1만원을 선불로 납부한다. 납부일 기준으로 한 달 뒤가 서비스 만료일이 된다.
    • 4월 1일에 만원을 납부하면 만료일은 4월 30일? 5월 1일?

    • 1월 31일에 만원을 납부하면 만료일은 2월 28일? 3월 2일?

      위와 같이 모호한 상황이 생긴다면 기획자와의 소통으로 답을 얻은 뒤, 이를 구체적인 예로 바꾸어 테스트에 구현한다.

    • 이는 예를 이용한 구체적인 명세가 된다.

      • 구체적인 예는 개발자가 요구사항을 더 잘 이해할 수 있게 만들고, 모호함을 없애 주어 개발자가 올바르게 동작하는 기능을 만들 수 있게 한다.

👉 TDD는 처음 접하는 보드게임을 익히는 과정과 유사하다.

처음 접하는 보드게임을 하려면 게임 규칙을 배운다.
하지만 규칙을 들었다고 해서 바로 완벽하게 이해를 하는 것은 어렵고 실제 게임을 진행하면서 다양한 상황에 규칙을 적용하다 보면 규칙을 점차 이해하게 된다.
TDD도 이와 유사하게 구체적인 예를 이용해서 테스트 코드를 추가하다 보면 기능 명세를 보다 잘 이해하고 모호함을 없앨 수 있다!



이 글을 읽는 모두, 좋은 하루 되세요. 😸

0개의 댓글

관련 채용 정보