spring boot application 테스트를 위해서 혹은 로컬에 개발 테스트 환경을 구축하기 위해서 3rd party 서비스를 로컬 환경에 설치하거나 embedded 서비스를 사용하여 테스트를 하는 경우가 많다. 이러한 작업은 꽤 번거로운 일이 될 수 있는데 TestContainers를 이용하여 손쉽게 container 기반의 테스트를 수행할 수 있다. 이번 포스팅에서는 샘플 코드를 기반으로 spring boot에서 TestContainers를 이용하는 방법에 대해서 기록한다.
spring boot 3.1.4 버전에서 샘플코드를 작성하였다. spring boot 3.1 버전 부터 ConnectionDetails 추상화 기능이 추가되어 TestContainers에 대한 몇 가지 개선사항이 있는데 해당 내용은 아래 링크를 참고하기 바란다.
https://spring.io/blog/2023/06/19/spring-boot-31-connectiondetails-abstraction/
https://spring.io/blog/2023/06/23/improved-testcontainers-support-in-spring-boot-3-1/
샘플코드는 테스트시 mariadb docker container 인스턴스를 자동으로 생성하여 repository 기능 동작을 테스트하는 코드다.
Dependency
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mariadb.jdbc</groupId>
<artifactId>mariadb-java-client</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-testcontainers</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>mariadb</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
start.spring.io를 통해서 프로젝트를 생성하는 경우 TESTING >> TestContainers 종속성을 추가하면 아래 자동으로 다음 종속성이 추가된다.
- org.springframework.boot:spring-boot-testcontainers
- org.testcontainers:junit-jupiter
- org.testcontainers:mariadb
org.testcontainers:mariadb 종속성의 경우에는 SQL 항목에서 MariaDB Driver 종속성이 추가되어 그에 맞게 종속성을 추가해 준다.
application.yml 설정
spring:
jpa:
database-platform: org.hibernate.dialect.MariaDBDialect
properties:
hibernate:
format_sql: true
show-sql: true
logging:
level:
org.hibernate.orm.jdbc.bind: trace
---
spring:
config:
activate:
on-profile: default
jpa:
hibernate:
ddl-auto: create
---
spring:
config:
activate:
on-profile: local
jpa:
hibernate:
ddl-auto: create
---
spring:
config:
activate:
on-profile: test
jpa:
hibernate:
ddl-auto: create
#@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) 대체
test:
database:
replace: none
---
spring:
config:
activate:
on-profile: production
jpa:
hibernate:
ddl-auto: none
4개의 profile을 정의하였다. 샘플코드에서 production profile은 사용하지 않겠지만 참고로 정의하였다.
spring.jpa.hibernate.ddl-auto: create 설정이 적용된 profile은 다음과 같다.
- local (local 환경에서 애플리케이션 구동 시 사용할 profile)
- test (테스트 코드에서 사용할 profile)
- default (profile이 지정되지 않은 경우 적용될 profile)
Entity 정의
/**
* 공통 Entity
*/
@Getter
@ToString
@MappedSuperclass
public abstract class BaseEntity {
/**
* 생성 일시
*/
@CreatedDate
@Column(name = "create_datetime", nullable = false, updatable = false)
protected LocalDateTime createDatetime = LocalDateTime.now();
/**
* 생성 계정 식별자
*/
@CreatedBy
@Column(name = "creator_id", nullable = false, updatable = false)
protected Long creatorId = 1L;
/**
* 수정 일시
*/
@LastModifiedDate
@Column(name = "modify_datetime", nullable = false)
protected LocalDateTime modifyDatetime = LocalDateTime.now();
/**
* 수정 계정 식별자
*/
@LastModifiedBy
@Column(name = "modifier_id", nullable = false)
protected Long modifierId = 1L;
}
@Entity
@Table(name = "Member")
@Getter
@Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Member extends BaseEntity{
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "name", nullable = false, unique = true)
private String userName;
@Column(name = "age", nullable = false, length = 4)
private Integer age;
public static Member of(Long id) {
return new Member(id);
}
private Member(Long id) {
this.id = id;
}
@Builder
protected Member(Long id, String userName, Integer age) {
this.id = id;
this.userName = userName;
this.age = age;
}
}
id, name, age 속성을 갖는 Member Entity를 정의하였다.
repository
@Repository
public interface MemberRepository extends JpaRepository<Member, Long> {
Optional<Member> findByUserName(String userName);
}
controller
@RestController
@RequiredArgsConstructor
public class TestController {
private final MemberRepository memberRepository;
@GetMapping("/member")
public ResponseEntity<List<Member>> listMember() {
return ResponseEntity.ok(memberRepository.findAll());
}
@PostMapping("/member")
public void createMember(@RequestBody Member member) {
memberRepository.save(member);
}
}
Member 를 추가하고 리스트 조회하는 단순한 controller다. 통합테스트 시 사용하기 위해 작성하였다.
Test Code
TestConfiguration
@TestConfiguration(proxyBeanMethods = false)
public class TestApplication {
//mariadb container instance 생성
@Bean
@ServiceConnection
MariaDBContainer<?> mariaDbContainer() {
return new MariaDBContainer<>(DockerImageName.parse("mariadb:latest"));
}
//통합테스트
public static void main(String[] args) {
SpringApplication.from(Application::main).with(TestApplication.class).run(args);
}
}
위 TestApplication 클래스는 starter.spring.io를 통해서 생성할 때 TestContainers 종속성이 추가되면 자동으로 만들어준다.
@ServiceConnection 어노테이션은 spring boot 3.1 버전에 추가된 어노테이션이다.
@ServiceConnection 어노테이션은 @DynamicPropertySource 어노테이션을 대체하여 자동으로 container instance에 접속하도록 한다.
base 클래스 정의
@Disabled
@ContextConfiguration(classes = TestApplication.class)
@ActiveProfiles("test")
public abstract class ContainerTest {
}
편의성을 위해서 공통적으로 적용될 annotation을 정의한 base class를 정의하였다.
각 단위 테스트 클래스는 ContainerTest 클래스를 상속받는다.
단위테스트
@DataJpaTest
class MemberRepositoryTest extends ContainerTest {
@Autowired
private MemberRepository memberRepository;
private final String userName = "test1";
@Test
void member_save_test() {
Member member = Member.builder()
.age(10)
.userName(userName)
.build();
member = memberRepository.save(member);
Assertions.assertThat(member.getId()).isNotNull();
}
@Test
void member_search_test() {
member_save_test();
Optional<Member> byUserName = memberRepository.findByUserName(userName);
Assertions.assertThat(byUserName).isPresent();
}
}
테스트
테스트를 실행하기 전에 docker 엔진이 구동되어 있어야 한다.
MemberRepositoryTest
MemberRepositoryTest를 실행하면 다음과 같이 docker container instance 가 생성되는 로그를 확인할 수 있다.
[ main] c.e.s.t.repository.MemberRepositoryTest : The following 1 profile is active: "test"
[ main] .s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data JPA repositories in DEFAULT mode.
[ main] .s.d.r.c.RepositoryConfigurationDelegate : Finished Spring Data repository scanning in 16 ms. Found 1 JPA repository interfaces.
[ main] o.t.utility.ImageNameSubstitutor : Image name substitution will be performed by: DefaultImageNameSubstitutor (composite of 'ConfigurationFileImageNameSubstitutor' and 'PrefixingImageNameSubstitutor')
[ main] o.t.d.DockerClientProviderStrategy : Loaded org.testcontainers.dockerclient.UnixSocketClientProviderStrategy from ~/.testcontainers.properties, will try it first
[ main] o.t.d.DockerClientProviderStrategy : Found Docker environment with local Unix socket (unix:///var/run/docker.sock)
[ main] org.testcontainers.DockerClientFactory : Docker host IP address is localhost
[ main] org.testcontainers.DockerClientFactory : Connected to docker:
Server Version: 20.10.23
API Version: 1.41
Operating System: Docker Desktop
Total Memory: 7851 MB
[ main] tc.testcontainers/ryuk:0.5.1 : Creating container for image: testcontainers/ryuk:0.5.1
[ main] o.t.utility.RegistryAuthLocator : Credential helper/store (docker-credential-desktop) does not have credentials for https://index.docker.io/v1/
[ main] tc.testcontainers/ryuk:0.5.1 : Container testcontainers/ryuk:0.5.1 is starting: 00269b3c72b085e73319861cd5c0abf002494c58135cacedb39fa8e412814cbe
[ main] tc.testcontainers/ryuk:0.5.1 : Container testcontainers/ryuk:0.5.1 started in PT0.709926S
통합테스트
TestApplication 클래스의 main()을 실행시켜 보자.
docker container instance가 생성되는 로그를 확인할 수 있을 것이다.
postman을 통해서
- POST http://localhost:8080/member
- Request Body
{
"userName": "test-1",
"age": 10
}
요청을 한 뒤
- GET http://localhost:8080/member
요청을 하면 아래와 같은 결과를 얻을 수 있다.
[
{
"createDatetime": "2023-10-19T10:15:08.81984",
"creatorId": 1,
"modifyDatetime": "2023-10-19T10:15:08.81985",
"modifierId": 1,
"id": 1,
"userName": "test-1",
"age": 10
}
]
docker container isolation
통합 테스트가 실행 중일때 MemberRepositoryTest 단위테스트를 실행하면 독립된 2개의 container instance 가 생성된다.
[ main] tc.mariadb:latest : Container mariadb:latest is starting: 9d1dc03b0b54f542f0f13e0de4c1f75608bed892a29e0907c489ce25fd7500c9
[ main] tc.mariadb:latest : Container mariadb:latest is starting: 7176f777dad261b371399b29f0059f4aef886261b4e396731c877757645cdef9
전체 코드는 GitLab 에서 확인할 수 있다.
'스프링부트' 카테고리의 다른 글
REST PUT vs PATCH (0) | 2023.10.22 |
---|---|
spring boot 3.1 docker compose support (0) | 2023.10.22 |
spring boot 로그백 (logback) 설정 방법 (0) | 2023.10.14 |
spring boot logging (0) | 2023.10.12 |
STOMP와 ACTIVEMQ를 이용한 메시지 broadcasting (0) | 2023.10.11 |
댓글