이 글은 최주호 님의 Infrean 강의를 학습한 내용을 정리하여 작성합니다.
@Sql teadown.sql 적용
PK 초기화
- 현재 테스트 구조는 다음과 같다.
- Test1과 Test2가 존재할 때 @BeforeEach 애노테이션을 사용해 더미 데이터로 Member 객체 ssar과 cos를 넣어준다.
- 이때 실행순서는 BeforeEach -> Test1 -> BeforeEach -> Test2 이다.
- 그러나 현재 테스트 코드에 @Transactional 애노테이션을 적용하였으므로 각 테스트 수행 후 롤백이 수행된다.
- 이때 문제가 존재한다.
- 처음 BeforeEach에서 PK 1과 2를 가지는 두 Member 객체가 생성된다.
- @Transactional 애노테이션에 의해 Test1이 수행되고 두 더미 데이터는 롤백된다.
- Test2가 실행 되기 전 다시 BeforeEach가 실행되어 PK로 1과 2가 아닌 3과 4를 가지는 두 Member 객체가 생성된다.
- 즉, auto-increment가 초기화 되지 않는다.
- 만약 테스트 코드에 더미 데이터로 넣어둔 Member 객체를 조회할 일이 생길 때 Pk를 알지 못해 에러가 발생한다.
- 이때 문제가 존재한다.
AccountControllerTest
@ActiveProfiles("test")
@AutoConfigureMockMvc
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
@Transactional
class AccountControllerTest extends DummyObject {
@Autowired
private MockMvc mvc;
@Autowired
private ObjectMapper om;
@Autowired
private UserRepository userRepository;
@Autowired
private AccountRepository accountRepository;
@Autowired
private EntityManager em;
@BeforeEach
public void setUp() {
User ssar = userRepository.save(newUser("ssar", "쌀"));
User cos = userRepository.save(newUser("cos", "코스"));
Account ssarAccount = accountRepository.save(newAccount(1111L, ssar));
Account cosAccount = accountRepository.save(newAccount(2222L, cos));
em.clear();
}
// jwt token -> 인증필터 -> 시큐리티 세션생성
// 헤더에 토큰이 없으므로 JwtAuthorizationFilter를 통과한다.
// setupBefore=TEST_METHOD (setUp 메서드 실행전에 수행)
// setupBefore=TEST_EXECUTION (saveAccount_test 메서드 실행전에 수행)
@WithUserDetails(value = "ssar", setupBefore = TestExecutionEvent.TEST_EXECUTION) // 디비에서 username=ssar 조회해서 세션에 담아주는 어노테이션!!
@Test
public void saveAccount_test() throws Exception {
// given
AccountSaveReqDto accountSaveReqDto = new AccountSaveReqDto();
accountSaveReqDto.setNumber(9999L);
accountSaveReqDto.setPassword(1234L);
String requestBody = om.writeValueAsString(accountSaveReqDto);
System.out.println("requestBody = " + requestBody);
// when
ResultActions resultActions =
mvc.perform(post("/api/s/account").content(requestBody).contentType(MediaType.APPLICATION_JSON));
String responseBody = resultActions.andReturn().getResponse().getContentAsString();
System.out.println("responseBody = " + responseBody);
// then
resultActions.andExpect(status().isCreated());
}
@WithUserDetails(value = "ssar", setupBefore = TestExecutionEvent.TEST_EXECUTION) // 디비에서 username=ssar 조회해서 세션에 담아주는 어노테이션!!
@Test
public void deleteAccount_test() throws Exception {
// given
Long number = 1111L;
// when
ResultActions resultActions =
mvc.perform(delete("/api/s/account/" + number));
String responseBody = resultActions.andReturn().getResponse().getContentAsString();
System.out.println("responseBody = " + responseBody);
// then
// junit 테스트에서 delete 쿼리는 가장 마지막에 실행되면 발동안됨.
assertThrows(CustomApiException.class, () -> accountRepository.findByNumber(number).orElseThrow(
() -> new CustomApiException("계좌를 찾을 수 없습니다.")
));
}
}
실행 쿼리
- AccountControllerTest를 수행하면 다음과 같다.
첫 번째 BeforeEach
- ssar의 PK는 1, cos의 PK는 2인 것을 확인할 수 있다.
두 번째 BeforeEach
- 첫 번째 테스트 수행 후 더미 데이터는 롤백된다.
- 두 번째 BeforeEach 수행 결과 ssar의 PK는 1에서 3으로, cos의 PK는 2에서 4로 변화된 것을 볼 수 있다.
- 이러한 결과가 발생하는 이유는 AUTO-INCREMENT 초기화가 되지 않았기 때문이다.
- 이러한 문제를 해결하기 위해서는 테스트 코드에 @Transactional 애노테이션 대신 쿼리를 실행시켜야 한다.
- 쿼리를 실행해 롤백이 아니라 테이블 자체를 drop 시키도록 한다.
코드 리팩토링
1. resources 디렉터리 아래 새로운 db 디렉터리를 생성하고 teardown.sql 파일을 생성한다.
2. teardown.sql 작성
teardown.sql
SET REFERENTIAL_INTEGRITY FALSE; -- 모든 제약 조건 비활성화
drop table transaction_tb;
drop table account_tb;
drop table user_tb;
SET REFERENTIAL_INTEGRITY TRUE; -- 모든 제약 조건 활성화
- trasaction_tb, account_tb, user_tb는 모두 foreign key로 제약 조건이 걸려있다.
- 이러한 이유로 drop 순서가 중요한데 SET REFERENTIAL_INTEGRITY FALSE; 를 사용해 모든 제약 조건을 비활성화 시킨다.
- 모든 테이블을 drop 완료 했다면 SET REFERENTIAL_INTEGRITY TRUE; 를 통해 다시 모든 제약 조건을 활성화 시켜준다.
- 이렇게 하면 각 테스트 코드가 수행된 후 데이터를 롤백하는 것이 아니라 테이블 자체를 drop 시킨다.
- 그러면 BeforeEach가 다시 실행될 때 테이블 자체가 존재하지 않으므로 테이블을 다시 create한다.
- 하지만 우리가 원하는 건 BeforeEach가 실행될 때마다 다시 테이블을 생성하는 것이 아니라 AUTO-INCREMENT만 초기화 하는 것이다.
- teardown.sql 파일을 다음과 같이 변경한다.
SET REFERENTIAL_INTEGRITY FALSE; -- 모든 제약 조건 비활성화
truncate table transaction_tb;
truncate table account_tb;
truncate table user_tb;
SET REFERENTIAL_INTEGRITY TRUE; -- 모든 제약 조건 활성화
- 이 SQL 쿼리는 데이터베이스에서 참조 무결성 제약 조건을 일시적으로 비활성화한 후, 특정 테이블인 transaction_tb, account_tb, user_tb의 모든 데이터를 삭제하는 작업을 수행하고, 마지막으로 참조 무결성 제약 조건을 다시 활성화하는 작업을 수행한다.
- 테이블이 아닌 테이블에 존재하는 모든 데이터를 삭제한다.
- 이 쿼리는 주의해서 사용해야 한다.
- 이전 데이터를 복구하지 못할 수 있다.
- 또한 데이터의 무결성을 보장하기 위해 가능한한 자주 참조 무결성 제약 조건을 비활성화하는 것은 피해야 한다.
- 이 쿼리는 테스트나 개발 목적으로만 사용해야 하며, 운영 환경에서는 사용하지 않는 것이 좋다.
참고
참조 무결성 제약 조건은 데이터베이스에서 데이터의 일관성과 정확성을 보장하는 중요한 제약 조건이다.
이 제약 조건은 일반적으로 외래 키 제약 조건으로 구현되며, 관련된 두 테이블 간의 데이터 일관성을 유지하기 위해 사용된다.
예를 들어, transaction_tb 테이블의 데이터가 account_tb 테이블의 데이터와 일치하지 않으면, 이러한 데이터 무결성 위반은 참조 무결성 제약 조건으로 확인된다.
- 이제 BeforeEach를 수행할 때마다 테이블을 생성하지 않고 AUTO-INCREMENT만 초기화된다.
3. 테스트 코드에 다음과 같이 @Sql 애노테이션을 추가한다.
- @Sql 어노테이션은 스프링 JUnit5에서 데이터베이스를 초기화하거나 테스트 전후에 SQL 스크립트를 실행할 때 사용된다.
- @Sql 애노테이션은 BeforeEach 실행 직전 실행된다.
- "classpath:db/teardown.sql"는 클래스패스 경로에 있는 db 폴더 내부의 teardown.sql 파일을 나타낸다.
- 이 파일은 데이터베이스에서 사용되는 테이블을 삭제하는 등의 작업을 수행하는 SQL 스크립트이다.
- 따라서 @Sql("classpath:db/teardown.sql")는 해당 테스트 클래스 또는 해당 테스트 메소드가 실행되기 전에 teardown.sql 파일 내부의 SQL 스크립트가 데이터베이스에서 실행되어 데이터베이스를 초기화하거나 테스트 데이터를 삭제하는 등의 작업을 수행한다는 것을 나타낸다.
수정 후
두 번째 BeforeEach
- 이전과 달리 더미 데이터의 PK가 초기화되어 1부터 다시 시작함을 알 수 있다.
주의!!
@SpringBootTest 애노테이션을 사용하는 테스트 코드에는 모두 teardown.sql을 사용해주도록 한다.
'스프링부트 JUnit 테스트 > Bank 애플리케이션' 카테고리의 다른 글
서비스 테스트에 관하여 생각해보기 (0) | 2023.05.05 |
---|---|
Jwt 토큰 만료시간 버그 잡기 (0) | 2023.04.30 |
계좌등록 컨트롤러 테스트 (0) | 2023.04.08 |