有一天,我遇到了从PostgreSQL下载一个相对较大的二进制数据文件的问题。存储和获取此类数据有几个限制(所有限制都可以在官方文档中找到)。为了解决这个问题,建议找到更合适的数据存储。
出于某些内部原因,为此目的选择了众所周知的 Amazon S3 存储桶。该选择影响了项目的单元测试基础。仍然无法继续使用 HSQL 或 H2 等轻量级数据库来实现测试。这是我们将在本文中尝试解决的一个关键问题。
对象存储构建
保持单元测试活跃的一种可能的解决方案是实现一些模拟对象存储,与 S3 存储桶客户端完全兼容,另一方面,我们可以使用这种类型的现有对象存储。MinIO 是一个非常简单但高性能的对象存储的一个很好的例子,它同时与 Amazon S3 兼容(至少在文档中是这样写的)。
为了将 MinIO 集成到我们的单元测试中,我们将使用一个用 Java 编写的强大的 Testcontainers 库。Testcontainers 是一个特殊的库,它支持 JUnit 测试,并提供通用数据库、Selenium Web 浏览器以及可以在 Docker 容器中运行的任何其他内容的轻量级一次性实例。要开始使用这个惊人的库,只需要拥有 Docker 并将以下依赖项添加到我们的pom.xml:
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers</artifactId>
<version>1.12.3</version>
<scope>test</scope>
</dependency>
不幸的是,没有适合我们目标的容器,但该库提供了所有必要的工具,可以自己轻松创建它。幸运的是,DockerHub 上有一个 MinIO 的官方 docker 镜像。
要创建自己的 MinIO 容器,必须使用自定义数据进行扩展: (MonIO 文档建议使用 9000 端口)、 (映像名称)、 (映像版本)。GenericContainerDEFAULT_PORTDEFAULT_IMAGEDEFAULT_TAG
注意标签分配!在我们的示例中,它使用“edge”标签来支持上次部署的 MinIO 版本,但大多数时候最好不时修复标签并手动更新它,以避免不可预测的测试崩溃。还强烈建议提供凭据(访问密钥、私有密钥)来控制对容器的访问。下面是自定义 MinIO 容器的实现示例:
public class MinioContainer extends GenericContainer<MinioContainer> {
private static final int DEFAULT_PORT = 9000;
private static final String DEFAULT_IMAGE = "minio/minio";
private static final String DEFAULT_TAG = "edge";
private static final String MINIO_ACCESS_KEY = "MINIO_ACCESS_KEY";
private static final String MINIO_SECRET_KEY = "MINIO_SECRET_KEY";
private static final String DEFAULT_STORAGE_DIRECTORY = "/data";
private static final String HEALTH_ENDPOINT = "/minio/health/ready";
public MinioContainer(CredentialsProvider credentials) {
this(DEFAULT_IMAGE + ":" + DEFAULT_TAG, credentials);
}
public MinioContainer(String image, CredentialsProvider credentials) {
super(image == null ? DEFAULT_IMAGE + ":" + DEFAULT_TAG : image);
withNetworkAliases("minio-" + Base58.randomString(6));
addExposedPort(DEFAULT_PORT);
if (credentials != null) {
withEnv(MINIO_ACCESS_KEY, credentials.getAccessKey());
withEnv(MINIO_SECRET_KEY, credentials.getSecretKey());
}
withCommand("server", DEFAULT_STORAGE_DIRECTORY);
setWaitStrategy(new HttpWaitStrategy()
.forPort(DEFAULT_PORT)
.forPath(HEALTH_ENDPOINT)
.withStartupTimeout(Duration.ofMinutes(2)));
}
public String getHostAddress() {
return getContainerIpAddress() + ":" + getMappedPort(DEFAULT_PORT);
}
public static class CredentialsProvider {
private String accessKey;
private String secretKey;
public CredentialsProvider(String accessKey, String secretKey) {
this.accessKey = accessKey;
this.secretKey = secretKey;
}
// getters
}
}
测试
由于我们有一个合适的测试容器,可以用作 Amazon S3 存储桶对象存储的提供程序,因此是时候展示一个简单的 JUnit 测试示例了。当然,首先我们需要配置 S3 客户端以与我们的容器进行交互。在我们的例子中,我们使用原始的 AmazonS3Client。因此,为了实现我们的单元测试,我们需要添加一个额外的依赖项。
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-java-sdk-s3</artifactId>
<version>1.11.60</version>
</dependency>
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-java-sdk</artifactId>
<version>1.11.60</version>
</dependency>
以下是创建具有指定名称的 S3 存储桶的普通测试:
public class MinioContainerTest {
private static final String ACCESS_KEY = "accessKey";
private static final String SECRET_KEY = "secretKey";
private static final String BUCKET = "bucket";
private AmazonS3Client client = null;
@After
public void shutDown() {
if (client != null) {
client.shutdown();
client = null;
}
}
@Test
public void testCreateBucket() {
try (MinioContainer container = new MinioContainer(
new MinioContainer.CredentialsProvider(ACCESS_KEY, SECRET_KEY))) {
container.start();
client = getClient(container);
Bucket bucket = client.createBucket(BUCKET);
assertNotNull(bucket);
assertEquals(BUCKET, bucket.getName());
List<Bucket> buckets = client.listBuckets();
assertNotNull(buckets);
assertEquals(1, buckets.size());
assertTrue(buckets.stream()
.map(Bucket::getName)
.collect(Collectors.toList())
.contains(BUCKET));
}
}
private AmazonS3Client getClient(MinioContainer container) {
S3ClientOptions clientOptions = S3ClientOptions
.builder()
.setPathStyleAccess(true)
.build();
client = new AmazonS3Client(new AWSStaticCredentialsProvider(
new BasicAWSCredentials(ACCESS_KEY, SECRET_KEY)));
client.setEndpoint("http://" + container.getHostAddress());
client.setS3ClientOptions(clientOptions);
return client;
}
}