자꾸 헷갈려서 정리

 

어플리케이션 시작 시 호출되는 순서

1. ApplicationStartingEvent

2. ApplicationEnvironmentPreparedEvent

3. ApplicationContextInitializedEvent

4. ApplicationPreparedEvent

5. ContextRefreshedEvent

6. ApplicationStartedEvent

7. ApplicationReadyEvent

 

여기서, 1-3 번 까지는 ApplicationContext 초기화 전에 시작된다. 때문에, 이 이벤트들은 @EventListener 를 통한 이벤트 등록으로 처리할 수 없다. 왜냐하면 @EventListener 는 해당 리스너 메서드를 가진 클래스가 스프링 빈으로 먼저 스캔되어야 작동하는데, 1-3번 이벤트들은 ApplicationContext 시작 전이기 때문에 이벤트 등록보다 실제 이벤트가 더 먼저 발생한다.

 

때문에 1-3 번 이벤트들이 작동하기 위해서는 아래와 같이 스프링 어플리케이션 시작 전에 리스너를 선등록해야 한다.

class MyApplicationStartingEventListener : ApplicationListener<ApplicationStaringEvent> {
	override fun onApplicationEvent(event: ApplicationStartingEvent) {
    	...
    }
}

...

fun main(args: Array<String>) {
    SpringApplicationBuilder(MyApplication::class.java)
        .listeners(
                MyApplicationStartingEventListener(),
                MyApplicationContextInitializedEventListener(),
                MyApplicationEnvironmentPreparedEventListener(),
                MyApplicationPreparedEventListener(),
            )
            .run(*args)
}

 

또, 이벤트는 등록한 순서대로 호출된다. 위의 방법으로 등록되는 이벤트는 ApplicationContext 초기화 시 스캐닝에 의해 등록된 이벤트보다 먼저 등록되기 때문에, 동일한 이벤트가 호출될 때, @EventListener 리스너보다 무조건 먼저 호출된다.

@Testcontainers 는 JUnit 5 (주피터) 의 테스트컨테이너 어노테이션이다.

 

이 어노테이션은 클래스에 지정하며, 이 어노테이션이 지정된 클래스는 테스트 실행 시 자신 안에서 @Container 가 지정된 필드를 찾아 컨테이너의 라이프싸이클을 관리한다.

 

@Container 필드가 static 이면 이 컨테이너는 첫 번째 테스트 메서드 시작 전에 한 번만 시작되어 마지막 테스트 메서드 종료 시 정지된다. 즉 모든 테스트 케이스에 대해 하나의 컨테이너를 공유해서 사용한다. (이 때 jdbc:tc:.. 를 사용하면 안 된다.)

오해하지 말아야 할 것이, '첫 번째 테스트 메서드' 는 하나의 테스트 클래스 안에서의 이야기이다. 프로젝트의 모든 테스트를 수행할 때는 여러 테스트 클래스를 실행하게 되는데, 컨테이너는 한 테스트 클래스의 시작과 함께 종료되고, 다른 테스트 클래스가 있으면 다시 시작된다.

그리고 컨테이너의 라이프싸이클(시작-종료) 는 알아서 관리되기 때문에, 사용자가 직접 start() 등을 호출할 필요가 없다.

 

또, MySQL 등의 컨테이너는 기본적으로 랜덤 포트를 사용하기 때문에 이 포트를 알기 위해서는 스프링 어플리케이션 컨텍스트 시작 이벤트를 받아서 getJdbcUrl() 등을 호출하게 되는데, 스프링 어플리케이션 컨텍스트는 테스트 시 기본적으로 한 번 로드되고 재사용된다.

이렇게되면 컨테이너는 테스트 클래스마다 다시 시작되어 포트가 변경되는데, jdbc url 은 처음 테스트에서 얻은 것을 그대로 사용하게 되어 첫 테스트 클래스만이 db 커넥션 연결에 성공하고 나머지는 실패하게 된다. 이를 방지하기 위한 방법 중 하나로, @DirtiesContext 를 사용하여 매 테스트 클래스마다 새로운 어플리케이션 컨텍스트를 로드하게 하여 매번 새로운 컨테이너의 jdbc url 을 받아서 사용하는 것이 있다.

 

@Container 필드가 인스턴스 필드이면 이 컨테이너는 각 테스트 메서드 시작 전에 시작되고, 메서드 종료 시 정지된다. 모든 테스트 케이스에 대해 각각의 컨테이너가 할당되는 셈이다.

 

예 (주피터 javadoc 에서 가져옴): 

  @Testcontainers
  class MyTestcontainersTests {
 
      // will be shared between test methods
      @Container
      private static final MySQLContainer MY_SQL_CONTAINER = new MySQLContainer();
 
      // will be started before and stopped after each test method
      @Container
      private PostgreSQLContainer postgresqlContainer = new PostgreSQLContainer()
              .withDatabaseName("foo")
              .withUsername("foo")
              .withPassword("secret");
 
      @Test
      void test() {
          assertTrue(MY_SQL_CONTAINER.isRunning());
          assertTrue(postgresqlContainer.isRunning());
      }
  }

 

@Testcontainers 를 슈퍼클래스에 지정하고 테스트 클래스는 이 클래스를 상속받아 사용하는 것이 가능하다. (@Inherited)

 

출처: https://javadoc.io/doc/org.testcontainers/junit-jupiter/latest/org/testcontainers/junit/jupiter/Testcontainers.html

'Java' 카테고리의 다른 글

JPA] @OneToOne lazy loading  (0) 2022.08.22
JPA] Hibernate 1차 캐시  (0) 2022.06.27

사용법

ApplicationContext 전체를 초기화하지 않고 일부 Bean 만을 테스트할 때

@ExtendWith(SpringExtension::class) + @ContextConfiguration + @TestConfiguration

=> @TestConfiguration 에서 원하는 빈을 초기화한다. (@Bean 을 쓰거나 @ComponentScan을 쓰거나..)

=> 프로파일 등 수동 프로퍼티 지정이 필요할 때는 @ContextConfiguration 의 initializers 에 ApplicationContextInitializer 구현체를 지정한다.

예(코틀린): 

    class PropertyOverrideContextInitializer : ApplicationContextInitializer<ConfigurableApplicationContext> {
        override fun initialize(applicationContext: ConfigurableApplicationContext) {
            TestPropertySourceUtils.addInlinedPropertiesToEnvironment(
                applicationContext,
                "spring.config.activate.on-profile=production",
            )
        }
    }

=> application.properties(or yml) 자동 로딩을 사용하고 싶으면 initializers 에 ConfigDataApplicationContextInitializer 클래스를 추가한다. 왜냐하면, 설정파일 자동 로딩을 통한 어플리케이션 자동 설정은 스프링 부트의 기능이다. SpringExtension::class 은 스프링 부트가 아닌 전통 방식으로 스프링을 가동하기 때문에, 별도 설정이 없다면 비어있는 프로퍼티 값을 보게 될 것이다.

 

 

ApplicationContext 전체를 초기화하지 않고 프로퍼티 설정을 테스트할 때

@ExtendWith(SpringExtension::class) + @ContextConfiguration with ConfigDataApplicationContextInitializer + @TestConfiguration + @EnableConfigurationProperties(optional)

=> 여기서 @TestConfiguration 은 초기화 대상 빈을 지정하는 역할을 한다. 만약 정말 프로퍼티 미러링 빈만을 필요로 한다면 아래와 같이 사용할 수 있다. (다른 빈들도 필요하다면 그냥 @ComponentScan 으로 프로퍼티 미러링 빈과 다른 빈들을 모두 함께 초기화하는 편이 낫다)

@ExtendWith(SpringExtension.class)
@ContextConfiguration(initializers = ConfigDataApplicationContextInitializer.class)
public class MongoPropertiesTest {
    
    @Autowired
    private MongoProperties properties;

    @Configuration
    @EnableConfigurationProperties({MongoProperties.class})
    public static class Config {

    }

}

 

 

Jpa 테스트 수행 시

@DataJpaTest 를 사용한다. 추가로 다른 빈이 필요한 경우 @TestConfiguration + @Import 를 사용한다.

 

웹 MVC 테스트 수행 시

@WebMvcTest 를 사용한다. 추가로 다른 빈이 필요한 경우 @TestConfiguration + @Import 를 사용한다.

 

@Service 등 비즈니스 로직 테스트 수행 시

1. 데이터 저장 애뮬레이션이 필요하다면 아래와 같이 사용한다.

@ExtendWith(SpringExtension.class)  @SpringBootTest + @TestConfiguration(선택) + @Import 

2. 데이터 저장 애뮬레이션이 필요하지 않다면 아래와 같이 사용한다.

@ExtendWith(SpringExtension.class) + @SpringBootTest + @TestConfiguration(선택) + @Import + @MockBean(to repositories) 

 

통합 테스트 수행 시

@SpringBootTest 를 사용한다.

 

어노테이션 설명

@RunWith

JUnit 4 의 어노테이션으로, JUnit 의 Test Runner 구현체를 인자로 받아 테스트 메서드 실행에 관한 동작을 확장할 수 있다. 주로 아래와 같이 사용된다.

@RunWith(SpringJUnit4ClassRunner.class)

Runner 에 대한 자세한 설명은 아래를 참조

https://eminentstar.github.io/2017/07/23/about-junit-and-test.html

 

 

@RunWith(SpringJUnit4ClassRunner.class) vs @RunWith(SpringRunner.class)

같은 녀석들이다. SpringJUnit4ClassRunner 의 이름을 줄인 버전이 SpringRunner 라고 생각하면 된다.

 

 

@ExtendWith

JUnit 5 의 어노테이션으로, @RunWith 을 대체한다. @RunWith 에서 Runner 의 구현체를 인자로 받았던 것처럼, 여기서는 Extension 인터페이스의 구현체를 인자로 받는다. Extension 은 주로 test 메서드의 전/후 동작, 조건부 테스트 실행, 테스트 메서드 라이프싸이클 콜백, 파라미터 리솔루션, 예외 처리 등 테스트에 영향을 미치는 동작에 대한 확장을 제공한다. 대표적인 구현체로 SpringExtension 이 있다.

SpringExtension 은 어플리케이션 컨텍스트를 로드한다. 아래 @SpringBootTest 또한 어플리케이션 컨텍스트를 로드하는데, 둘의 차이는 SpringExtension 은 어플리케이션 컨텍스트를 '전통적인(tranditional) 스프링 방식' 으로 로드하고, @SpringBootTest 는 '스프링 부트 방식' 으로 로드한다는 점이다. 때문에 SpringExtension 는 스프링 부트에서 제공하는 '외부 설정 로딩' 을 사용하지 못하여 application.properties, 환경변수, cli 아규먼트 등으로 어플리케이션 옵션을 주입할 수 없다. 프로퍼티를 사용하려면 ConfigDataApplicationContextInitializer, @EnableConfigurationProperties 등을 사용해서 직접 프로퍼티 설정 로딩 코드를 작성해야 한다.

 

 

@SpringBootTest

스프링 어플리케이션 컨텍스트를 로드하는 어노테이션으로, 운영 환경에 가장 가까운 환경을 애뮬레이션한다. 필요한 모든 빈을 초기화하기 때문에 무겁게 작동한다. 내장 톰캣을 사용한 웹 환경 애뮬레이션을 지원하며, 통합 테스트에 적절하다. 이 어노테이션의 실체는 @BootStrapWith(SpringBootTestContextBootstrapper.class) 이다. SpringBootTestContextBootstrapper 에서 어플리케이션 컨텍스트를 로드하고 클래스패스에서 @SpringBootConfiguration(@SpringBootApplication) 을 찾아 초기화한다.

스프링 부트 버전 2.1 부터 이 어노테이션에 @ExtendWith(SpringExtension.class) 가 붙었다. 즉 버전 2.1 부터는 @ExtendWith 을 함께 사용할 필요가 없다.

위 SpringExtension 에서 설명하였듯, 이 어노테이션은 어플리케이션 컨텍스트를 '스프링 부트 방식' 으로 로드한다. 때문에 프로퍼티, 환경변수, cli 아규먼트를 사용하는 일이 가능하다.

@Configuration 또는 classes 가 함께 지정되지 않으면, 이 어노테이션이 지정된 클래스의 상위, 하위에 존재하는 @SpringBootConfiguration 을 모두 찾는다.

@ContextConfiguration(loader=...) 를 지정하지 않으면 기본적으로 SpringBootContextLoader 를 사용한다.

 

 

@MockBean

목 인스턴스를 주입한다. 테스트 클래스가 다른 빈에 의존할 때, 논리적으로는 필요하지만 실제 동작 테스트까지는 필요하지 않은 빈이 있을 수 있다. 그럴 때 @Autowired 를 대신해서 이 어노테이션을 사용한다. 목 인스턴스는 실제로 작동하지는 않고 '작동하는 척' 한다. 아래와 같이 given 등 메서드를 사용해서 마치 실제로 호출해서 어떤 결과를 반환받은 것처럼 사용한다.

given(mockRepository.save(any(MyEntity.class))).willReturn(mySavedEntity())

 

 

@DataJpaTest

data jpa 에 관련한 테스트를 지원하기 위한 어노테이션으로, jpa 리포지토리 테스트에 사용하기 적절하다. data jpa 작동에 필요한 빈만을 초기화하기 때문에 @SpringBootTest 보다 훨씬 가볍게 작동한다. 하지만 @Service 등 다른 빈을 초기화하지 않기 때문에, 테스트가 퍼시스턴스 레이어 위쪽에 의존성을 가진 경우 이 어노테이션만으로는 테스트 불가능하다.

이 어노테이션은 기본 설정으로 테스트 시 인메모리 데이터베이스를 사용하도록 하며, 테스트 케이스마다 @Transactional 을 적용하며 이 트랜잭션은 한 테스트 케이스가 종료되면 자동으로 롤백을 수행한다.

 

 

@AutoConfigureTestDatabase

어플리케이션에 설정된 데이터베이스를 테스트용 데이터베이스로 대체하는 어노테이션이다. replace 라는 인자값이 중요한데, 아래와 같은 값들이 있다.

Replace.ANY: 기본값으로, 명시적 or auto-configured 된 테스트용 데이터베이스로 대체한다. 

Replace.AUTO_CONFIGURED: 오직 auto-configured 된 테스트용 데이터베이스로만 대체한다.

Replace.NONE: 데이터베이스를 대체하지 않는다.

@DataJpaTest 는 이 어노테이션을 ANY 로 사용하며 인메모리 데이터베이스로 대체한다. 직접 데이터베이스를 구동하여 테스트 결과를 조회하고 싶거나 하는, 이런 동작을 원치 않는 경우에는 Replace.NONE 을 사용하면 된다.

 

 

@WebMvcTest

data jpa 전용 테스트를 지원하기 위해 @DataJpaTest 가 있듯, 웹 MVC를 테스트하기 위해 이 어노테이션이 있다. 아래와 같은 대상만 스캔하여 빈으로 초기화한다.

@Controller, @ControllerAdvice, @JsonComponent, Converter, GenericConverter, Filter, HandlerInterceptor

주로 컨트롤러-인터셉터-필터를 테스트하기 위해 사용한다.

 

 

@TestConfiguration

테스트 설정을 위한 어노테이션으로, @Configuration 을 테스트 환경에 맞게 확장한 것이라 생각할 수 있다. 사실 그 동작은 @Configuration 과 다르지 않다. (실제로 @TestComponent 가 붙어있을 뿐이다.) 차이점이 있다면 이 어노테이션은 기본적으로 @SpringBootConfiguration 에서 스캔하지 않는다. 때문에 직접 @Import or @ContextConfiguration 을 명시해야 한다. (대신, @SpringBootTest(classes=... ) 를 사용하거나, @ComponentScan을 직접 사용한 경우 스캔 대상이 되므로 이 때 이 어노테이션이 지정된 클래스를 제외하기를 원한다면 TypeExcludeFilter 를 사용해야 한다.)

 

이 어노테이션의 사용법은 대표적으로 두 가지가 있다.

1. @SpringBootTest 지정 클래스의 내부클래스에 지정하기

이 때는 @Import 를 사용하지 않아도 자동으로 설정이 로드된다. 특정 @SpringBootTest 테스트 클래스 안에서만 사용할 설정을 정의한다.

 

2. 독립된 외부 클래스에 지정하고 @Import 하기

여러 테스트에서 공유해서 사용할 설정을 정의한다.

 

이 어노테이션의 주 용도는:

1. 테스트 환경에서 필요한 빈들만을 초기화하는 데에 있다. @DataJpaTest, @WebMvcTest 와 같이 특정 레이어에 대한, 통합테스트보다 가벼운 환경으로 테스트하는 경우 초기화 대상에서 벗어난 빈이 필요할 수 있다. 이를 위해 @SpringBootTest 를 사용한다면 불필요하게 무거운 동작이 추가될 것이다. 이 때 @TestConfiguration 을 사용해서 원하는 빈들을 초기화하는 클래스를 작성하고, 테스트 클래스에서 이 클래스를 @Import 하면 여기의 빈들만을 초기화한다.

https://csy7792.tistory.com/335

 

2. 테스트 환경에서 특정 빈을 오버라이딩할 수 있다. 어떤 빈들은 테스트 시 다른 구현체를 사용하고 싶다면, 이 어노테이션을 사용하여 같은 타입, 이름의 빈 초기화 코드를 넣고 @Import 하면 테스트 시 이 빈을 사용한다.

빈 오버라이딩 시에는 spring.main.allow-bean-definition-overriding=true 를 설정하거나, 빈 이름을 따로 지정해서 기존 빈 이름과 겹치지 않게 하여야 한다. 그렇지 않으면 빈 중복으로 에러가 발생한다.

https://www.logicbig.com/tutorials/spring-framework/spring-boot/test-configuration.html

 

 

 

 

 

 

+ Recent posts