如何使用 Java 对 AWS S3 SDK 的 getObject 方法进行单元测试?

How can I unit test the getObject method of the AWS S3 SDK using Java?

我正在使用 Java 并使用 AWS SDK 与 S3 进行交互。 我有以下方法,我想对其进行单元测试

private final S3Client s3Client;
...
...
public byte[] download(String key) throws IOException {
    GetObjectRequest getObjectRequest = GetObjectRequest.builder()
            .bucket("myBucket")
            .key(key)
            .build();
    return s3Client.getObject(getObjectRequest).readAllBytes();
}

为此,我使用了 JUnit 5 和 Mockito。 问题是我不知道如何模拟

的结果
s3Client.getObject(getObjectRequest) 

因为 return 类型

ResponseInputStream<GetObjectResponse> 

是最后一个 class。

有什么想法或建议吗? 谢谢

我能够通过使用 Objenesis 的 GroovyMock 在 Spock 中实现这一点。我知道这不是原始发布者的堆栈,但在我的搜索中出现了这个,所以我想我会在这里回复以防部分内容对其他人有帮助。

要测试的 S3Repo

import software.amazon.awssdk.services.s3.S3Client
import software.amazon.awssdk.services.s3.model.GetObjectRequest

class S3Repo {
    S3Client s3
    String bucket

    def getS3ObjectText(String key) {
        def response
        try {
            response = s3.getObject(GetObjectRequest
                    .builder().bucket(bucket).key(key).build() as GetObjectRequest)
            return response.text
        } finally {
            if (response) response.close()
        }
    }
}

史波克测试

import software.amazon.awssdk.core.ResponseInputStream
import software.amazon.awssdk.services.s3.S3Client
import software.amazon.awssdk.services.s3.model.GetObjectRequest
import spock.lang.Specification

class S3RepoTest extends Specification {

    def "get object text"() {
        given:
        def response = GroovyMock(ResponseInputStream)
        def s3Text = 'this would be in file stored in s3'
        def key = 'my-file.txt'
        def s3 = Mock(S3Client)
        def bucket = 'mock-bucket'
        def repo = new S3Repo(s3: s3, bucket: bucket)

        when:
        def text = repo.getS3ObjectText(key)

        then:
        1 * s3.getObject(_) >> { args ->
            def req = args.first() as GetObjectRequest
            assert req.bucket() == bucket
            assert req.key() == key
            return response
        }
        1 * response.text >> s3Text

        and:
        text == s3Text
    }
}

我认为这里的关键部分是需要 Objenesis 的 GroovyMock。您当然可以使用 Groovy 测试您的 Java 代码,并且您可能会在 JUnit 中使用 GroovyMock

问题已解决。 在 Maven 项目中,您可以在文件夹 "src/test/resources/mockito-extensions".

中添加一个名为 "org.mockito.plugins.MockMaker" 的文件

在文件中,添加不带引号的 "mock-maker-inline"。

从现在开始,Mockito 也将能够模拟 final 类。

我在一个 gradle 项目中做到了这一点。 添加以下依赖 -

    testCompile group: 'net.bytebuddy', name: 'byte-buddy-agent', version: '1.9.10'
    testCompile group: 'org.mockito', name: 'mockito-inline', version: '2.7.21'
    compile group: 'net.bytebuddy', name: 'byte-buddy', version: '1.9.10'

bytebuddy 依赖项是 mockito-core 支持 mockito-inline 依赖项所需的编译依赖项。 (参考 - https://howtodoinjava.com/mockito/plugin-mockmaker-error/ https://mvnrepository.com/artifact/org.mockito/mockito-core/2.27.2(在这里查看编译依赖))

之后能够模拟 getObject 方法。

如果有人仍在寻找不同的解决方案,我就是这样做的:
这是需要mock的代码:

InputStream objectStream =
    this.s3Client.getObject(
        GetObjectRequest.builder().bucket(bucket).key(key).build(),
        ResponseTransformer.toInputStream());

这是模拟它的方法:

S3Client s3Client = Mockito.mock(S3Client.class);
String bucket = "bucket";
String key = "key";
InputStream objectStream = getFakeInputStream();
when(s3Client.getObject(
        Mockito.any(GetObjectRequest.class),
        ArgumentMatchers
            .<ResponseTransformer<GetObjectResponse, ResponseInputStream<GetObjectResponse>>>
                any()))
    .then(
        invocation -> {
          GetObjectRequest getObjectRequest = invocation.getArgument(0);
          assertEquals(bucket, getObjectRequest.bucket());
          assertEquals(key, getObjectRequest.key());

          return new ResponseInputStream<>(
              GetObjectResponse.builder().build(), AbortableInputStream.create(objectStream));
        });

我在这里补充了所有的好答案。

有一种观点认为,开发人员不应模拟他们不拥有的类型和 API。以下是有关此主题的一些链接:

TLDR:通过模拟,您可以在测试中引入一些关于事物应该如何工作的假设。因此,您的测试不再是真正的黑盒,而它们应该是。你的假设可能是错误的。或者您模拟的 API 可能会改变,使您的模拟和测试漂移和陈旧。这可能会导致通过测试的损坏代码。

需要示例吗?考虑这段代码:

@Test
void test() throws Exception {
    GetObjectRequest request = GetObjectRequest
        .builder()
        .bucket("bucket")
        .key("file.json")
        .range("bytes=0-0,-1")
        .build();
    ResponseInputStream<GetObjectResponse> response = client.getObject(request);
    byte[] bytes = response.readAllBytes();

    Assertions.assertEquals("{}", new String(bytes));
}

人们希望它能够成功读取 JSON 文件的第一个和最后一个字节。这将给出 {} (假设没有额外的空格或换行符)。所以她嘲笑 getObject 和 returns 那些字节。

现实中发生了什么?她将获得整个 JSON,而不仅仅是第一个和最后一个字节,因为

Amazon S3 doesn't support retrieving multiple ranges of data per GET request.

这就是它在现实世界中的工作方式:没有异常,没有警告,只返回全部内容。

如果你只留下一个范围,比方说只留下第一个字节,它会起作用:

@Test
void test() throws Exception {
    GetObjectRequest request = GetObjectRequest
        .builder()
        .bucket("bucket")
        .key("file.json")
        .range("bytes=0-0")
        .build();
    ResponseInputStream<GetObjectResponse> response = client.getObject(request);
    byte[] bytes = response.readAllBytes();

    Assertions.assertEquals("{", new String(bytes));
}

虽然这个例子完全是虚构的,而且这样的情况很少见,但我希望你已经明白了。

解决方案?

集成测试!是的,这是一个更高的级别,编写它们需要更多的努力,但是它们可以捕获像上面那样的错误。

所以,下次考虑编写集成测试并使用真实的东西而不是模拟。对于某些技术,您甚至可以找到官方的轻量级实现。喜欢 DynamoDB Local. Or use a mock that is closer to the original API. Like a LocalStack.

如果您是 Java/Kotlin 和 JUnit 5 的幸运用户,我建议您使用 aws-junit5, a set of JUnit 5 extensions for AWS. I am it's author. These extensions can be used to inject clients for AWS services provided by tools like LocalStack or any other AWS-compatible API (including the real AWS, of course). Both AWS Java SDK v 2.x and v 1.x are supported. You aws-junit5 to inject clients for S3, DynamoDB, Kinesis, SES, SNS and SQS. Read more in the user guide