[테스트] DTO 테스트 - POJO 테스트 | @Valid 테스트 | ConstraintViolation | ReflextionTestUtils

서예진·2024년 3월 6일
0

SPRING

목록 보기
1/5
post-thumbnail

목차

✅ POJO 테스트
▸ POJO 란?
▸ POJO 테스트란?
✅ UserRequestDto 테스트
▸ CommonTest
▸ UserRequestDtoTest
✅ ConstraintViolation
▸ ConstraintViolation의 method
✅ ReflextionTestUtils
▸ ReflextionTestUtils의 method


🔎 키워드

▸ POJO 테스트
▸ @Valid 테스트
▸ ConstraintViolation
▸ ReflextionTestUtils


POJO 테스트

POJO 란?

  • POJO(Plain Old Java Object)
    • 스프링 프레임워크와 같은 중량 프레임워크를 사용하지 않고 순수한 자바 객체를 가리킨다.
    • 이는 객체 지향적인 원칙에 따라 설계되어 환경과 기술에 종속되지 않고 재활용이 가능한 형태로 구현된 오브젝트를 의미한다.
    • 간단하게 말해서 필드와 Getter, Setter와 같은 기본 기능만을 갖는 기본 객체를 의미한다.
    • 주로 POJO는 프레임워크와 관련 없이 단순한 자바 클래스를 나타내며, 이를 통해 코드의 유연성과 테스트 용이성을 높일 수 있다.

POJO 테스트란?

  • 스프링 프레임워크에 의존하지 않는 순수한 자바 객체를 테스트하는 방식이다.
  • 주로 단위 테스트를 작성할 때 사용되며, 객체의 동작을 확인하고 검증하는 것이 목적이다.
  • 이러한 테스트 방식은 스프링과 같은 프레임워크에 의존하지 않기 때문에 빠르고 간편하게 실행되며, 코드의 품질을 개선하는 데 도움이 된다.
  • 외부에서 주입 받을 의존성도 없고 Mocking할 대상도 없기 때문에 테스하기 편하다.
  • POJO 테스트를 거치면 높은 안정성을 갖게 된다.
  • DTO, Entity등이 POJO 테스트의 타겟이다.

  • DTO 테스트 코드를 작성하기에 앞서 아래의 주의사항을 주의해야한다.
  • 테스트 코드를 작성하기 위해 @Setter를 추가하는 것은 위험하다.
  • 테스트용 객체를 만들기 위해서는 ReflectionTestUtils를 사용한다.

UserRequestDto 테스트

@Getter
public class UserRequsetDto {

    @NotBlank(message = "username을 입력하세요.")
    @Pattern(regexp = "^[a-z0-9]{4,10}$", message = "a-z, 0-9, 글자길이 4-10")
    private String username;

    @NotBlank(message = "password를 입력하세요.")
    @Pattern(regexp = "^[a-zA-Z0-9]{8,15}$", message = "a-z, A-z, 0-9, 글자길이 8-15")
    private String password;
}
  • 위의 UserRequestDto를 테스트하고자 한다.
  • 여기서는 controller 단에서 @Valid로 유효성 검증을 하기 때문에 그 부분에 대해서 테스트 할 수 있다.

CommonTest

  • 테스트를 하기 위해서 CommonTest를 만들었다.
  • CommonTest는 interface로 일반적인 테스트에 필요한 상속값들을 모아놓았다.
public interface CommonTest {
	String ANOTHER_PREFIX = "another-";
	Long TEST_USER_ID = 1L;
	Long TEST_ANOTHER_USER_ID = 2L;
	String TEST_USER_NAME = "username";
	String TEST_USER_PASSWORD = "password";
	User TEST_USER = User.builder()
		.username(TEST_USER_NAME)
		.password(TEST_USER_PASSWORD)
		.build();
	User TEST_ANOTHER_USER = User.builder()
		.username(ANOTHER_PREFIX + TEST_USER_NAME)
		.password(ANOTHER_PREFIX + TEST_USER_PASSWORD)
		.build();
}
//builder를 사용하기 위해서는 UserEntity의 생성자에 @Builder가 적용되어야 한다.

@Builder
    public User(String username, String password){
        this.username = username;
        this.password = password;
    }

UserRequestDtoTest

private <T> void setDto(T dto, String username, String password) {
		ReflectionTestUtils.setField(dto, "username", username);
		ReflectionTestUtils.setField(dto, "password", password);
	}
  • DTO의 변수들을 set하기 위해 @Setter 대신 ReflectionTestUtils를 사용했다.
  • 🔎 validate 메서드
    • @Valid와 같은 기능을 테스트 코드에서 구현하기 위해 validate 메서드를 만들었다.

      private Set<ConstraintViolation<UserRequsetDto>> validate(UserRequsetDto userRequestDTO) {
      		ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
      		Validator validator = factory.getValidator();
      		return validator.validate(userRequestDTO);
      	}
    • ConstraintViolation -> 예외가 발생했다면 메세지 값을 조회할 수 있게 -> getMessage

  • UserRequestDtoTest
    • 위에서 만들어 놓은 CommonTest 내의 필드들을 사용하기 위해서 CommonTest 를 implement 한다.
public class UserRequestDtoTest implements CommonTest {

	private <T> void setDto(T dto, String username, String password) {
		ReflectionTestUtils.setField(dto, "username", username);
		ReflectionTestUtils.setField(dto, "password", password);
	}

	@Nested
	class 유저_요청_DTO_생성_테스트 {

		@Test
		void 생성_성공() {
			// given
			UserRequsetDto userRequsetDto = new UserRequsetDto();
			setDto(userRequsetDto, TEST_USER_NAME, TEST_USER_PASSWORD);

			// when
			Set<ConstraintViolation<UserRequsetDto>> violations = validate(userRequsetDto);

			// then
			assertThat(violations).isEmpty();
			//비어있으면 아무것도 잘못된게 없음 -> 정상적으로 UserRequestDto를 생성함

		}

		@Test
		void 생성_실패_잘못된_username() {
			// given
			UserRequsetDto userRequsetDto = new UserRequsetDto();
			setDto(userRequsetDto, "Invalid user name", TEST_USER_PASSWORD);

			// when
			Set<ConstraintViolation<UserRequsetDto>> violations = validate(userRequsetDto);

			// then
			assertThat(violations).hasSize(1); // 몇개 틀렸는지 hasSize
			assertThat(violations)
				.extracting("message") //"message"에 있는 값을 뽑아 올 때, extracting
				.contains("a-z, 0-9, 글자길이 4-10");
		}

		@Test
		void 생성_실패_null_username() {
			// given
			UserRequsetDto userRequsetDto = new UserRequsetDto();
			setDto(userRequsetDto, " ", TEST_USER_PASSWORD);

			// when
			Set<ConstraintViolation<UserRequsetDto>> violations = validate(userRequsetDto);

			// then
			assertThat(violations).hasSize(2); // 몇개 틀렸는지 hasSize
			assertThat(violations)
				.extracting("message") //"message"에 있는 값을 뽑아 올 때, extracting
				.contains("username을 입력하세요.");
		}

		@Test
		void 생성_실패_잘못된_password() {
			// given
			UserRequsetDto userRequsetDto = new UserRequsetDto();
			setDto(userRequsetDto, TEST_USER_NAME, "password!");

			// when
			Set<ConstraintViolation<UserRequsetDto>> violations = validate(userRequsetDto);

			// then
			assertThat(violations).hasSize(1); // 몇개 틀렸는지 hasSize
			assertThat(violations)
				.extracting("message") //"message"에 있는 값을 뽑아 올 때, extracting
				.contains("a-z, A-z, 0-9, 글자길이 8-15");
		}

		@Test
		void 생성_실패_null_password() {
			// given
			UserRequsetDto userRequsetDto = new UserRequsetDto();
			setDto(userRequsetDto, TEST_USER_NAME, " ");

			// when
			Set<ConstraintViolation<UserRequsetDto>> violations = validate(userRequsetDto);

			// then
			assertThat(violations).hasSize(2); // 몇개 틀렸는지 hasSize
			assertThat(violations)
				.extracting("message") //"message"에 있는 값을 뽑아 올 때, extracting
				.contains("password를 입력하세요.");
		}
	}

	private Set<ConstraintViolation<UserRequsetDto>> validate(UserRequsetDto userRequestDTO) {
		ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
		Validator validator = factory.getValidator();
		return validator.validate(userRequestDTO);
	}
}

  • 위의 DTO를 테스트하면서 ConstraintViolation 와 ReflextionTestUtils 가 활용된 것을 볼 수 있다.
  • 따라서, ConstraintViolation 와 ReflextionTestUtils 에 대해서 설명하고자 한다.

ConstraintViolation

  • ConstraintViolation 는 자바 플랫폼의 javax.validation 패키지에서 제공되는 인터페이스이다.
  • 이 인터페이스는 객체의 유효성을 검사하는 데 사용된다.
  • Spring과 같은 프레임워크에서 사용되며, 어노테이션을 통해 지정된 유효성 규칙을 객체에 적용하고, 객체가 이 규칙을 준수하지 않을 때 ConstraintViolation 인터페이스를 통해 오류를 보고한다.

ConstraintViolation의 method

  • ConstraintViolation는 다음과 같은 메서드를 제공한다.
타입메서드반환
StringgetMessage()유효성 검사에 실패한 항목에 대한 오류 메시지를 반환합니다.
ObjectgetInvalidValue()유효성 검사에 실패한 항목의 값을 반환합니다.
PathgetPropertyPath()유효성 검사에 실패한 속성의 경로를 반환합니다.
  • 이 밖에도 많은 메서드를 제공한다.
  • DTO 테스트에서는 오류 메세지가 필요했기 때문에 getMessage()를 호출했다.

ReflextionTestUtils

  • ReflextionTestUtils 는 스프링 프레임워크에서 제공하는 유틸리티 클래스이다.
  • 이 클래스는 테스트 시에 리플렉션(reflection)을 사용하여 객체의 필드나 메서드에 접근하고 수정할 수 있는 기능을 제공한다.
  • 주로 스프링 기반의 단위 테스트 또는 통합테스트에서 사용된다.
  • ReflextionTestUtils 를 사용하면 테스트 코드에서 private 필드나 메서드에 접근하여 값을 설정하거나 호출할 수 있다.
  • ReflextionTestUtils 를 사용하기 전에는 어쩔 수 없이 set메서드나 @Setter를 적용했는데 @Setter를 적용하는 방식이 위험하다는 피드백을 받았다.

ReflextionTestUtils의 method

  • ReflextionTestUtils 클래스의 메서드는 다음과 같다.
메서드설명
setField(Object target, String name, Object value)지정된 객체(target)의 필드(name)에 값을 설정한다.
getField(Object target, String name)지정된 객체(target)의 필드(name)의 값을 반환한다.
invokeMethod(Object target, String name, Object... args)지정된 객체(target)의 메서드(name)를 호출한다.
  • UserRequestDto 테스트에서는 UserRequestDto의 필드 값을 설정하기 위해 setField 를 호출했다.
private <T> void setDto(T dto, String username, String password) {
		ReflectionTestUtils.setField(dto, "username", username);
		ReflectionTestUtils.setField(dto, "password", password);
	}
  • 위와 같이 메서드를 만들어두고 테스트 코드에서 해당 객체를 생성하고 이 메서드를 호출하면된다.
// given
UserRequsetDto userRequsetDto = new UserRequsetDto();
etDto(userRequsetDto, TEST_USER_NAME, TEST_USER_PASSWORD);

profile
안녕하세요

0개의 댓글