SpringBoot 2.7 TestContainer

2024. 5. 13. 10:24JAVA/Spring

개요

Spring 프레임워크로 웹 애플리케이션 개발시 테스트 코드를 구현하는 일이 많습니다. 하지만 웹 애플리케이션이 단독으로 사용되는 일은 많지 않고 MySQL과 같은 데이터베이스와 같이 서비스하는 경우가 많습니다. 테스트 코드 실행시 데이터베이스와 연결하는 방법중 하나는 데이터베이스 프로세스를 실행시킨 상태에서 JDBC를 통하여 연결하는 방법이 존재합니다. 그러나 테스트 코드를 실행할때 사전 조건으로 데이터베이스 프로세스를 실행되어야 정상적으로 수행되야 하는 제약이 있습니다. 이러한 문제를 해결하기 위한 방법 중 하나는 테스트 컨테이너(TestContainer)를 사용하는 방법이 있습니다. 테스트 컨테이너는 도커 기술을 사용하여 테스트 코드를 수행할때만 일시적으로 데이터베이스 컨테이너를 실행시킨 다음에 테스트를 수행하게 하고 테스트가 종료되면 자동으로 데이터베이스를 종료시키는 기술입니다. 이 글에서는 Spring Boot 2.7 버전에서 MySQL 데이터베이스 컨테이너와 Redis 컨테이너를 테스트 컨테이너로써 실행시킬 수 있도록 하는 방법을 소개합니다.

 

TestContainer 의존성 추가

테스트 컨테이너를 적용하기 위해서 스프링 프레임워크에 테스트 컨테이너 의존성을 추가합니다. 1.19.7은 글 작성 당시 최신 버전입니다.

testImplementation 'org.testcontainers:junit-jupiter:1.19.7'
testImplementation 'org.testcontainers:mysql:1.19.7'

 

Note
Spring Boot 3.1 부터는 spring-boot-testcontainers 의존성을 지원하기 때문에 해당 의존성 라이브러리를 이용하여  동적 프로퍼티들을 정의하지 않고 모든 프로퍼티들을 자동 설정해주는 @ServiceConnetion 애노테이션과 같은 추가적인 기능 사용이 가능합니다.

 

 

이 글의 예제에 사용되는 build.gradle의 의존성들은 다음과 같습니다.

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'

    // mysql
    runtimeOnly 'com.mysql:mysql-connector-j'

    // test containers
    testImplementation 'org.testcontainers:junit-jupiter:1.19.7'
    testImplementation 'org.testcontainers:mysql:1.19.7'

    // rest-assured
    testImplementation 'io.rest-assured:rest-assured'

    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

실행된 MySQL 컨테이너와 연결하기 위해서 mysql-connector-j 의존성이 필요합니다. 그리고 통합 테스트를 실행하기 위해서 API 요청을 위한 rest-assured 의존성을 추가합니다.

 

TestContainer 클래스 구현

테스트 수행시 MySQL 컨테이너와 Redis 컨테이너를 실행하기 위해서 다음과 같이 클래스를 구현할 수 있습니다.

@ActiveProfiles("test")
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@Testcontainers
public class AbstractContainerBaseTest {
	private static final String REDIS_IMAGE = "redis:7-alpine";
	private static final int REDIS_PORT = 6379;

	private static final GenericContainer REDIS_CONTAINER = new GenericContainer(REDIS_IMAGE)
		.withExposedPorts(REDIS_PORT)
		.withReuse(true);

	static {
		REDIS_CONTAINER.start();
	}

	@LocalServerPort
	protected int port;

	@DynamicPropertySource
	public static void overrideProps(DynamicPropertyRegistry registry) {
		// redis property config
		registry.add("spring.redis.host", REDIS_CONTAINER::getHost);
		registry.add("spring.redis.port", () -> REDIS_CONTAINER.getMappedPort(REDIS_PORT).toString());

		// mysql property config
		registry.add("spring.datasource.driver-class-name", () -> "org.testcontainers.jdbc.ContainerDatabaseDriver");
		registry.add("spring.datasource.url", () -> "jdbc:tc:mysql:8.0.33://localhost/testdb");
		registry.add("spring.datasource.username", () -> "admin");
		registry.add("spring.datasource.password", () -> "password1234!");
	}

	@BeforeEach
	void setup(){
		RestAssured.port = port;
	}
}

 

MySQL 컨테이너를 실행하기 위해서 @DynamicPropertySource 애노테이션이 적용된 메소드 구현에서 registry 객체에 MySQL에 대한 드라이버 클래스 이름, url, username, passowrd를 명시합니다. Redis 컨테이너와 같은 사례처럼 정적 필드 멤버로 정의하지 않은 이유는 정적 필드 멤버로 정의하여 실행하게 되면 테스트 코드 실행시 MySQL 컨테이너가 2개이상 실행될 수 있기 때문입니다. 이미 동적 프로퍼티 부부에서 드라이버 클래스 이름과 연결 정보를 명시하였기 때문에 이 부분에서 한개의 테스트 컨테이너가 실행됩니다. 따라서 별도의 정적 필드 멤버로 MySQL 테스트 컨테이너를 정의할 필요가 없습니다.

 

위와 같이 AbstractContainerBaseTest 클래스를 구현하면 바로 아래에 테스트 코드를 작성할 수도 있고 아니면 해당 클래스를 상속받아서 테스트를 다음과 같이 작성할 수 있습니다.

 

TestContainer 테스트 위한 코드 구현

@Getter
@Entity
@NoArgsConstructor
public class Member {
	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private Long id;

	private String name;

	public Member(String name) {
		this.name = name;
	}
}

 

@Repository
public interface MemberRepository extends JpaRepository<Member, Long> {
}

 

@RestController
@RequestMapping("/members")
@RequiredArgsConstructor
public class MemberRestController {

    private final MemberRepository repository;
    
    @GetMapping
    public List<Member> findAll() {
        return repository.findAll();
    }
}

 

테스트 실행시 jpa를 위한 프로퍼티를 다음과 같이 정의합니다. (resources/application-test.yml)

spring:
  jpa:
    database: mysql
    database-platform: org.hibernate.dialect.MySQL8Dialect
    hibernate:
      ddl-auto: create
    properties:
      hibernate:
        format_sql: true
        dialect: org.hibernate.dialect.MySQL8Dialect
    defer-datasource-initialization: true
    show-sql: true

 

테스트 작성

위에서 정의한 AbstractContainerBaseTest를 상속받고 회원목록 조회를 위한 통합 테스트를 작성합니다.

public class MemberIntegrationTest extends AbstractContainerBaseTest{

	@Autowired
	private MemberRepository repository;

	@BeforeEach
	void clean(){
		repository.deleteAllInBatch();
	}

	@AfterEach
	void tearDown(){
		repository.deleteAllInBatch();
	}

	@DisplayName("회원들을 조회한다")
	@Test
	void whenRequestMembers_thenReturnMembers(){
		repository.saveAll(List.of(
			new Member("kim"),
			new Member("lee"),
			new Member("park")
		));

		when().get("/members")
			.then().statusCode(200)
			.and().body("name", hasItems("kim", "lee", "park"));
	}

}

 

실행 결과는 다음과 같습니다. 테스트를 실행하면 Redis, MySQL 컨테이너가 실행되는 것을 볼 수 있습니다.

 

테스트 실행시 콘솔창을 보면 이또한 마찬가지로 MySQL 컨테이너가 생성되는것을 볼수 있습니다.

 

References

https://www.baeldung.com/spring-boot-built-in-testcontainers
https://www.baeldung.com/spring-boot-redis-testcontainers