[SpringBoot] 테스트 환경 구축(MySQL docker)

2024. 3. 22. 10:27Spring

설명

로컬 MySQL DB를 이용한 테스트환경 구축을 정리했습니다.

 

Docker 구성

1. Pull image

docker pull --platform linux/amd64 mysql:8.0.28
  • 기술적으로는 문제(ARM에서 MySQL 실행)가 해결되지는 않지만 당분간은 platform을 amd로 사용해야한다.

2. Docker run

docker volume create mysql_data
docker run --name mysql -itd \
    -p 13306:3306 \
    -e MYSQL_ROOT_PASSWORD=password \
    -v mysql_data:/var/lib/mysql \
    --restart unless-stopped \
    mysql:8.0.28
  • m1의 경우 해당 옵션 추가 --platform linux/x86_64
  • 버전은 운영환경의 DB와 동일하게 설정
  • docker volume 설정을 통해 데이터를 영속적으로 저장

3. DDL 추출

  • 테스트환경 테이블 구성을 위함.
sudo mysqldump -h [호스트명] -u[유저명] -p[패스워드] -P [포트번호] --all-databases --no-data  > ~/db_ddl.sql

4. DDL dump 적용

sudo mysql --host=127.0.0.1 -P 13306 -u root -p < ~/db_ddl.sql

 

  • Amazon Aurora 사용 시에는 AWS AREPLICA_HOST_STATUS 에러가 발생할 수 있으나 Amazon Aurora 관련 테이블 임으로 무시.
  •  

AREPLICA_HOST_STATUS 관련 에러

5. 테이블 생성 확인

 

테스트 환경 구축

1. test.resource.application.yml 추가

spring:
  application:
    name: test
  datasource:
    url: jdbc:mysql://localhost:13306
    username: root
    password: catsstudent123
    driver-class-name: com.mysql.cj.jdbc.Driver
  jpa:
    database: mysql
    database-platform: org.hibernate.dialect.MySQL5InnoDBDialect
    hibernate:
      ddl-auto: none
    show-sql: true

2. test.resource.application-test.yml 추가

  1. 해당 yml은 @SpringBootTest + @ActiveProfiles("test") 형태로 사용하기 위함.
  2. datasource는 로컬 DB 처리
    1. SpringBootTest 사용 시 개발환경은 master, slave 구성이 되어있기 때문에 DataSource 관련 내용을 전부 모킹처리를 해야하는데 불편함을 제거하기 위해 local MySQL을 보도록 수정
# JWT Secret Key
security:
  jwt:
    base64-secret: test!@#@!#!@#@!#@!#
        
test:
  cloud-front-url: localhost
  datasource:
    master:
      driver-class-name: com.mysql.cj.jdbc.Driver
      jdbc-url: jdbc:mysql://localhost:13306/test
      username: root
      password: password
      read-only: false
    slave:
      driver-class-name: com.mysql.cj.jdbc.Driver
      jdbc-url: jdbc:mysql://localhost:13306/test
      username: root
      password: password
      read-only: true

3. 유저 로그인 처리

WithCustomMockUser 인터페이스 구성

package com.test.mock.security;


import org.springframework.security.test.context.support.WithSecurityContext;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

@Retention(RetentionPolicy.RUNTIME)
@WithSecurityContext(factory = WithMockCustomUserSecurityContextFactory.class)
public @interface WithCustomMockUser {

}

WithMockCustomUserSecurityContextFactory 구성

  • AuthenticationProvider 구현체의 authenticate 매서드를 참고하여 로그인 처리
public class WithMockCustomUserSecurityContextFactory implements WithSecurityContextFactory<WithCustomMockUser> {
    @Override
    public SecurityContext createSecurityContext(WithCustomMockUser annotation) {
     	SecurityContext contxt = SecurityContextHolder.createEmptyContext();
		// AuthenticationProvider 구현체의 authenticate 매서드를 참고하여 로그인 처리
        return contxt;
    }
}

 

JPA 테스트 코드 예시

@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@Import({PersistenceConfig.class, QueryDslConfig.class})
class NoteRepositoryTest {
    @Autowired
    NoteRepository NoteRepository;

    private List<Note> createMyNote(Long max_student_id) {
        List<Note> noteList = new ArrayList<>();
        LongStream.rangeClosed(1L, max_student_id)
                .forEach(i -> {
                    String myNoteId = StringUtil.getUUID();
                    System.out.println(i);
                    System.out.println(myNoteId);
                    Note Note = Note.builder()
                            .myNoteId(myNoteId)
                            .studentId(i)
                            .build();
                    noteList.add(Note);
                    Note.save(Note);
                });

        return noteList;
    }
    
    @Test
    @DisplayName("노트 목록조회(in student_id)")
    void findByNoteIdInAndStudentId() {
        // Note 생성
        // studentId 1~3학생
        List<Note> myNotes1 = createMyNote(3L);
        // Note 생성
        // studentId 1 학생
        List<Note> myNotes2 = createMyNote(1L);

        // 테스트 대상 회원
        Long testStudent = myNotes2.get(0).getStudentId();

        // 대상 회원의 Note id 목록
        List<String> testMyNoteIds = Stream.of(myNotes1, myNotes2)
                .flatMap(Collection::stream)
                .filter(note -> note.getStudentId().equals(testStudent))
                .map(Note::getMyNoteId)
                .collect(Collectors.toList());
        // 대상 회원의 Note 조회
        List<Note> myNotesOfTestStudent = NoteRepository.findByMyNoteIdInAndStudentId(testMyNoteIds, testStudent);
        // 전체 조회
        List<Note> myNotes = mrMyNoteRepository.findAll();
        assertThat(myNotes.size()).isEqualTo(4);
        assertThat(myNotesOfTestStudent.size()).isEqualTo(2);
    }
}
  • @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
    • 기본적으로 embedded H2 로 설정되어 있기 때문에 local MySQL로 설정하기 위해 Replace None 처리
    • @Import({PersistenceConfig.class, QueryDslConfig.class})
      • PersistenceConfig
        • @EnableJpaAuditing(auditorAwareRef="auditorProvider")
        • AuditorProvider bean 등록
          • AuditorAware 구현체
      • QueryDslConfig
        • EntityManager를 @PeristenceContext로 등록
          • EntityManager를 Proxy로 감싸서 Thread-Safe를 보장
        • JPAQueryFactory Bean 등록

Controller 테스트 예시

@ActiveProfiles("test")
@SpringBootTest
@AutoConfigureMockMvc
class NoteControllerTest {
    @Autowired
    private MockMvc mockMvc;

    @MockBean
    private NoteService noteService;

    @Test
    @WithCustomMockUser
    void getNote() throws Exception {
        // getNote 응답값 Mocking
        String noteId = "1";
        noteResponse noteResponse = NoteResponse.builder()
                .myNoteId(myNoteId)
                .build();

        given(noteService.getNote(noteId))
                .willReturn(noteResponse);

        // MockUser Controller 테스트
        String token = "accessToken";
        mockMvc.perform(MockMvcRequestBuilders.get("/v1/my/note/1/")
                        .header(HttpHeaders.AUTHORIZATION, "Bearer " + token)
                        .contentType(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk())
                .andExpect(content().contentType(MediaType.APPLICATION_JSON))
                .andExpect(jsonPath("$.noteId").value(noteResponse.getNoteId()));
    }
}
  • @SpringBootTest
    • 애플리케이션의 모든 Bean을 다 등록.
    • 속도 이슈 때문에 @WebMvcTest를 사용해야하지만 @Import 시 import한 클래스에 @Value 가 동작하지 않는 이슈 때문에 SpringBootTest 사용.
      • 위와 같은 이슈가 없다면 WebMvcTest를 사용하고 Profile 설정을 제거해도 됨
  • @ActiveProfiles("test")
    • applicaion-test.yml 파일을 읽도록 설정
      • applicaion.yml 이 먼저 읽히고 다음  applicaion-test.yml을 읽는다.(Property가 중복이 되면 안됨)
  • @AutoConfigureMockMvc
    • MockMvc 설정
  • @WithCustomMockUser