도커 컨테이너에 접근하는 프로젝트에서 테스트할 때의 컨테이너는 어떤 걸 사용하시나요? 혹시 서버에서 실운영되고 있는 컨테이너를 바라보고 있나요? 엇 그러면 만약 어떠한 이유에 의해 컨테이너 안 애플리케이션의 설정이 변경되었다면... 그래서 어제와 같은 소스인데도 불구하고 테스트가 실패한다면... 억울해!!
멱등성(idempotent)
: 연산을 여러 번 적용하더라도 결괏값이 달라지지 않는 일
프로젝트 테스트 환경에서 멱등성은 중요한 속성입니다. 내가 짠 소스에 대해서만 테스트하는 것이 목적이기 때문에 외부 환경이 테스트 결과에 영향을 미쳐선 안되니까요. 그래서 독립적인 테스트 환경을 구축하기 위해 테스트할 때만 잠깐 사용할 컨테이너를 띄어주는 게 testContainer가 해주는 일입니다!
디펜던시
testCompile("org.testcontainers:testcontainers:1.12.4")
먼저 테스트로 띄워주고 싶은 컨테이너 설정을 보겠습니다
docker-compose.yml
version: "2"
services:
elasticsearch:
image: docker.elastic.co/elasticsearch/elasticsearch:7.8.1
environment:
- discovery.type=single-node
- http.host=0.0.0.0
ports:
- 9200:9200
volumes:
- ./dict:/usr/share/elasticsearch/config/dict
elasticsearch 7.8.1 버전에 포트 바인딩은 9200, 볼륨 마운트는 ./dict 를 컨테이너 안 /usr/share/elasticsearch/config/dict 과 시켜주고 있네요
이제 자바 테스트 코드로 컨테이너를 어떻게 띄워주는지 보겠습니다. Junit5 코드까지 추가되어 뭐가 조금 더 붙었지만 메소드 안의 내용만 보시면 됩니다.
import com.github.dockerjava.api.model.*;
import org.junit.jupiter.api.extension.AfterAllCallback;
import org.junit.jupiter.api.extension.BeforeAllCallback;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.images.builder.ImageFromDockerfile;
public class ContainerExtension implements BeforeAllCallback, AfterAllCallback {
static GenericContainer<?> esContainer;
@Override
public void beforeAll(ExtensionContext context) throws Exception {
esContainer = new GenericContainer<>(new ImageFromDockerfile().withDockerfileFromBuilder(
builder -> builder.from("docker.elastic.co/elasticsearch/elasticsearch:7.8.1")
.run("bin/elasticsearch-plugin", "install", "analysis-nori")
.build()
));
esContainer
.withEnv("discovery.type", "single-node")
.withEnv("http.host", "0.0.0.0")
.withFileSystemBind("./dict", "/usr/share/elasticsearch/config/dict", BindMode.READ_ONLY)
.addExposedPorts(9200)
;
esContainer.start();
String host = String.format("http://%s:%s", esContainer.getContainerIpAddress(), esContainer.getMappedPort(9200));
System.out.println(host); // http://localhost:32888
}
@Override
public void afterAll(ExtensionContext context) {
esContainer.close();
}
}
- builder -> builder.from("docker.elastic.co/elasticsearch/elasticsearch:7.8.1") : 이미지 지정
- .run("bin/elasticsearch-plugin", "install", "analysis-nori") :
- 이미지 띄우기 전 실행할 커맨드
- 엘라스틱서치 플러그인은 기동 전에 미리 설치해야하기 때문에 추가
- .withEnv("discovery.type", "single-node") : 컨테이너 안 환경변수
- .withFileSystemBind("./dict", "/usr/share/elasticsearch/config/dict", BindMode.READ_ONLY) : 디렉토리 마운트
- .addExposedPorts(9200) : 포트 바인딩 (컨테이너 안에서 노출할 포트 지정)
- esContainer.getContainerIpAddress() : 컨테이너에 접근할 수 있는 IP
- esContainer.getMappedPort(9200) : 컨테이너 안 9200 포트에 바인딩 된 외부 포트 반환
- esContainer.start() : 드디어 컨테이너 띄우기
- esContainer.stop() : 컨테이너 내리기
이 경우에는 사용 가능한 포트를 찾아 컨테이너 내부의 9200 포트와 랜덤으로 바인딩시켜줍니다. 랜덤말고 지정된 포트를 사용하고 싶으시다구요? 전 이렇게 했습니다
import com.github.dockerjava.api.model.*;
import org.junit.jupiter.api.extension.AfterAllCallback;
import org.junit.jupiter.api.extension.BeforeAllCallback;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.images.builder.ImageFromDockerfile;
public class ContainerExtension implements BeforeAllCallback, AfterAllCallback {
static GenericContainer<?> esContainer;
@Override
public void beforeAll(ExtensionContext context) throws Exception {
esContainer = new GenericContainer<>(new ImageFromDockerfile().withDockerfileFromBuilder(
builder -> builder.from("docker.elastic.co/elasticsearch/elasticsearch:7.8.1")
.run("bin/elasticsearch-plugin", "install", "analysis-nori")
.build()
));
esContainer
.withEnv("discovery.type", "single-node")
.withEnv("http.host", "0.0.0.0")
.withCreateContainerCmdModifier(command -> {
HostConfig hostConfig = new HostConfig();
hostConfig.withBinds(new Bind("./dict", new Volume("/usr/share/elasticsearch/config/dict")));
hostConfig.withPortBindings(new PortBinding(Ports.Binding.bindPort(32880), new ExposedPort(9200)));
command.withHostConfig(hostConfig);
}
);
esContainer.start();
String host = String.format("http://%s:%s", esContainer.getContainerIpAddress(), esContainer.getMappedPort(9200));
System.out.println(host); // http://localhost:32880
}
@Override
public void afterAll(ExtensionContext context) {
esContainer.close();
}
}
달라진 부분은 이 부분입니다.
.withCreateContainerCmdModifier(command -> {
HostConfig hostConfig = new HostConfig();
hostConfig.withBinds(new Bind("./dict", new Volume("/usr/share/elasticsearch/config/dict")));
hostConfig.withPortBindings(new PortBinding(Ports.Binding.bindPort(32880), new ExposedPort(9200)));
command.withHostConfig(hostConfig);
});
withPortBindings() 로 직접 매핑할 포트를 지정할 수 있습니다.
다만 withCreateContainerCmdModifier() 를 적용하고나니 withFileSystemBind() 로는 디렉토리 마운트가 안 먹혀서 hostConfig.withBinds() 로 변경했습니다.
자 이제 테스트를 실행하면,
특정 조건일 때 이미지를 지워주는 testcontainers-* 라는 이름의 testcontainers/ryuk 이미지 컨테이너가 먼저 올라가고, 곧바고 랜덤 이름(happy_thompson)의 엘라스틱 이미지가 올라갑니다. 지정된 포트 32880 가 보이시죠?
Java 에서 테스트용 도커 컨테이너 띄우는 법 : TestContainer
글 읽어주셔서 언제나 감사합니다. 좋은 피드백, 개선 피드백 너무나도 환영합니다.
'SearchDeveloper > Java' 카테고리의 다른 글
ListenableFuture 의 Callback Hell 해결하기 (0) | 2023.05.03 |
---|---|
[JPA] 영속성 컨텍스트(Persistence Context) : 엔티티 관리 공간 (0) | 2022.10.03 |
Java 애플리케이션 메모리 누수(Memory leak) 잡기 - jstat, MAT (0) | 2022.10.02 |
[JAVA] stream 코드 스니펫 (0) | 2021.05.30 |
[번역] Pro Java Programming - 자바 아키텍쳐 (0) | 2020.10.18 |