본문 바로가기
개발/Spring

Test Driven Development(TDD)

by BellOne4222 2024. 2. 18.

Test Driven Development(TDD)

  • 테스트 주도 개발 기법
  • 프로그램의 설계와 구현, 사고의 흐름을 테스트 중심으로 생각하는 개발 방법
  • 개발 순서의 변화
    • as - is : 구현 -> 테스트
    • to - be : 테스트를 만든다 -> 구현
  • 익스트림 프로그래밍(XP), 애자일, 폭포수 모델, Test-First Programming
  • 개발 사이클
    1. RED : 실패하는 테스트를 설계(요구사항의 명세)
    2. GREEN : 테스트를 성공시킨다(구현)
    3. REFACTOR : 구현 코드를 리팩토링
  • 테스트 구조
    • Given(Arrange) : 상태(state)의 정의(테스트를 수행 할 때 전제 조건)
    • When(Act) : 동작(테스트 실행)
    • Then(Assert) : 검증(동작의 결과(actual)과 예상값(expected)를 비교하여 검증)
  • TDD를 하는 이유
    • 개인의 입장 : 뭘 하려는지에 대한 사실을 스스로 지속적으로 확인하고 명확하게 하기 위해서
      • 프로젝트 중 막막하거나 막히는 부분해 대해 개발이 지연되는 부분을 방지
    • 팀의 입장 : 내가 하는 일 도는 동료가 하는 일에 대해 명확히 알기 위해서 동료와 소스 코드로 공유하고 소통(코드 리뷰) 해야한다.
      • 개발 계획을 문서와 구현 코드를 공유하고 테스트 코드를 공유하며 무슨 부분을 진행하고 있는지, 구현의 진행도 및 척도에 대해 파악 할 수 있다.
  • 테스트 개발을 효율적으로 하기 위한 방법
    • 테스트 설계 흐름에 익숙해야한다.
      • 사람의 요구사항을 프로그램이 할 수 있는 기능으로 변환
      • 기능을 단위 기능으로 세분화
      • 기능의 관계와 상호작용을 설계
    • 테스트 작성 기술을 숙련하는 과정 필요
  • 단계
    1. 메인 요구사항의 기본 목표 위주로만 우선 테스트 작성
    2. 메인 요구사항 기본 + 세부 목표를 테스트로 작성
    3. 메인 요구사항 기본 + 세부 + 더욱 구체적인 기능적 고려 

 

@Getter // Lombok에서 getter 메서드를 자동으로 생성합니다.
@ToString // Lombok에서 toString 메서드를 자동으로 생성합니다.
@EqualsAndHashCode(callSuper = true) // 부모 클래스의 필드를 고려하여 equals 및 hashCode 메서드를 생성합니다.
public class ApiDataResponse extends ApiErrorResponse { // ApiErrorResponse를 확장한 ApiDataResponse 클래스 선언

	private final Object data; // 응답 데이터를 저장하는 필드

	private ApiDataResponse(boolean success, Integer errorCode, String message, Object data) {
		// 부모 클래스의 생성자를 호출하여 상태 설정
		super(true, ErrorCode.OK.getCode(), ErrorCode.OK.getMessage());
		this.data = data; // 데이터를 설정합니다.
	}

	public static ApiDataResponse of(boolean success, Integer errorCode, String message, Object data) {
		// 새로운 ApiDataResponse 객체를 생성하고 반환합니다.
		return new ApiDataResponse(success, errorCode, message, data);
	}
}

 

기존 Test Code

class ApiDataResponseTest { // ApiDataResponse 클래스를 테스트하는 테스트 클래스 선언

	@Test // JUnit에게 이 메서드가 테스트 메서드임을 알립니다.
	void test(){ // test 메서드 정의

		// Given 테스트에 사용될 데이터를 설정합니다. 여기서는 "test data"를 설정했습니다.
		String data = "test data"; // 테스트 데이터 설정

		// When ApiDataResponse.of 메서드를 호출하여 응답 객체를 생성합니다.
		ApiDataResponse response = ApiDataResponse.of(true, ErrorCode.OK.getCode(), ErrorCode.OK.getMessage(), data);
		// ApiDataResponse의 of 메서드를 호출하여 응답 객체를 생성합니다.

		// Then 생성된 응답 객체를 검증합니다. response 객체가 올바른지 확인하기 위해 success, errorCode, message, data 필드가 예상된 값과 일치하는지 확인합니다.
		assertThat(response) // response 객체에 대한 검증 시작
			.hasFieldOrPropertyWithValue("success", true) // success 필드가 true인지 확인
			.hasFieldOrPropertyWithValue("errorCode", ErrorCode.OK.getCode()) // errorCode 필드가 OK 코드와 일치하는지 확인
			.hasFieldOrPropertyWithValue("message", ErrorCode.OK.getMessage()) // message 필드가 OK 메시지와 일치하는지 확인
			.hasFieldOrPropertyWithValue("data", data); // data 필드가 테스트 데이터와 일치하는지 확인
	}
}

 

 

TDD Style Refactoring

@Getter // Lombok에서 getter 메서드를 자동으로 생성합니다.
@ToString // Lombok에서 toString 메서드를 자동으로 생성합니다.
@EqualsAndHashCode(callSuper = true) // 부모 클래스의 필드를 고려하여 equals 및 hashCode 메서드를 생성합니다.
public class ApiDataResponse<T> extends ApiErrorResponse { // ApiErrorResponse를 확장한 ApiDataResponse 클래스 선언

	private final T data; // 응답 데이터를 저장하는 필드

	private ApiDataResponse(T data) { // 생성자 정의
		super(true, ErrorCode.OK.getCode(), ErrorCode.OK.getMessage()); // 상위 클래스의 생성자를 호출하여 상태 설정
		this.data = data; // 데이터를 설정합니다.
	}

	public static <T> ApiDataResponse<T> of(T data) { // 데이터를 받아 새로운 ApiDataResponse 객체를 생성하여 반환하는 정적 메서드
		return new ApiDataResponse<>(data); // 새로운 ApiDataResponse 객체를 생성하여 반환합니다.
	}

	public static <T> ApiDataResponse<T> empty() { // 빈 응답을 생성하여 반환하는 정적 메서드
		return new ApiDataResponse<>(null); // 빈 응답을 생성하여 반환합니다.
	}
}

 

class ApiDataResponseTest { // ApiDataResponse 클래스를 테스트하는 테스트 클래스 선언

	@DisplayName("문자열 데이터가 주어지면, 표준 성공 응답을 생성한다.")
	@Test // JUnit에게 이 메서드가 테스트 메서드임을 알립니다.
	void givenStringData_whenCreatingResponse_thenReturnsSuccessfulResponse(){ // test 메서드 정의

		// Given 테스트에 사용될 데이터를 설정합니다. 여기서는 "test data"를 설정했습니다.
		String data = "test data"; // 테스트 데이터 설정

		// When ApiDataResponse.of 메서드를 호출하여 응답 객체를 생성합니다.
		ApiDataResponse<String> response = ApiDataResponse.of(data);
		// ApiDataResponse의 of 메서드를 호출하여 응답 객체를 생성합니다.

		// Then 생성된 응답 객체를 검증합니다. response 객체가 올바른지 확인하기 위해 success, errorCode, message, data 필드가 예상된 값과 일치하는지 확인합니다.
		assertThat(response) // response 객체에 대한 검증 시작
			.hasFieldOrPropertyWithValue("success", true) // success 필드가 true인지 확인
			.hasFieldOrPropertyWithValue("errorCode", ErrorCode.OK.getCode()) // errorCode 필드가 OK 코드와 일치하는지 확인
			.hasFieldOrPropertyWithValue("message", ErrorCode.OK.getMessage()) // message 필드가 OK 메시지와 일치하는지 확인
			.hasFieldOrPropertyWithValue("data", data); // data 필드가 테스트 데이터와 일치하는지 확인
	}

	@DisplayName("데이터가 없을 때, 비어있는 표준 성공 응답을 생성한다.") // 테스트 케이스의 이름 설정
	@Test // JUnit에게 이 메서드가 테스트 메서드임을 알립니다.
	void givenNothing_whenCreatingResponse_thenReturnsEmptySuccessfulResponse() { // givenNothing_whenCreatingResponse_thenReturnsEmptySuccessfulResponse 메서드 정의

		// Given
		// 테스트에 사용될 데이터가 없으므로 아무 작업도 수행하지 않습니다.

		// When
		ApiDataResponse<String> response = ApiDataResponse.empty(); // ApiDataResponse 클래스의 empty 메서드를 호출하여 응답 객체를 생성합니다.

		// Then
		assertThat(response) // response 객체에 대한 검증 시작
			.hasFieldOrPropertyWithValue("success", true) // success 필드가 true인지 확인
			.hasFieldOrPropertyWithValue("errorCode", ErrorCode.OK.getCode()) // errorCode 필드가 OK 코드와 일치하는지 확인
			.hasFieldOrPropertyWithValue("message", ErrorCode.OK.getMessage()) // message 필드가 OK 메시지와 일치하는지 확인
			.hasFieldOrPropertyWithValue("data", null); // data 필드가 null인지 확인
	}
}

 

컬렉션 팩토리 of

  • Arrays 클래스는 List를 제외한 Set, Map 같은 다른 형태의 자료구조 팩토리 메서드를 지원하지 않는다는 단점이 있다.
    • 해결하기 위해선 리스트를 인수로 받거나 스트림 API 등의 방법을 통해 해결해야 한다.
    • 내부적으로 불필요한 객체 할당을 필요로 하는 단점이 있다.
    • 자료구조 내부가 변환할 수 있다
  •  자바 9부터는 컬렉션 API를 개선하여 of라는 팩토리 메서드를 제공해준다. List, Set, Map은 다음과 같이 of 팩토리 메서드를 이용해서 간단하게 생성할 수 있다.
    List<Integer> list = Arrays.asList(1, 2, 3);
    Set<Integer> set = Set.of(1, 2, 3);
    Map<Integer, String> map = Map.of(1, "A", 2, "B", 3, "C");


  • 가변 인수를 활용한 of() 메서드 하나만 있으면 해결할 수 있는데 왜 굳이 0~10개의 매개변수를 가지는 of() 메서드를 오버로딩하여 구현해놓은 것일까?
    • 불필요한 비용을 제거하기 위해서이다.
    •  사용하지 않을 추가적인 배열을 할당 및 초기화한 다음 가비지 컬렉션을 통해 제거해야 하는 비용이 추가적으로 필요하기 때문에 일반적으로 활용되는 0~10개의 매개변수를 가지는 of() 메서드를 미리 구현

'개발 > Spring' 카테고리의 다른 글

Validation  (0) 2024.02.19
MVC 패턴(3) - 비즈니스 로직 구현  (0) 2024.02.19
Controller Test  (0) 2024.02.18
ControllerAdvice  (0) 2024.02.17
MVC 패턴 - API 설계(2)  (0) 2024.02.17