Backend/Testing

SpringBoot + JUnit으로 JPA 테스트하기 (mysql, mongodb)

얌얌념념 2023. 2. 1. 16:59

1. build.gradle 설정

dependencies {
	...
	// Junit5
	testImplementation 'org.junit.jupiter:junit-jupiter'
	testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1'
	testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.1'
}

test {
	useJUnitPlatform {
		includeTags 'fast'
		excludeTags 'slow'
	}
}

build.gradle의 dependencies와 test에 위의 코드를 추가한다.

 

2. application.yml 설정

원하는 DB를 연결하면 된다. 나는 MySQL과 MongoDB를 함께 사용하였다.

 

3. Test Package 만들기

src/test/java 밑에 테스트 패키지를 생성해준다.

 

4. Test Class 생성

JUnit5에서는 JUnit4의 @RunWith의 어노테이션 대신에 다음의 어노테이션을 달아주어야 한다.

  • @ExtendWith(SpringExtension.class)

그 외에도 @DisplayName(str): 클래스를 설명할 수 있음 등 다양한 어노테이션이 있지만, 지금 이 프로젝트에서 반드시 알아야 할 세가지 어노테이션이 있다.

  • @DataJpaTest: JPA 테스트시 사용
  • @DataMongoTest: MongoDB 테스트시 사용

jpa 말고도 mvc 테스트를 할 수도 있고, mongoDB 대신 redis 등 다른 DB도 테스트할 수 있는데 모두 필요한 어노테이션이 다르다. 자세한 것은 스프링 공식 홈페이지를 확인하면 된다.

 

  • @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE): 테스트 시 사용할 데이터베이스 (@DataJpaTest + 테스트 용으로 외부 DB 사용시)

https://docs.spring.io/spring-boot/docs/current/api/org/springframework/boot/test/autoconfigure/orm/jpa/DataJpaTest.html

@DataJpaTest는 기본적으로 내장된 메모리 DB를 이용해 테스트를 실행하는데, 물리적인 MySQL DB로 테스트를 시도하면 문제가 발생한다. 그렇기 때문에 테스트 실행시 사용할 DB를 지정해야 하는데 그게 @AutoConfigureTestDatabase 어노테이션이다.

replace로 any, auto_configured, none 세 가지의 ENUM이 있는데,

  • any: 자동 구성, 수동 구성 상관 없이 DataSource 빈을 내장된 DB(H2, DERBY, HSQLDB)로 교체
  • auto_configured: 자동 구성된 경우에만 DataSource 빈 교체
  • none: DataSource 빈을 교체하지 않음

더 자세한 내용은 아래 블로그를 참고하면 된다. (MongoDB만 사용하면 필요 없음)

 

4-1. MySQL (MemberRepositoryTest)

Member.class

@Setter
@Getter
@Entity
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Table(name = "member", uniqueConstraints = {
        @UniqueConstraint(columnNames = "email")
})
public class Member {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @JsonIgnore
    private Long id;
    
    @NotNull
    @Column(nullable = false)
    private String name;

    @Email
    @Column(nullable = false)
    private String email;
}

 

MemberRepository.interface

@Repository
public interface MemberRepository extends JpaRepository<Member, Long> {
    Optional<Member> findByEmail(String email);
    Optional<Member> findByName(String name);  
}

 

MemberRepositoryTest.class

@ExtendWith(SpringExtension.class)
@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@DisplayName("Member Repository test case")
public class MemberRepositoryTest {
    @Autowired
    private MemberRepository memberRepository;
    @Test
    void findMemberByEmailTest(){
        memberRepository.save(Member.builder()
                .email("test@gmail.com")
                .name("test").build());
        Member member = memberRepository.findByEmail("test@gmail.com")
                .orElseThrow(()-> new BadRequestException("해당하는 사용자가 없습니다."));
        assertEquals("test", member.getName());
    }
}

위의 파일을 실행하면 정상적으로 테스트가 돌아간다.

 

4-2. MongoDB (BoardRepositoryTest)

Board.class

@Builder
@Getter
@Setter
@Document
public class Board {
    @Id
    private String id;
    private String writer;
    private String content;
    private String image_url;
    private String created_at;
}

 

BoardRepository.interface

@Repository
public interface BoardRepository extends MongoRepository<Board, String> {
//  "db.orders.find().sort({"created_date": -1"}).skip((page-1)*20).limit(20)"
    @Aggregation(pipeline = {
            "{'$sort':  {'created_at':  -1}}", /* create_at 기준 -1(내림차순) 정렬 */
            "{'$skip':  ?0}", /* 0번째 파라미터(start) 값만큼 skip */
            "{'$limit':  20}" /* 최대 20개 가져옴 */
    })
    List<Board> findN(int start);
}

위의 @Aggregation 어노테이션은 JPA의 @Query 어노테이션과 비슷한 역할을 한다.

해당 함수는 최근 순으로 정렬한 게시글을 page 부터 20개씩 끊어 가져오는 함수이다.

 

BoardRepositoryTest.class

@ExtendWith(SpringExtension.class)
@DataMongoTest
@DisplayName("Board Repository test case")
public class BoardRepositoryTest {
    @Autowired
    private BoardRepository boardRepository;
    
    @Test
    void getCardsTest(){
        int n = 10;
        for(int i=0; i<n; i++){
            boardRepository.save(Board.builder()
                    .writer("테스트" + (i + 1))
                    .content("테스트 컨텐트" + (i + 1))
                    .created_at("2022.02.22")
                    .build());
        }
        List<Board> boardList = boardRepository.findAll();

        assertEquals(n, boardList.size());
    }

    @Test
    void getNCardsTest(){
        int n = 30;
        for(int i=0; i<n; i++){
            boardRepository.save(Board.builder()
                    .writer("테스트" + (i + 1))
                    .content("테스트 컨텐트" + (i + 1))
                    .created_at("2022.02.22")
                    .build());
        }
        int page = 2;
        List<Board> boardList = boardRepository.findN(page);
        assertEquals(20, boardList.size());
        for(int i=1; i<n; i++){
            assertNotEquals(boardList.get(i-1).getId(), boardList.get(i).getId());
        }
    }
}

결과는 다음과 같이 나온다.

만약 assertion이 잘못되었다면 다음과 같이 내가 설정한 기댓값과 실제 결과값, 그리고 어디의 assertion에서 에러가 났는지도 알려준다.