Habrã«ã¯TestContainersã«é¢ããæ å ±ã¯ãŸã£ãããããŸããã ãã®èšäºã®å·çæç¹ã§ã¯ãäŒè°ã®æ€çŽ¢çµæã«çºè¡šããããããã ãã§ãã äžæ¹ã圌ãã¯ãã§ã«GitHubãããžã§ã¯ãã§ 700以äžã®ã³ãããã54人ã®è²¢ç®è ã5å¹Žã®æŽå²ãæã£ãŠããŸã ã ãã®5幎éããããžã§ã¯ãã¯ã·ãŒã¯ã¬ãããµãŒãã¹ãšUFOã«ãã£ãŠæ éã«é ãããŠããããã§ãã 圱ããå ã®äžã«åºããšãã§ãã
ãã¥ã¯ãã¯èªè ã§ãããäœå®¶ã§ã¯ãããŸããã ãããã£ãŠãããã¹ããæžã代ããã«ãRebelLabsããã°ãã察å¿ããèšäºã翻蚳ããèš±å¯ãæ±ããŸããã
ãã®ãããããã§ã¯çµ±åãã¹ãçšã®ææ°ã®Javaã©ã€ãã©ãªã§ããTestContainersã«ã€ããŠããã€ãã®èšèãå ±æããŸã ã ãã以å€ã¯ãZeroTurnaroundã«ãšã£ãŠçµ±åãã¹ããéåžžã«éèŠã§ããçç±ãšãçµ±åãã¹ãã®èŠä»¶ã«ã€ããŠã¯ã»ãšãã©èª¬æããŸããã ãããŠãã¡ãããJavaãšãŒãžã§ã³ãã®å®å šã«æ©èœããçµ±åãã¹ãã®äŸããããŸãã 誰ããJavaãšãŒãžã§ã³ãã³ãŒããç®ã«ããããšããªãå Žåãä»ããã®æã§ãã ç«ãžããããïŒ
ZeroTurnaroundã§ã®çµ±åãã¹ã
ZeroTurnaround補åã¯ãJavaãšã³ã·ã¹ãã ã®å€ããšçµ±åãããŸãã ç¹ã«ãJRebelãšXRebelã¯JavaãšãŒãžã§ã³ããã¯ãããžãŒã«åºã¥ããŠãããJavaã¢ããªã±ãŒã·ã§ã³ããã¬ãŒã ã¯ãŒã¯ãã¢ããªã±ãŒã·ã§ã³ãµãŒããŒãªã©ãšçµ±åãããŠããŸãã
JavaãšãŒãžã§ã³ãã䜿çšãããšãå¿ èŠãªè¿œå æ©èœã远å ããããJavaã³ãŒãã«æç€ºã§ããŸãã ãããã®é©çšåŸãã¢ããªã±ãŒã·ã§ã³ã®åäœããã¹ãããã«ã¯ãäºåã«æ§æãããJavaãšãŒãžã§ã³ããä»ããŠå®è¡ããå¿ èŠããããŸãã ã¢ããªã±ãŒã·ã§ã³ãèµ·åããŠå®è¡ããããšãHTTPèŠæ±ãã¢ããªã±ãŒã·ã§ã³ã«éä¿¡ããŠãç®çã®åäœãåçŸã§ããŸãã
ãã®ãããªãã¹ããå€§èŠæš¡ã«å®è¡ããã«ã¯ãã¢ããªã±ãŒã·ã§ã³ãµãŒããŒãã¢ããªã±ãŒã·ã§ã³ãäŸåããä»ã®å€éšäŸåé¢ä¿ãªã©ãã©ã³ã¿ã€ã ãéå§ããã³åæ¢ã§ããèªåã·ã¹ãã ãå¿ èŠã§ãã ç¶ç¶ççµ±åç°å¢ãšéçºè ã®ã³ââã³ãã¥ãŒã¿ãŒã®äž¡æ¹ã§ãã»ãŒåãããšãå®è¡ã§ããã¯ãã§ãã
ãã®çµæã倿°ã®ãã¹ããè¡ââããããããã¯ãŸã£ããè¿ éã«æ©èœããŸããããããã£ãŠããããã䞊è¡ããŠå®è¡ããããšã¯ééããããŸããã ããã¯ããªãœãŒã¹ã®ç«¶åããªãããã«ãã¹ããåé¢ããå¿ èŠãããããšãèªåçã«æå³ããŸãã ããšãã°ãåããã¹ãäžã§Tomcatã®è€æ°ã®ã€ã³ã¹ã¿ã³ã¹ãå®è¡ããå ŽåãããŒã䜿çšã®ç«¶åãåé¿ããããšæããŸãã
ãã®ãããªçµ±åãã¹ãã§ã¯ãå°ãããŠçŸããTestContainersã©ã€ãã©ãªã圹ã«ç«ã¡ãŸãã äžèšã®èŠä»¶ã«è¿ã¥ããã ãã§ãªããå®è£ åŸãçç£æ§ãå€§å¹ ã«åäžããŸããã
ãã¹ãã³ã³ãã
TestContainersã®å ¬åŒããã¥ã¡ã³ãã«ã¯æ¬¡ã®ããã«æžãããŠããŸãïŒ
ãTestContainersã¯ãJUnitãã¹ãããµããŒãããJavaã©ã€ãã©ãªã§ãããã³ã¢ããŒã¿ããŒã¹ã®è»œéã®äžæã€ã³ã¹ã¿ã³ã¹ãSeleniumã®Webãã©ãŠã¶ãŒããŸãã¯Dockerã³ã³ãããŒã§å®è¡ã§ããä»ã®ãã®ãæäŸããŸããã
TestContainersã¯ãç°å¢ã®ã«ã¹ã¿ãã€ãºãèªååããããã®APIãæäŸããŸãã ãã¹ãã®æéäžã«å¿ èŠãªDockerã³ã³ãããæ£ç¢ºã«èµ·åãããã¹ããå®äºãããšããã«ããããæ¶ããŸãã ããã«ãGitHubã®ãªããžããªã«ããå ¬åŒäŸã«åºã¥ããããã€ãã®ãã¢ãèŠãŠãããŸãã
GenericContainer
TestContainersã䜿çšããå ŽåãGenericContainerã¯ã©ã¹ãéåžžã«é »ç¹ã«äœ¿çšãããŸãã
public class RedisBackedCacheTest { @Rule public GenericContainer redis = new GenericContainer("redis:3.0.6") .withExposedPorts(6379);
ãã®ã³ã³ã¹ãã©ã¯ã¿ãŒã¯ãDockerã€ã¡ãŒãžã瀺ãæååããã©ã¡ãŒã¿ãŒãšããŠåãåããŸããããã¯ä»åŸäœ¿çšããŸãã èµ·åæã«ãTestContainersã¯é©åãªã€ã¡ãŒãžãèªåçã«ããŠã³ããŒãããŸãïŒä»¥åã«ããŠã³ããŒãããŠããªãå ŽåïŒã
éèŠãªæ³šæïŒ withExposedPorts(6379)
ã¡ãœããã§ã¯ã6379ã¯ã³ã³ããããã³ã°ããããŒãã§ãã æ¬¡ã«ãã³ã³ããã€ã³ã¹ã¿ã³ã¹ã§getMappedPort(6379)
ã¡ãœãããåŒã³åºãããšã«ããã察å¿ããé¢é£ããŒããèŠã€ããããšãã§ããŸãã ãããgetContainerIpAddress()
ãšçµã¿åããããšãã³ã³ããã§å®è¡ãããŠãããµãŒãã¹ã®å®å
šãªURLãååŸã§ããŸãã
String redisUrl = redis.getContainerIpAddres() + â:â + redis.getMappedPort(6379);
ãã®äŸã®ãã£ãŒã«ãã«ã¯@Rule
泚éãä»ããŠããããšã«æ°ä»ããããããŸããã JUnitã®@Ruleã¢ãããŒã·ã§ã³ã¯ ããã®ã¯ã©ã¹ã®åãã¹ãã¡ãœããã§GenericContainer
æ°ããã€ã³ã¹ã¿ã³ã¹ãåãåãããšã決å®ããŸãã ã³ã³ããã€ã³ã¹ã¿ã³ã¹ãåå©çšããå Žåã¯ã @ClassRule
ã¢ãããŒã·ã§ã³ãååšããŸãã
ã¿ã¹ã¯ã³ã³ãã
GenericContainer
ç¶æ¿è
ã¯ã GenericContainer
åºæã®ã³ã³ããã§ãã ããŒã¿ã¢ã¯ã»ã¹ã®ã¬ãã«ããã¹ãããããã«ãMySQLãPostgreSQLãããã³Oracleã®ã³ã³ããåãããã€ã¡ãŒãžãããã«äœ¿çšã§ããŸãã
PostgreSQLContainer postgreSQLContainer = new PostgreSQLContainer("postgres:9.6.2") .withUsername(POSTGRES_USERNAME) .withPassword(POSTGRES_PASSWORD);
ãã®1è¡ã ãã§ããã¹ãäžãã£ãšæ®ã£ãŠããã³ã³ããã®ã³ããŒãååŸã§ããŸãã ãã¹ããå®è¡ãããã·ã³ã§ã¯ãããŒã¿ããŒã¹ãæåã§ã€ã³ã¹ããŒã«ããå¿ èŠã¯ãããŸããã åãããŒã¿ããŒã¹ã®è€æ°ã®ããŒãžã§ã³ã§ãã¹ããããå Žåãããã¯ç¹ã«å€§ããªå©çããããããŸãã
èªåã®ã³ã³ãã
GenericContainer
ããç¶æ¿ãããšãæ°ããã¿ã€ãã®ã³ã³ãããäœæã§ããŸãã ããã¯ã察å¿ãããµãŒãã¹ãšããžãã¯ãã«ãã»ã«åããå Žåã«éåžžã«äŸ¿å©ã§ãã ããšãã°ã MockServerã䜿çšããŠãã¢ããªã±ãŒã·ã§ã³ãHTTPãä»ããŠçžäºã«éä¿¡ãã忣ã·ã¹ãã ã®äŸåé¢ä¿ãããã¯ã§ããŸãã
public class MockServerContainer extends BaseContainer<MockServerContainer> { MockServerClient client; public MockServerContainer() { super("jamesdbloom/mockserver:latest"); withCommand("/opt/mockserver/run_mockserver.sh -logLevel INFO -serverPort 80"); addExposedPorts(80); } @Override protected void containerIsStarted(InspectContainerResponse containerInfo) { client = new MockServerClient(getContainerIpAddress(), getMappedPort(80)); } }
ãã®äŸã§ã¯ãã³ã³ãããŒãåæåãããçŽåŸã«containerIsStarted(...)
ã³ãŒã«ããã¯ã䜿çšããã MockServerClient
ã€ã³ã¹ã¿ã³ã¹ãåæåãããŸãã ãããã£ãŠãç¬èªã®ã³ã³ããã¿ã€ãå
ã®ã³ã³ããã«åºæã®å®è£
ã®è©³çްããã¹ãŠé ããŸããã ããã«ããããã¹ãçšã®ããã¯ãªãŒã³ãªã¯ã©ã€ã¢ã³ãã³ãŒããšããããããªAPIãåŸãããŸããã
ããã«ãæåã§å®çŸ©ãããã³ã³ããããJavaãšãŒãžã§ã³ãããã¹ãããããã®ç°å¢ã®æ§ç¯ã«åœ¹ç«ã€ããšãããããŸãã
TestContainersã䜿çšããJavaãšãŒãžã§ã³ãã®ãã¹ã
ãã®ã¢ã€ãã¢ãå®èšŒããããã«ãTestContainersãããžã§ã¯ãã®å ±åã¹ãã³ãµãŒã§ããSergey @bsideup Egorovãã芪åã«æäŸãããäŸã䜿çšããŸãã
ãã¢ã¢ããªã±ãŒã·ã§ã³
ãã¹ãã¢ããªã±ãŒã·ã§ã³ããå§ããŸãããã HTTP GETãªã¯ãšã¹ãã«å¿çããWebã¢ããªã±ãŒã·ã§ã³ãå¿ èŠã§ãã èèªã®å€ããã¬ãŒã ã¯ãŒã¯ã¯å¿ èŠãããŸãããSparkJavaã䜿çšããŠã¿ãŸãããïŒ æ¥œããã远å ããããã«ãããã«Groovyã§ã³ãŒãã£ã³ã°ãå§ããŸããã ããã§ããã®ã¢ããªã±ãŒã·ã§ã³ããã¹ãããŸãã
//app.groovy @Grab("com.sparkjava:spark-core:2.1") import static spark.Spark.* get("/hello/") { req, res -> "Hello!" }
ããã¯ã Grapeã䜿çšããŠSparkJavaã®äŸåé¢ä¿ãããŒããããHelloïŒãã¡ãã»ãŒãžã§å¿çããåäžã®HTTPãšã³ããã€ã³ããå®çŸ©ããåçŽãªGroovyã¹ã¯ãªããã§ãã
JavaãšãŒãžã§ã³ã
確èªããããšãããšãŒãžã§ã³ãã¯ãJettyãµãŒããŒã«ããããé©çšããHTTPå¿çã«è¿œå ã®ããããŒã远å ããŸãã
public class Agent { public static void premain(String args, Instrumentation instrumentation) { instrumentation.addTransformer( (loader, className, clazz, domain, buffer) -> { if ("spark/webserver/JettyHandler".equals(className)) { try { ClassPool cp = new ClassPool(); cp.appendClassPath(new LoaderClassPath(loader)); CtClass ct = cp.makeClass(new ByteArrayInputStream(buffer)); CtMethod ctMethod = ct.getDeclaredMethod("doHandle"); ctMethod.insertBefore("{ $4.setHeader(\"X-My-Super-Header\", \"42\"); }"); return ct.toBytecode(); } catch (Throwable e) { e.printStackTrace(); } } return buffer; }); } }
ãã®äŸã§ã¯JettyHandler.doHandle
䜿çšãJettyHandler.doHandle
ã¡ãœããã«JettyHandler.doHandle
é©çšãã X-My-Super-Header
ãèšå®ãã远å ã³ãã³ãã远å ããŸãã
ãã¡ãããJavaãšãŒãžã§ã³ãã«ãªãã«ã¯ãããã±ãŒãžã«æ£ããã¢ã»ã³ãã«ããé©åãªå±æ§ãMANIFEST.MF
ãã¡ã€ã«ã«è¿œå ããå¿
èŠããããŸãã ã¢ã»ã³ããªã¹ã¯ãªããã¯ãèšäºãç
©éã«ããªãããã«ããããã¹ãŠè¡ããŸããGitHubã«æçš¿ãããŸããbuild.gradeãã¡ã€ã«ã®å
容ãåç
§ããŠãã ããã
å®éã«ããã¹ãïŒ
ãã¹ãã¯éåžžã«ç°¡åã§ããã¢ããªã±ãŒã·ã§ã³ã«ãªã¯ãšã¹ããè¡ããç¹å¥ãªããããŒã®å¿çã確èªããå¿ èŠããããŸããç¹å¥ãªããããŒã¯ãçè«çã«ã¯ããã«è¿œå ããŸãã ã¿ã€ãã«ãèŠã€ãããã¿ã€ãã«ã®å€ãæåŸ å€ãšäžèŽããå Žåããã¹ãã¯æ£åžžã«åæ ŒããŸããã ã³ãŒããèŠãŠãã ããïŒ
@Test public void testIt() throws Exception { // Using Feign client to execute the request Response response = app.getClient().getHello(); assertThat(response.headers().get("X-My-Super-Header")) .isNotNull() .hasSize(1) .containsExactly("42"); }
IDEããçŽæ¥å®è¡ããããšããã³ãã³ãã©ã€ã³ããå®è¡ããããšããç¶ç¶çãªçµ±åç°å¢ã§å®è¡ããããšãã§ããŸãã TestContainersã¯ããšãŒãžã§ã³ããéé¢ãããç°å¢ãDockerã³ã³ãããŒã«ããããã«ãã¢ããªã±ãŒã·ã§ã³ãèµ·åããã®ã«åœ¹ç«ã¡ãŸãã
ã¢ããªã±ãŒã·ã§ã³ãå®è¡ããã«ã¯ãGroovyããµããŒãããDockerã€ã¡ãŒãžãå¿
èŠã§ãã å¿«é©ã«ããããã«ãDockerã€ã¡ãŒãžzeroturnaround / groovyãååŸããŸãããããã¯Docker Hubã«ãããŸãã GenericContainer
ããç¶æ¿ããŠäœ¿çšããæ¹æ³ã¯æ¬¡ã®ãšããã§ãã
public class GroovyTestApp<SELF extends GroovyTestApp<SELF>> extends GenericContainer<SELF> { public GroovyTestApp(String script) { super("zeroturnaround/groovy:2.4.5"); withClasspathResourceMapping("agent.jar", "/agent.jar", BindMode.READ_ONLY); withClasspathResourceMapping(script, "/app/app.groovy", BindMode.READ_ONLY); withEnv("JAVA_OPTS", "-javaagent:/agent.jar"); withCommand("/opt/groovy/bin/groovy /app/app.groovy"); withLogConsumer(new Slf4jLogConsumer(LoggerFactory.getLogger(script))); } public String getURL() { return "http://" + getContainerIpAddress() + ":" + getMappedPort(getExposedPorts().get(0)); } }
APIãã³ã³ããã®IPã¢ãã¬ã¹ãšé¢é£ããŒãïŒå®éã«ã¯ã©ã³ãã åãããŠããïŒãååŸããã¡ãœãããã©ã®ããã«æäŸããŠããããã芧ãã ããã ã€ãŸãããã¹ããå®è¡ããããã³ã«ããŒããç°ãªããŸãã ãããã£ãŠããã¹ãŠã®ãã¹ããåæã«å®è¡ããå ŽåãããŒãéã§ç«¶åã¯çºçããããã¹ãã¯åŽ©ããŸããã
ããã§ãGroovyã§ã¹ã¯ãªãããç°¡åã«äœæã§ããç¹å¥ãªGroovyTestApp
ã¯ã©ã¹ãã§ããŸããããã®å Žåããã¢ã¢ããªã±ãŒã·ã§ã³ããã¹ãããããã®ãã®ã§ãã
GroovyTestApp app = new GroovyTestApp(âapp.groovyâ) .withExposedPorts(4567); //the default port for SparkJava .setWaitStrategy(new HttpWaitStrategy().forPath("/hello/"));
ãã¹ããå®è¡ããææ°ãèŠãŠãã ããïŒ
$ ./gradlew test 16:42:51.462 [I] d.DockerClientProviderStrategy - Accessing unix domain socket via TCP proxy (/var/run/docker.sock via localhost:50652) ⊠⊠⊠16:43:01.497 [I] app.groovy - STDERR: [Thread-1] INFO spark.webserver.SparkServer - == Spark has ignited ... 16:43:01.498 [I] app.groovy - STDERR: [Thread-1] INFO spark.webserver.SparkServer - >> Listening on 0.0.0.0:4567 16:43:01.511 [I] app.groovy - STDERR: [Thread-1] INFO org.eclipse.jetty.server.Server - jetty-9.0.2.v20130417 16:43:01.825 [I] app.groovy - STDERR: [Thread-1] INFO org.eclipse.jetty.server.ServerConnector - Started ServerConnector@72f63426{HTTP/1.1}{0.0.0.0:4567} 16:43:02.199 [I] ?.4.5] - Container zeroturnaround/groovy:2.4.5 started AgentTest > testIt STANDARD_OUT Got response: HTTP/1.1 200 OK content-length: 6 content-type: text/html; charset=UTF-8 server: Jetty(9.0.2.v20130417) x-my-super-header: 42 Hello! BUILD SUCCESSFUL Total time: 36.014 secs
ãã®ãã¹ãã¯éåžžã«é«éã§ã¯ãããŸããã Grapesã®ããŠã³ããŒãã«ã¯æéãããããŸããããããåããŠã§ãã ãã ããããã¯ãHTTPã¹ã¿ãã¯ã䜿çšããã¢ããªã±ãŒã·ã§ã³ã§ããDockerã³ã³ãããŒãèµ·åããHTTPèŠæ±ãè¡ãå®å šãªçµ±åãã¹ãã§ãã ããã«ãã¢ããªã±ãŒã·ã§ã³ã¯åç¬ã§å®è¡ãããéåžžã«ç°¡åã§ãã ãããŠãã®ãã¹ãŠ-TestContainersã«æè¬ããŸãïŒ
ãããã«
ãããã¯ç§ã®ã³ã³ãã¥ãŒã¿ãŒã§åäœããŸãããšããã®ã¯äžè¬çãªèšãèš³ã§ããããã¯ãèšãèš³ã«ãªãã¹ãã§ã¯ãããŸããã ã³ã³ããåæè¡ããŸããŸãå€ãã®éçºè ã«å©çšå¯èœã«ãªããšããŸããŸã決å®çãªãã¹ããè¡ãããšãå¯èœã«ãªããŸãã
TestContainersã¯ãJavaã¢ããªã±ãŒã·ã§ã³çµ±åãã¹ãã®çæ°ã®éãæžãããŸãã ãã®ã©ã€ãã©ãªã¯ãæ¢åã®ãã¹ãã«ç°¡åã«çµ±åã§ããŸãã å€éšäŸåé¢ä¿ãæåã§ç®¡çããå¿ èŠããªããªããŸãããããã¯ãç¹ã«ç¶ç¶çãªçµ±åç°å¢ã§ã¯å€§ããªåå©ã§ãã
èªãã å å®¹ãæ°ã«å ¥ã£ãå Žåã¯ãGeekOut Javaã«ã³ãã¡ã¬ã³ã¹ã®èšé²ãã芧ã«ãªãããšã匷ããå§ãããŸã ããããžã§ã¯ãã®å äœè ã§ãããªãã£ãŒãããŒã¹ã¯ãéçºèšç»ãå«ãTestContainersã®èæ¯æ å ±ãæäŸããŠããŸã ã ãŸãã¯ãå°ãªããšããã®ãã¬ãŒã³ããŒã·ã§ã³ã®ã¹ã©ã€ããèŠãŠãã ããã
翻蚳è ããã®ããã€ãã®èšèã
ãŸããäžæ£ç¢ºããšã©ãŒãã¿ã€ããã¹ãèŠã€ããå Žåãå人ã¢ã«ãŠã³ãã§ãªã¬ã°ãã£ãŒã«è¡ãããã¹ãŠããã®ãŸãŸèª¬æããå¿ èŠããããŸãã ç§ã¯æ¬åœã«ã¡ãã»ãŒãžãèªã¿ããã°ãä¿®æ£ããŸãã
Java ãæ°ãããã¯ãããžãŒãã©ã€ãã©ãªã«èå³ãããå Žåã¯ãJavaã«ã³ãã¡ã¬ã³ã¹ã«ã¢ã¯ã»ã¹ããŠãã ããã æãè¿ãã®ã¯JPointãšJBreakã§ãã ã¡ãªã¿ã«ãZeroTurnaroundã®åŸæ¥å¡ã¯ãå€ãã®å ŽåãäŒè°ã§è¬æŒè ãšããŠè¬æŒããããã°ã©ã å§å¡äŒã®ã¡ã³ããŒãšããŠåããŠããŸãã
ãã¹ãã«èå³ãããå Žå㯠ã Heisenbug 2017 Moscowã«ã³ãã¡ã¬ã³ã¹ãéå¬ããŸããããã¯ã1é±éåã§æåéãéå¬ãããŸãã Dockerã䜿çšãããã¹ãã®ãããã¯ã¯ãå€ãã®ã¬ããŒãã«äœããã®åœ¢ã§èšèŒãããŠããŸã ã