스프링부트

spring boot container 기반 테스트

알쓸개잡 2023. 10. 19.

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/

 

Spring Boot 3.1's ConnectionDetails abstraction

If you've used Spring Boot for a while, you're probably familiar with setting up connection details using properties. For example, you may have used spring.datasource.url to configure a JDBC connection. In Spring Boot 3.1 this continues to work as you'd ex

spring.io

https://spring.io/blog/2023/06/23/improved-testcontainers-support-in-spring-boot-3-1/

 

Improved Testcontainers Support in Spring Boot 3.1

There's been support for Testcontainers in Spring Boot for some time now, and Spring Boot 3.1 improves it further. But first, let's take a look at what Testcontainers is and how it's usually used. Testcontainers is an open source framework for providing th

spring.io

 

샘플코드는 테스트시 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

댓글

💲 추천 글