본문 바로가기
Java/Junit

[Spring] JUnit5 기반 기능 구현해보기 - Repository 단위 테스트

by tableMinPark 2023. 8. 31.

사전 준비

  • 서브 도메인 브랜치 생성 후 Entity 생성
  • Entity와 대응하는 Repository 생성

Entity와 Repository 생성


테스트 환경만의 application-test.yml

spring:
  config:
    activate:
      on-profile: test			# 테스트 클래스에서 설정 파일을 구분하기 위한 프로필명을 설정

  datasource:
    url: jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;DATABASE_TO_UPPER=false;MODE=MySQL
    username: sa
    password:
    driver-class-name: org.h2.Driver

  jpa:
    hibernate:
      ddl-auto: create-drop		# 매 실행마다 데이터베이스를 삭제하고 새로 생성하는 설정
    properties:
      hibernate:
        dialect: org.hibernate.dialect.MySQL8Dialect
        format_sql: true
        globally_quoted_identifiers: true
    show_sql: true
    generate-ddl: true
    database-platform: org.hibernate.dialect.MySQL8Dialect
    database: h2				# h2 데이터베이스 지정

  redis:						# redis 설정
    host: 127.0.0.1
    port: 6379
    password: auth.!

  h2:
    console:
      enabled: true

jwt:
  secret: 999d95876cb0e556f8546fbee8356a3e919cf761b863c5efd39b1672cef5b6d0c7a40a29b176169aa1ab705f38fa1cf7979f9815fef6d32c2e30be9d980f5d41
  expired:
    access_token: 3
    refresh_token: 5

@TestPropertySource란?

테스트 케이스에서 원하는 시나리오를 설정하기 위해 특정 설정을 사용해야 하는 경우가 있습니다. 이러한 상황에서 @TestPropertySource 어노테이션을 이용해 프로젝트에서 사용되는 다른 설정 파일보다 우선하여 적용할 수 있습니다. 

 

1. @TestPropertySource 사용 시 주의점

하지만 yml 형식의 설정 파일인 경우, 테스트를 실행하게 되면 직접 지정한 "application-test.yml" 이 아니라 기본 값인 "application.yml" 파일을 참조하게 되어 에러가 발생할 수도 있습니다.

위 에러는 개발 환경에서 사용하는 Mysql에 Auto_Increment 값 초기화하는 SQL을 H2 문법으로 실행시켜서 발생했습니다.

 

이를 해결하기 위해서는 "application-test.yml" 에서 지정한 "on-profile" 값을 테스트 클래스에 @ActiveProfile 어노테이션을 통해 프로필을 지정해주면 application-test.yml를 적용할 수 있습니다.

 

2. @AutoConfigureTestDatabase

테이블을 ddl-auto를 통해 자동 생성하게끔 했지만 테이블을 찾을 수 없어서 에러가 발생한다.

테스트 환경에서는 H2를 MySQL 모드로 사용할 수 있게 기본 H2 In-memory가 아닌 H2의 Mode와 hibernate의 Dialect를 MYSQL 설정하여 사용하려고 했습니다. 그래서 application-test.yml에 각종 설정을 했지만, 적용이 되지 않고 자동으로 구성된 H2 In-memory를 사용하는 문제점이 발생했습니다.

 

기본적으로 내장되어 있는 DataSource와 Connection를 구현체로 테스트를 진행하기 때문에 생기는 문제였습니다.

이 문제점은 @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) 이 설정을 사용하면 해결할 수 있습니다. 위 설정은 자동으로 된 설정을 replace 해서 해당 설정이 동작하지 않고, 내가 설정한 파일대로 만들어진 DataSource가 Bean으로 등록되게끔 합니다.

 

3. 테스트 코드

@DataJpaTest
@ActiveProfiles("test")
@TestPropertySource(locations = "classpath:application-test.yml")
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
class MemberRepositoryTest {
	@BeforeEach
    void registerMember() {
        this.entityManager.createNativeQuery("ALTER TABLE member ALTER member_id RESTART WITH 1").executeUpdate();
        Member member = Member.builder()
                .email(EMAIL)
                .password(PASSWORD)
                .build();

        memberRepository.save(member);
    }
        
    @DisplayName("Member 생성 테스트")
    @Test
    void memberRegisterTest() {
        assertTrue(true);
    }
}

Member 생성 단위 테스트 성공


H2 기반 in-memory 테스트 환경 구축

1. 의존성 설정

dependencies {
	testImplementation 'com.h2database:h2'
}

2. application-test.yml

  ...

  datasource:
    url: jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;DATABASE_TO_UPPER=false;MODE=MySQL
    username: sa
    password:
    driver-class-name: org.h2.Driver
  jpa:
    hibernate:
      ddl-auto: create-drop
    properties:
      hibernate:
        dialect: org.hibernate.dialect.MySQL8Dialect
        format_sql: true
        globally_quoted_identifiers: true
    show_sql: true
    generate-ddl: true
    database-platform: org.hibernate.dialect.MySQL8Dialect
    database: h2
  h2:
    console:
      enabled: true

  ...

@DataJpaTest란? 

JPA에 관련된 요소들만 테스트하기 위한 어노테이션으로 JPA 테스트에 관련된 설정들만 적용해주는 특징이 있습니다.

@DataJpaTest 어노테이션은 내부적으로 @Transactional을 포함하고 있기 때문에 데이터베이스 롤백을 위해 테스트 메소드 마다 별도로 @Transactional 어노테이션을 붙힐 필요가 없습니다. 

@DataJpaTest
@ActiveProfiles("test")
@TestPropertySource(locations = "classpath:application-test.yml")
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
class MemberRepositoryTest {
	@BeforeEach
    void registerMember() {
        this.entityManager.createNativeQuery("ALTER TABLE member ALTER member_id RESTART WITH 1").executeUpdate();
        Member member = Member.builder()
                .email(EMAIL)
                .password(PASSWORD)
                .build();

        memberRepository.save(member);
    }
        
    @DisplayName("Member 생성 테스트")
    @Test
    // @Transactional 없이도 자동으로 데이터베이스 롤백을 수행합니다.
    void memberRegisterTest() {
        assertTrue(true);
    }
}

단위 테스트 케이스

1. @BeforeEach

void registerMember() {
    this.entityManager.createNativeQuery("ALTER TABLE member ALTER member_id RESTART WITH 1").executeUpdate();
    Member member = Member.builder()
            .email(EMAIL)
            .password(PASSWORD)
            .build();

    memberRepository.save(member);
}

데이터베이스에서 PK값이 Auto_Increment로 자동 생성되기 때문에 테스트 시작 전 1로 초기화하는 로직과 회원을 등록하는 로직을 수행하는 메소드입니다.

 

2. 데이터 삽입

@DisplayName("Member 생성 테스트")
@Test
void memberRegisterTest() {
    assertTrue(true);
}

데이터 삽입은 @BeforeEach에서 데이터 삽입(회원 등록)이 발생하기 때문에 예외가 발생하는지만 확인하기 위해서 위와 같이 코드를 작성했습니다.

 

3. 데이터 조회

@DisplayName("Member 조회 테스트")
@Test
void memberFindTest() {
    Optional<Member> op = memberRepository.findById(MEMBER_ID);
    assertTrue(op.isPresent());

    Member member = op.get();
    assertEquals(member.getMemberId(), MEMBER_ID);
    assertEquals(member.getEmail(), EMAIL);
    assertEquals(member.getPassword(), PASSWORD);
}

데이터 조회는 @BeforeEach에서 삽입한 데이터를 가져와서 값이 일치하는지 확인하는 과정을 통해 테스트 케이스를 진행합니다.

 

4. 데이터 수정

@DisplayName("Member 수정 테스트")
@Test
void memberModifyTest() {
    Optional<Member> op = memberRepository.findById(MEMBER_ID);
    assertTrue(op.isPresent());

    String modifyEmail = "modify@test.com";
    String modifyPassword = "5678";

    Member member = op.get();
    member.setEmail(modifyEmail);
    member.setPassword(modifyPassword);
    memberRepository.flush();

    op = memberRepository.findById(MEMBER_ID);
    assertTrue(op.isPresent());
    member = op.get();
    assertEquals(modifyEmail, member.getEmail());
    assertEquals(modifyPassword, member.getPassword());
}

데이터 수정은 @BeforeEach에서 삽입한 데이터를 가져와서 수정하고, flush()로 데이터베이스로 플러싱을 진행합니다. 이후 다시 데이터베이스에서 데이터를 조회하고 수정한 값과 일치하는지 여부를 판별합니다.

 

5. 데이터 삭제

@DisplayName("Member 삭제 테스트")
@Test
void memberDeleteTest() {
    memberRepository.deleteById(MEMBER_ID);

    Optional<Member> op = memberRepository.findById(MEMBER_ID);
    assertFalse(op.isPresent());
}

데이터 삭제는 @BeforeEach에서 삽입한 데이터를 삭제하고, 다시 조회했을 때 데이터 유무를 판단하는 과정으로 테스트 케이스가 진행됩니다.

 

전체 코드

@DataJpaTest
@ActiveProfiles("test")
@TestPropertySource(locations = "classpath:application-test.yml")
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
class MemberRepositoryTest {
    @Autowired
    private EntityManager entityManager;
    @Autowired
    private MemberRepository memberRepository;
    private final String EMAIL = "test@test.com";
    private final String PASSWORD = "1234";
    private final Long MEMBER_ID = 1L;

    @BeforeEach
    void registerMember() {
        this.entityManager.createNativeQuery("ALTER TABLE member ALTER member_id RESTART WITH 1").executeUpdate();
        Member member = Member.builder()
                .email(EMAIL)
                .password(PASSWORD)
                .build();

        memberRepository.save(member);
    }

    @DisplayName("Member 생성 테스트")
    @Test
    void memberRegisterTest() {
        assertTrue(true);
    }

    @DisplayName("Member 조회 테스트")
    @Test
    void memberFindTest() {
        Optional<Member> op = memberRepository.findById(MEMBER_ID);
        assertTrue(op.isPresent());

        Member member = op.get();
        assertEquals(member.getMemberId(), MEMBER_ID);
        assertEquals(member.getEmail(), EMAIL);
        assertEquals(member.getPassword(), PASSWORD);
    }

    @DisplayName("Member 수정 테스트")
    @Test
    void memberModifyTest() {
        Optional<Member> op = memberRepository.findById(MEMBER_ID);
        assertTrue(op.isPresent());

        String modifyEmail = "modify@test.com";
        String modifyPassword = "5678";

        Member member = op.get();
        member.setEmail(modifyEmail);
        member.setPassword(modifyPassword);
        memberRepository.flush();

        op = memberRepository.findById(MEMBER_ID);
        assertTrue(op.isPresent());
        member = op.get();
        assertEquals(modifyEmail, member.getEmail());
        assertEquals(modifyPassword, member.getPassword());
    }

    @DisplayName("Member 삭제 테스트")
    @Test
    void memberDeleteTest() {
        memberRepository.deleteById(MEMBER_ID);

        Optional<Member> op = memberRepository.findById(MEMBER_ID);
        assertFalse(op.isPresent());
    }
}