尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Testcontainers 实战:使用 ComposeContainer 在 JUnit 测试中启动 Docker Compose 服务

Testcontainers 实战:使用 ComposeContainer 在 JUnit 测试中启动 Docker Compose 服务 Testcontainers 实战使用 ComposeContainer 在 JUnit 测试中启动 Docker Compose 服务【免费下载链接】testcontainers-javaTestcontainers is a Java library that supports JUnit tests, providing lightweight, throwaway instances of common databases, Selenium web browsers, or anything else that can run in a Docker container.项目地址: https://gitcode.com/GitHub_Trending/te/testcontainers-javaTestcontainers Java 的ComposeContainer核心库自带允许你直接基于docker-compose.yml启动一组服务让测试环境与开发环境共享同一套服务定义。本文以 Docker Compose Module 文档 为主线结合仓库源码ComposeContainer.java、ComposeDelegate.java与对应测试完整讲解创建容器、访问服务、等待策略、Local Compose 模式、文件拷贝与私有仓库认证等实战细节读完后你可以直接在自己的 JUnit 测试中落地使用。为什么使用 Docker Compose 模块与单个容器GenericContainer类似Testcontainers 也支持启动由docker-compose.yml定义的一组服务。这对于那些在开发环境或其他环境已经使用 Docker Compose 来定义应用依赖服务的项目尤其有用——测试可以直接复用同一份 Compose 文件无需在代码里重新逐个声明容器。关键点在于ComposeContainer基于Compose V2实现让你在测试中使用与本地开发几乎一致的依赖定义。从源码看ComposeContainer.java 明确注释其 Testcontainers implementation for Docker Compose V2底层既可使用 Docker 内置的 Compose V2 命令也可使用容器化的 Compose V2。快速上手创建 ComposeContainer文档给出的核心用法是只需要一个基于docker-compose.yml定义的ComposeContainer类就足以启动测试所需的任意数量服务。参考官方测试 ComposeContainerTest.java典型构造方式如下public ComposeContainer environment new ComposeContainer( DockerImageName.parse(docker:25.0.5), new File(src/test/resources/composev2/compose-test.yml) ) .withExposedService(redis-1, REDIS_PORT) .withExposedService(db-1, 3306);对应的docker-compose.yml内容大致为services: redis: image: redis db: image: mysql:8.0.36注意请确保 Compose 文件中的服务名使用-连字符而不是_下划线作为分隔符。这里有两个容易忽略的关键约定无需在 YAML 中声明暴露端口。在 YAML 中显式定义ports反而会妨碍该文件在其他上下文中被复用/包含。Testcontainers 会启动一个小型ambassador大使容器在 Compose 管理的容器与测试可访问的端口之间做代理转发。服务实例的命名规则。从 ComposeDelegate.java 可以看到getServiceInstanceName会给未带实例号的服务名自动补上-1Compose V2 的分隔符为-因此redis会被识别为redis-1。这也是文档要求服务名使用-而非_的源码原因——V1 模式下的分隔符是_见 ComposeDelegate.java 中ComposeVersion枚举。ambassador 容器是如何工作的从源码可以看清 ambassador 容器的实现细节ComposeDelegate.java对每个需要暴露的「服务名/端口」组合Testcontainers 会在 ambassador 容器上注册一个目标端口并建立到对应 Compose 服务的 TCP 代理链路ambassador 容器在 Docker 网络内与 Compose 服务链接addLink使用FutureContainer延迟解析把内部流量代理到其自身暴露的端口上这避免了 Compose 文件必须显式暴露端口的要求同时也让 host 机器上的测试代码通过 ambassador 容器就能访问到内部服务。ComposeContainer vs DockerComposeContainer文档特别区分了两个 APIComposeContainer基于Compose V2是推荐使用的类本文所有示例均可适用DockerComposeContainer基于Compose V1Docker 已将其标记为弃用在仓库中位于 DockerComposeContainer.java。两个 API 非常相似本文页面上的大多数示例对两者都适用。从源码对比可以看出差异ComposeContainer.COMPOSE_EXECUTABLE在 Windows 下为docker.exe其他平台为docker利用 Docker 内置的 Compose V2 子命令DockerComposeContainer.COMPOSE_EXECUTABLE则为独立的docker-compose可执行文件见 DockerComposeContainer.java。这也是两者底层执行机制的本质区别一个走docker compose子命令一个走独立的docker-compose二进制。访问容器服务获取 Host 与 PortComposeContainer提供了两个核心方法用于让测试代码发现如何与容器交互getServiceHost(serviceName, servicePort)返回容器监听的 IP 地址通过 ambassador 容器getServicePort(serviceName, servicePort)返回已被暴露的端口在 Docker 中映射出的端口通过 ambassador 容器。官方测试 ComposeContainerTest.java 展示了实际用法String serviceHost environment.getServiceHost(redis-1, REDIS_PORT); int serviceWithInstancePort environment.getServicePort(redis-1, REDIS_PORT); // 不带实例号的服务名也能工作 int serviceWithoutInstancePort environment.getServicePort(redis, REDIS_PORT);结合源码ComposeDelegate.java可以确认getServiceHost()实际返回的是 ambassador 容器的 hostambassadorContainer.getHost()getServicePort()通过内部维护的ambassadorPortMappings服务名 → 服务端口 → ambassador 端口找到映射再返回 ambassador 容器映射到宿主机的端口若某服务从未通过withExposedService声明暴露会抛出IllegalArgumentException并提示如何修复。测试中还展示了getContainerByServiceName的用法ComposeContainerTest.java可以通过服务名拿到对应的ContainerState已暴露端口等查不到时返回Optional.empty()。等待策略与启动超时默认情况下Testcontainers 会等待每个被暴露容器的第一个映射网络端口开始监听最长等待60 秒以此作为容器是否就绪的基础检查。withExposedService提供了接受WaitStrategy的重载方法可以为每个容器单独指定超时策略。你可以使用流式 API 创建自定义策略参见 启动与等待策略文档或直接使用Wait类中现成的静态工厂方法。官方测试 ComposeContainerWithWaitStrategiesTest.java 演示了「等待端口监听 自定义 30 秒超时」ComposeContainer compose new ComposeContainer( DockerImageName.parse(docker:25.0.5), new File(src/test/resources/composev2/compose-test.yml) ) .withExposedService( redis-1, REDIS_PORT, Wait.forListeningPort().withStartupTimeout(Duration.ofSeconds(30)) );同一个 Compose 环境中的不同服务可以使用各自不同的等待策略。例如 Redis 容器等待redis-cli命令执行成功而 db 服务等待特定的日志消息ComposeContainerWithWaitStrategiesTest.javaComposeContainer compose new ComposeContainer( DockerImageName.parse(docker:25.0.5), new File(src/test/resources/composev2/compose-test.yml) ) .withExposedService(redis-1, REDIS_PORT, Wait.forSuccessfulCommand(redis-cli ping)) .withExposedService(db-1, 3306, Wait.forLogMessage(.*ready for connections.*\\n, 1));从 ComposeDelegate.java 可以看到底层实现同一服务上的多个等待策略会被聚合进一个WaitAllStrategy模式为WITH_MAXIMUM_OUTER_TIMEOUT外层启动超时上限默认是30 分钟Duration.ofMinutes(30)见 ComposeDelegate.java。这也解释了文档中60 秒默认等待与源码中外层 30 分钟上限并存的原因60 秒是默认监听端口等待策略自身的时限30 分钟是所有策略的外层兜底上限。此外ComposeContainer还提供了withStartupTimeout(Duration)方法可全局调整所有等待策略的外层上限ComposeContainer.java。Local Compose 模式默认情况下 Testcontainers 会在容器内运行 Compose。如果希望直接使用宿主机上安装的docker compose二进制可以覆盖默认行为进入Local Compose 模式。这种模式通常更接近在本地手动运行docker compose的体验但代价是开发机和 CI 机器上都必须预先安装 Docker Compose。官方测试 ComposeProfilesOptionTest.java 演示了 Local Compose 模式的用法ComposeContainer compose new ComposeContainer(COMPOSE_FILE) .withOptions(--profilecache);注意这里与容器化模式的区别使用不带镜像参数的构造函数new ComposeContainer(File...)即进入 Local Compose 模式。从 ComposeContainer.java 可以看到这些构造器内部会设置localCompose true而带DockerImageName参数如docker:25.0.5的构造器则走容器化 Compose。Local Compose 模式下Testcontainers 使用LocalDockerCompose在宿主机直接执行docker compose命令而容器化模式则使用ContainerisedDockerCompose把 Compose 文件与相关文件拷贝进一个运行 Compose 的容器中执行见 ComposeDelegate.java。构建工作目录与文件拷贝默认情况下compose 文件所在目录下的所有文件都会被拷贝进运行 Compose 的容器中。如果想只拷贝特定文件可以使用withCopyFilesInContainer精确指定官方测试 ComposeContainerWithCopyFilesTest.java 演示如下ComposeContainer environment new ComposeContainer( DockerImageName.parse(docker:25.0.5), new File(src/test/resources/compose-file-copy-inclusions/compose-test-only.yml) ) .withExposedService(app, 8080) .withCopyFilesInContainer(Dockerfile, EnvVariableRestEndpoint.java, test);要点只拷贝 compose 文件和 env 文件时就只传入这两个文件名支持文件引用和目录引用路径始终相对于 compose 文件所在的目录解析上面的测试验证了当.env文件未被拷贝时应用读到的是原始环境变量值当把整个test目录拷贝进去后读到的是被覆盖后的值ComposeContainerWithCopyFilesTest.java如果所需的 env 文件没有被拷贝启动会以ContainerLaunchException失败同测试文件testShouldNotBeAbleToStartIfNeededEnvFileIsNotCopied。注意withCopyFilesInContainer可用于DockerComposeContainer和ComposeContainer但仅在容器化 Compose 模式下生效Local Compose 模式下不可用。在 Docker Compose 中使用私有仓库当 Docker Compose 运行在容器模式而非 Local 模式时需要让它感知到 Docker 针对私有仓库的认证设置。默认情况下这些设置位于$HOME/.docker/config.json。有 3 种方式指定config.json的位置使用DOCKER_CONFIG_FILE环境变量export DOCKER_CONFIG_FILE/some/location/config.json使用dockerConfigFileJava 系统属性java -DdockerConfigFile/some/location/config.json不指定任何配置此时若$HOME/.docker/config.json存在则使用该默认位置。仓库中还可见另一条路径在宿主侧RegistryAuthLocator.java 会读取DOCKER_CONFIG环境变量默认user.home/.docker来定位宿主机上的config.json。Docker Compose 与 Credential Store / Credential Helper 的兼容问题现代 Docker 倾向于使用credential store/helper 机制存储凭据而不是把凭据直接写进 Docker 配置文件。因此你的config.json可能长这样{ auths : { https://index.docker.io/v1/ : { } }, credsStore : osxkeychain }问题在于在容器内运行时Docker Compose 无法访问宿主机的 Keychain因而这份配置实际上毫无用处。针对这一问题有两种解决方式方式一把真实认证信息放进独立的 config 文件在单独位置创建一个包含真实认证密钥的config.json{ auths : { https://index.docker.io/v1/ : { auth: QWEADSZXC... } }, credsStore : osxkeychain }然后用前面提到的两种方法DOCKER_CONFIG_FILE环境变量或-DdockerConfigFile系统属性把该位置告知 Testcontainers。方式二使用 Local Compose 模式正如上文所述Local Compose 模式允许 Compose 直接访问 Docker 认证系统效果与手动运行docker-composeCLI 一致因而不存在容器内访问不到 Keychain 的问题。更多常用 API来自源码除了文档重点介绍的 API从 ComposeContainer.java 源码还可以看到一系列常用的流式配置方法可进一步提升 Compose 测试的灵活性withServices(String...)仅启动指定服务不传则启动全部服务withScaledService(serviceBaseName, numInstances)按指定实例数扩容某个服务如--scale见 ComposeContainerScalingTest.javawithEnv(key, value)/withEnv(Map)向 Compose 与 ambassador 容器传递环境变量withPull(boolean)是否先拉取镜像默认true拉取失败时会回退使用本地已有镜像ComposeContainer.javawithBuild(boolean)启动前是否总是构建镜像追加--buildwithOptions(String...)向docker compose命令追加额外选项例如测试中使用的--profilecache、--compatibilitywithRemoveImages(RemoveImages)关闭后是否删除镜像RemoveImages.ALL删除所有服务用到的镜像RemoveImages.LOCAL仅删除没有自定义 tag 的镜像ComposeContainer.javawithRemoveVolumes(boolean)关闭后是否删除卷默认true删除时追加-vwithLogConsumer(serviceName, consumer)为指定服务挂接日志输出消费者便于断言日志或转发到 SLF4JgetContainerByServiceName(serviceName)按服务名获取ContainerState。添加模块依赖Docker Compose 支持属于 Testcontainers 核心库的一部分无需额外引入独立模块。在pom.xml/build.gradle中添加如下依赖即可将版本号替换为你实际使用的 Testcontainers 发布版本 Gradlegroovy testImplementation org.testcontainers:testcontainers:{{latest_version}} Mavenxml dependency groupIdorg.testcontainers/groupId artifactIdtestcontainers/artifactId version{{latest_version}}/version scopetest/scope /dependency小结ComposeContainer让 Testcontainers 测试与已有的 Docker Compose 开发环境无缝衔接你只需提供一个docker-compose.yml通过withExposedService声明需要访问的服务与端口ambassador 容器会自动完成端口代理测试代码通过getServiceHost/getServicePort即可建立连接。等待策略、Local Compose 模式、文件拷贝粒度、私有仓库认证等能力则覆盖了从本地开发到 CI 落地的绝大多数场景。相关完整示例可在仓库的 ComposeContainerTest.java、ComposeContainerWithWaitStrategiesTest.java 与 ComposeContainerWithCopyFilesTest.java 中继续深入阅读。【免费下载链接】testcontainers-javaTestcontainers is a Java library that supports JUnit tests, providing lightweight, throwaway instances of common databases, Selenium web browsers, or anything else that can run in a Docker container.项目地址: https://gitcode.com/GitHub_Trending/te/testcontainers-java创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表