ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • ReflectionTestUtils를 활용한 단위 테스트, 객체의 ID 설정하기
    Spring/Trouble Shooting 2023. 11. 19. 01:21

    문제 상황

    질문의 번호와 유저의 번호를 받고, 질문의 작성자 번호가 유저의 번호와 같다면 삭제하는 과정이다.

    public void deleteById(Long questionId, Long memberId) {  
    	Question question = findById(questionId);  
    	if (validateQuestionWriter(memberId, question)) {  
    		questionRepository.deleteById(questionId);  
    	}  
    }  
      
    private Boolean validateQuestionWriter(Long memberId, Question question) {  
    	return question.getMember().getId().equals(memberId);  
    }
    

    그다지 이해하기 어려운 상황은 아니다.

    @Test  
    @DisplayName("성공한다.")  
    void success() {  
    	//given  
    	Member member = getMember(getOauth2Entity("123", SocialProvider.KAKAO));  
    	Question question = getQuestion(  
    					member,  
    					getMbtiEntity("ENTP"),  
    					"질문 내용");  
    	  
    	Long mockQuestionId = 1L;  
    	Long mockMemberId = 1L;  
    	  
    	given(questionRepository.findById(mockQuestionId))  
    		.willReturn(Optional.of(question));  
    	  
    	//when  
    	questionService.deleteById(mockQuestionId, mockMemberId);  
    	  
    	//then  
    	verify(questionRepository, times(1)).deleteById(mockQuestionId);  
    }
    
    
    private Member getMember(Oauth2Entity oauth2Entity) {  
    	Member member = Member.builder()  
    					.oauth2Entity(oauth2Entity)  
    					.build();
    return member;  
    }
    

    일단 필요한 테스트 코드를 작성하고 나서 잘 돌아가는지 테스트를 해보면

    java.lang.NullPointerException: Cannot invoke “java.lang.Long.equals(Object)” because the return value of “…Member.getId()” is null

    NPE가 발생하게 된다.

    private 메소드에 필요한 파라미터로 Long memberId이 필요하기 때문이다.
    하지만 member를 생성하는 빌더 패턴을 보면 Id를 사용하지 않는다.
    JPA의 GenerationType.IDENTITY 으로 설정 해뒀기 때문에 비즈니스 로직에서 DB에 저장하고 조회해오지 않는 이상 인덱스로 활용되는 Id를 임의로 조작하여 생성할 필요는 없기 때문이다.

    그렇기에 현재 member의 id는 null이다. 똑같은 원리로 question의 id도 null이다.

    null이 null과 equals를 활용해 같은지 판단하니 NPE가 발생하게 되는 것이다.

    문제 해결

    그렇다면 id를 어떻게 설정해줄 것인가?
    Setter를 설정해줘서 테스트 상황에서만 사용한다? => 본인은 안사용할지 몰라도 누군가는 쓸텐데?
    Builder 패턴에 Id를 넣는다? => 임의로 id값 저장하면 이후 발생할 후폭풍은 어떻게 감당할건지?

    즉, 현재 만들어져있는 로직을 수정하지 않고 테스트 상에서만 쓸 수 있도록 해야한다.
    이를 위한 방법으로 ReflectionTestUtils을 사용해볼까 한다.

    ReflectionTestUtils 이란?

    ReflectionTestUtils는 Spring Framework에서 제공하는 유틸리티 클래스로, Java의 Reflection 기능을 쉽게 사용할 수 있도록 도와준다.

    그 전에 Reflection이란, 컴파일 시간에 알 수 없는 클래스나 메소드에 대해 런타임 시간에 접근하는 기능을 의미한다.

    간단하게 런타임에 private로 선언된 값을 수정할 수 있는 자바에서 제공해주는 기능(API)라고 이해하면 된다.

    적용 예시

    바로 활용해보자

    @Test  
    @DisplayName("성공한다.")  
    void success() {  
    	//given  
    	Member member = getMember(getOauth2Entity("123", SocialProvider.KAKAO));  
    	Question question = getQuestion(
    				member,  
    				getMbtiEntity("ENTP"),  
    				"질문 내용");  
    	  
    	Long mockQuestionId = 1L;  
    	Long mockMemberId = 1L;  
    
    	//전
    	System.out.println("question.getId() = " + question.getId());  
    	System.out.println("question.getMember().getId() = " + question.getMember().getId());  
    	System.out.println("member.getId() = " + member.getId());  
    	
    	ReflectionTestUtils.setField(member, "id", mockMemberId);  
    	ReflectionTestUtils.setField(question, "id", mockQuestionId);  
    
    	//후
    	System.out.println("question.getId() = " + question.getId());  
    	System.out.println("question.getMember().getId() = " + question.getMember().getId());  
    	System.out.println("member.getId() = " + member.getId());
    	given(questionRepository.findById(mockQuestionId))  
    		.willReturn(Optional.of(question));  
    	  
    	//when  
    	questionService.deleteById(mockQuestionId, mockMemberId);  
    	  
    	//then  
    	verify(questionRepository, times(1)).deleteById(mockQuestionId);  
    }
    
    

    ReflectionTestUtils로 각 id를 설정하기 전과 후를 출력해보자

    question.getId() = null
    question.getMember().getId() = null
    member.getId() = null
    question.getId() = 1
    question.getMember().getId() = 1
    member.getId() = 1
    

    손쉽게 Id를 설정해서 테스트를 수행할 수 있다.

    근데 Record에서는…

    java 14부터 사용되는 record를 DTO로 활용하여 개발할 수 있게된다.
    record는 불변 객체로 값을 수정하지는 못한다.
    즉 DTO에 어떤 값을 ReflectionTestUtils로 수정하려고 해도 못한다는 의미이다.
    이 점 유의해서 테스트에 사용하면 좋을것 같다.

    결론

    테스트 하나 하겠다고 Setter, Builder 패턴 수정해서 비지니스 로직 바꾸지 말고 ReflectionTestUtils를 활용해서 단위 테스트를 손쉽게 해볼 수 있다.

    댓글

Designed by black7375.