如何使用 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。以下是有关此主题的一些链接:
- Should you only mock types you own?
- TDD: Only mock types you own
- Don't Mock What You Don't Own
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。
我正在使用 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。以下是有关此主题的一些链接:
- Should you only mock types you own?
- TDD: Only mock types you own
- Don't Mock What You Don't Own
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。