1. 통합 테스트
- 실제 운영 환경에서 사용될 클래스들을 통합하여 테스트 한다.
- 단위 테스트와 같이 기능 검증을 위한 것이 아니라 spring framework에서 전체적으로 플로우가 제대로 동작하는지 검증하기 위해 사용 한다.
1.1 @SpringBootTest
- 스프링 부트는 @SpringBootTest 어노테이션을 통해 스프링부트 어플리케이션 테스트에 필요한 거의 모든 의존성을 제공한다.
- @SpringBootTest는 통합 테스트를 제공하는 기본적인 스프링 부트 테스트 어노테이션 이다.
- JUnit4 : @RunWith(SpringRunner.class)
- JUnit5 : 기입 X
- 장점
- 애플리케이션의 설정, 모든 Bean을 모두 로드하기 때문에 운영 환경과 가장 유사한 테스트가 가능하다.
- 전체적인 Flow를 쉽게 테스트 가능하다.
- 단점
- 애플리케이션의 설정, 모든 Bean을 모두 로드하기 때문에 시간이 오래걸리고 무겁다.
- 테스트 단위가 크기 때문에 디버깅이 어려운 편이다.
- 의존성 주입이 하나라도 잘못되면 springBootTest가 붙은 모든 테스트는 통과되지 않는다.
- bulid 폴더와 java 코드 폴더의 동기화에 대한 오류가 발생하여 의존성 주입할 클래스를 찾을 수 없다는 오류가 발생할 수도 있다.
- springBootTest 실행 중 ‘cannot be opened because it does not exist’ 오류
- 이는 클래스명이나 구조가 바꼈는데 build 폴더 내의 파일이 동기화가 되어 있지 않기 때문에 발생하는 문제다.
- 만약 테스트시 이러한 문제가 생기면 모든 모듈의 build 폴더를 다 삭제시켜주고 재빌드 하면 된다.
1.1.1 property
- 프로퍼티를 {key=value} 형식으로 직접 추가할 수 있다.
- 프로퍼티는 기본적으로 클래스 경로상의 application.properties(또는 application.yml)를 통해 애플리케이션 설정을 수행 한다.
- ex)
@SpringBootTest(properties = "testValue=test")
@Value("${testValue}") 를 통해 코드에서 property 값을 사용할 수 있다.
value 또한 테스트가 실행되기 전에 프로퍼티를 주입시킬 수 있다. 하지만 property와 함께 사용할 수는 없다.
1.1.2 webEnvironment
1.1.3 classes
@SpringBootTest는 기본적으로 SpringApplicationConfiguration 클래스를 찾아 로드한다.
@SpringBootApplication 가 위치한 클래스를 기준으로 로드한다.
- 이 어노테이션은 대체적으로 어플리케이션을 실행하는 곳에 위치해 있다.
- 그러나 어플리케이션을 실행하지 않는 모듈에서는 특정 위치를 지정해서
@SpringBootApplication를 위치시켜야 그 곳을 기준으로 정해 @SpringBootTest테스트가 실행된다.
@SpringBootTest에서 classes 속성을 사용하면 classes 에 적힌 @Configuration 클래스나 기타 클래스만 Application Context에 로드된다.
- 위의
@SpringBootApplication 기준으로 모든 Bean을 로드하고 싶지 않다면 사용하면 된다.
- 따라서 전체 애플리케이션의 설정, 모든 Bean을 모두 로드하지 않고 가볍게 테스트를 진행할 수 있다.
- 하지만 설정이나 의존성 주입이 누락된다면 오류를 발생시킨다.
1.1.4 exclude
- exclude 속성을 사용하면
@SpringBootApplication로 인해 자동으로 설정되는 모든 bean 중 특정 자동 설정을 제외할 수 있습니다.
1.2 TestRestTemplate
- webEnvironment 설정 시 그에 맞춰서 자동으로 설정되어 빈이 생성되며, RestTemplate의 테스트를 처리가 가능하다.
- webEnvironment 가 NONE이면 사용할 수 없다.
- Spring 4.x 이후부터 지원하는 Spring의 HTTP 통신 템플릿이다.
- HTTP 요청 후 Json, xml, String 과 같은 응답을 받을 수 있는 템플릿이다.
- Http request를 지원하는 HttpClient를 사용한다.
- ResponseEntity와 Server to Server 통신하는데 자주 쓰인다.
- ResponseEntity는 응답 처리시 값 뿐만 아니라 상태 코드, 응답 메세지 등을 포함하여 리턴 가능하다. HttpEntity를 상속받기 때문에 HttpHeader와 body를 가질 수 있다.
- Header, Content-Type등을 설정하여 외부 API 호출
1.3 @MockBean
- Mock 객체를 빈으로써 등록할 수 있다.
- @MockBean은 Spring의 ApplicationContext는 Mock 객체를 빈으로 등록하며, 혹시 @MockBean으로 선언된 객체와 같은 이름과 타입으로 이미 빈이 등록되어있다면 해당 빈은 선언한 @MockBean으로 대체된다.
1.4 @Transactional
- 테스트 완료 후 자동으로 rollback 처리 한다.
- spring-boot-test는 단순히 spring-test를 확장한 것이기 때문에
- @Test 와 함께 @Transactional 을 함께 사용하면 테스트가 끝날 때 rollback 처리한다.
- WebEnvironment.RANDOM_PORT, DEFINED_PORT를 사용하면 트랜잭션이 롤백되지 않는다.
- 실제 테스트는 별도의 스레드에서 진행되기 때문이다.
1.5 @ActiveProfiles
- 프로파일 전략을 사용 중이라면 원하는 프로파일 환경값 설정이 가능 하다.
1.6 @LocalServerPort
- 통합 테스트에서 사용되는 스프링 부트 애플리케이션의 임의 포트 번호를 가져오기 위한 어노테이션이다.
1.7 @WithMockUser, @WithAnonymousUser, @WithUserDetails
- TestRestTemplate를 통한 통합 테스트에서도 @WithMockUser, @WithAnonymousUser, @WithUserDetails를 통해서 시큐리티 필터를 통과할 수 있다.
아래의 코드는 위의 내용들을 조합해서 통합 테스트를 하는 예시 코드이다.
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class GreetingControllerIntegrationTest {
@LocalServerPort
private int port;
@Autowired
private TestRestTemplate restTemplate;
@Test
@WithMockUser
public void testGreetEndpoint() {
String url = "http://localhost:" + port + "/api/greet/John";
String response = restTemplate.getForObject(url, String.class);
assertThat(response).contains("Hello, John!");
}
}
2. Controller 테스트
2.1 테스트 방법
2.1.1 @WebMvcTest
- 실제 테스트 (통합 테스트)
- @AutoConfigureMockMvc를 통해 MockMVC를 자동 구성함.
- @WebMvcTest와 다르게 애플리케이션의 전체 클래스를 로드한다.
- 직접 스프링 컨테이너에 클래스를 로드할 필요가 없다.
- @Component, @Service, @Repository… 모두 로드함
- 하지만 의존성 문제나 빈 주입 단계에서 오류가 발생하면 @SpringBootTest가 있는 모든 테스트 클래스는 오류가 발생한다.
2.2 Spring Security 테스트 설정
-
@WithMockUser, @WithAnonymousUser, @WithUserDetails가 있다.
-
@WebMvcTest나 @SpringBootTest + @AutoConfigureMockMvc와 함께 controoler 테스트에서 사용한다.
-
인증된 사용자 or 미인증된 사용자를 만들어서 securityFilter를 통과하게 해주는 어노테이션이다.
-
특정한 사용자 또는 권한으로 시뮬레이트된 사용자로서의 인증을 제공한다.
-
securityFilter를 무시하는 것과는 관련이 없다.
-
mockmvc를 사용할 때도 securityFilter를 거쳐야 하는데 번거로운 인증 작업을 간편하게 처리해준다.
-
컨트롤러 테스트에서 Spring Security 설정하는 방법은 두가지가 있다.
- 기존에 만들어 놓았던 SecurityConfig 설정을 사용하면서 @WithMockUser와 같은 어노테이션을 사용하는 방법. 이는 시뮬레이트된 사용자로서의 인증을 하는 방법이다.
- 테스트용 SecurityConfig 클래스를 만들어서 HttpSecurity를 설정해서 모든 경로를 다 열어두는 방법. (컨트롤러 테스트는 컨트롤러에 대한 테스트에 집중하면 되기 때문
2.2.1 @WithMockUser
- 별도의 UserDetailsService와 같은 스텁을 제공하지 않아도 간단하게 인증정보를 설정하기 위한 어노테이션.
- @WithMockUser 어노테이션은 Controller 테스트 시에 Spring Security에 설정한 인증 정보를 제공해 주는 역할을 함.
- 사용자 인증 정보를 담은 Authentication을 UsernamePasswordAuthenticationToken으로 넣어주고, Principal은 User 객체에 넣어 SecurityContext에 보관해 준다.
- username, password, role의 기본 값은 각각 “user”, “password”, “ROLE_USER” 이다.
- username, password, role의 기본값을 변경할 수 있다.
- ex) @WithMockUser(username = "xxx", password = "xxx", roles = {"xxx"})
2.2.2 @WithAnonymousUser
- 익명유저의 인증정보를 설정하기 위한 어노테이션
- @WithMockUser와 반대로 인증되지 않은 사용자를 만들어서 사용해 준다.
- @WithMockUser에서 사용된 UsernamePasswordAuthenticationToken이 아닌 AnonymousAuthenticationToken을 Authentication에 담아서 보내준다.
2.2.3 @WithUserDetails
- UserDetailsService를 통해서 유저정보를 취득하여 설정하기 위한 어노테이션
- 사용자 정보가 @WithMockUser나 @WithAnonymousUser와 동일한 경우는 바로 사용하면 되지만, 추가적인 정보가 있다면 UserDetailsService를 구현한 서비스에서 사용자 정보를 load 하도록 설정해 줄 수 있다.
- 직접 구현한 UserDetailService를 찾아 사용 정보를 Principal로 설정해준다. 물론 User 객체도 UserDetails를 구현해야 한다.
- 실제로 해당 username을 가진 user가 존재해야한다.
- @BeforeEach, @BeforeAll과 함께 사용할 때 발생하는 문제
-
SecurityContext는 default로 TestExecutionListener.beforeTestMethod로 설정이 되어있다.
-
즉 @Before 전에 @withUserDetails이 동작해서 SecurityContext 안에 넣으려고 하기 때문에 실제 user객체가 생기기 전에 해당 user를 찾아 SecurityContext가 제대로 생기지 못하고 테스트가 실패함.
-
이 문제는 setupBefore를 TestExecutionListener.beforeTestExecution로 설정하는 것으로 해결 가능함.
@WithUserDetails(value = "example2@naver.com", setupBefore = TestExecutionEvent.TEST_EXECUTION)
3. Service 테스트
- service 테스트는 따로 어노테이션이 없고 대부분 앞선 게시글의 Mockito를 사용해서 모의 테스트를 진행한다.
4. Repository 테스트
4.1 @DataJpaTest
- Repository 슬라이스 테스트
- DataSource의 설정이 정상적인지, JPA를 사용하여 데이터를 제대로 생성, 수정, 삭제하는지 등의 테스트가 가능
- 내장형 데이터베이스를 사용 (in-memory embedded database)
- @Entity 어노테이션이 적용된 클래스를 스캔하여 스프링 데이터 JPA 저장소를 구성
- @ActiveProfiles("test") 등의 프로파일이 설정도 가능하다.
4.1.1 JPA 관련 테스트 설정만 로드
- @SpringBootTest : 애플리케이션의 전체 클래스를 로드
- @DataJpaTest : JPA 관련 테스트 설정에 관련된 클래스만 로드
- JpaRepository를 상속하는 클래스(repository)는 @DataJpaTest에서 알아서 올려주기 때문에 import를 통해 직접 주입하지 않아도 됨.
- JPA 관련 테스트 설정이 아닌 경우, 직접 스프링 컨테이너에 클래스를 로드시켜줘야함.
- 테스트 대상 클래스가 의존하는 것이 인터페이스라면 @TestConfiguration 사용해서 안에서 @Bean을 만들고 직접 주입해줘야 함.
- 테스트 대상 클래스가 의존하는 것이 클래스라면 @Import 어노테이션을 통해 컨테이너에 로드해줘야 함.
4.1.2 @Transactional
- 내부에 @Transactional를 포함해 테스트 진행 후 데이터를 롤백함.
- @Transactional 기능 비활성화 :
- @Transactional(propagation = Propagation.NOT_SUPPORTED)
- Replace.Any : 기본적으로 내장된 임베디드 데이터베이스를 사용
- Replace.NONE :
- yml 파일에 사용자가 지정한 데이터베이스를 사용하도록 함. (실제 DB)
- @ActiveProfiles에 설정한 프로파일(yml) 환경값에 따라 데이터 소스가 적용
- yml에 spring.test.database.replace: NONE와 동일
- 특정 테스트 데이터베이스를 사용할 것인 선택이 가능
- yml 파일의
spring.test.database.connection: H2 설정과 동일함.
- 기본 설정 : H2 데이터 베이스를 사용함.
4.1.5 TestEntityManager
- @DataJpaTest에서 EntityManager의 대체제로 만들어진 테스트용 EntitiyManager
- persist, flush, find 등과 같은 기본적인 JPA테스트가 가능함
4.2 @DataRedisTest
4.2.1 Redis 관련 빈들을 로드
- 스프링 애플리케이션 컨텍스트에 필요한 Redis 관련 빈들이 로드됨.
- Embedded Redis 서버를 시작하고 Redis 연결을 위한 설정을 로드
- @RedisHash로 표시된 엔티티들에 대한 Redis Hash Mapping을 구성
- CrudRepository 또는 ReactiveCrudRepository를 구현한 레포지토리 빈들을 생성하고 자동으로 구성
- RedisConnectionFactory, RedisTemplate, StringRedisTemplate 등을 포함
4.2.2 테스트용 Embedded Redis 사용
- 테스트를 위해 내장된(Embedded) Redis 서버를 사용.
- 따라서 별도로 Redis 서버를 설치하거나 실행할 필요 없이 테스트를 진행할 수 있음
4.2.3 Redis 테스트 지원 기능 활성화:
- @DataRedisTest는 Redis와 관련된 테스트를 수행하기 위한 기능들을 활성화한다.
5. 냉부 프로젝트에 적용
5.1 controller 테스트 (통합 테스트)
-
@SpringBootTest
- 실제 테스트 (통합 테스트)
- 자동으로 애플레이션 컨텍스트에 모든 빈을 로드한다. (@Component, @Service, @Repository…)
-
@AutoConfigureMockMvc를 통해 MockMVC를 자동 구성함.
-
@Transactional 사용해서 테스트 후 데이터를 모두 롤백함.
-
스프링 시큐리티를 사용하고 있지만 @WithMockUser, @WithAnonymousUser, @WithUserDetails 중 아무것도 사용하고 있지 않다.
-
controller 테스트에서 통합 테스트를 진행하는 이유 :
- 의존성 문제나 빈 주입 단계에서 오류가 발생하면
@SpringBootTest가 있는 모든 테스트 클래스는 오류가 발생하는 문제가 생긴다.
- 하지만
TestRestTemplate를 사용해서 하는 통합 테스트 또한 의존성 문제나 빈 주입 문제가 발생하면 @SpringBootTest가 있는 모든 테스트 클래스에 오류가 발생하는 것은 똑같다.
TestRestTemplate 를 사용해서 테스트하는 것과 controller에서 mockMvc를 이용해서 테스트 하는 것의 차이를 모르겠다. 결국 둘 다 url로 테스트를 하는 것인데 굳이 같은 행동을 2번하는 느낌이 들어서 controller에서 통합 테스트를 진행했다.
- 이 2가지가 다른 효용을 가지고 특별히 분리해야한다고 깨닫는다면 리팩토링을 진행해야겠다.
-
@WithMockUser, @WithAnonymousUser, @WithUserDetails 사용하지 않는 이유 :
- 지금은 TestTokenService이라는 테스트 클래스를 만들어서 createToken 을 수행하고 바로 그 토큰을 header에
HttpHeaders.AUTHORIZATION와 함께 담아서 전송하는 방법을 통해 시큐리티 필터를 통과하는 방법으로 사용하고 있다.
- 향후 리팩토링을 진행할 때 수정해보도록 해야겠다.
5.2 Service 테스트 (슬라이스 테스트)
- Mockito를 이용
- @Mock + @InjectMock 사용
- Service 클래스가 의존하는 port에 스터빙을 하고 모의 테스트를 통해 검증
5.3 Repository 테스트 (슬라이스 테스트)
- @DataJpaTest 사용
- 데이터 롤백,
- JPA 관련 테스트 설정만 로드
- 그 외의 다른 설정은 @Import를 이용하고 @TestConfiguration를 통해 직접 Bean 주입
- @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) 사용
- yml에 설정된 database를 사용
- TestEntityManager 사용
- 특정 Repository를 테스트하기 위해 다른 Repository 통해서 테스트 결과를 검증할 수 있지만 다른 Repository를 의존하게 되기 때문에 특정 Repository를 테스트하는 경우 해당 Repository만 독립적으로 사용되도록 되어야함.
- 그렇기 때문에 TestEntityManager를 사용해서 테스트 결과를 검증
- First 원칙 중 I(Isolated - 고립성)이 깨지게 된다.
- @TestDataInit 사용
- 테스트를 진행하기 전에 데이터 초기화를 위해 만든 커스텀 어노테이션
@TestDataInit({"/ingredientImage.sql", "/ingredient.sql"})
- 각각 테스트를 진행하기 전 @TestDataInit 내의 sql 파일의 SQL 쿼리를 실행 (데이터 초기화)
- 각각 테스트가 끝나고 delete.sql 파일의 내의 SQL 쿼리를 실행 (테이블 및 데이터 삭제)
- @DataRedisTest 사용
- Embedded Redis 서버 생성
- @RedisHash로 표시된 엔티티와 그 엔티티와 관련된 RedisRepository도 모두 로드시켜줌.
6. 참고
1. 통합 테스트
2. Controller 테스트
3. Service 테스트
4. Repository 테스트