I have a function below which uploads data to s3
val s3Client = S3Client.create()
data class S3Object(
val bucket: String,
val key: String,
val contents: String,
val contentType: String
)
fun putS3Object(s3Object: S3Object){
val putObjectRequest = PutObjectRequest.builder()
.bucket(s3Object.bucket)
.key(s3Object.key.toString())
.contentType(s3Object.contentType)
.build()
s3Client.putObject(putObjectRequest, RequestBody.fromString(s3Object.contents))
}
I am mocking it as below:
every { S3Client.create()} returns s3Client
val s3Object = S3Object("test-bucket", "/test/key", "test-event", "application/json; charset=UTF-8")
val putRequest = PutObjectRequest.builder()
.bucket(s3Object.bucket)
.contentType(s3Object.contentType)
.key(s3Object.key)
.build()
val putObjectResponse = PutObjectResponse
.builder()
.build()
every{ s3Client.putObject(putObjectRequest, RequestBody.fromString(s3Object.contents))} answers {putObjectResponse}
I am creating the s3 PutObjectRequest and PutObjectResponse as part of the mock. However when I try to run my test I get the following mockK exception:
io.mockk.MockKException: no answer found for: S3Client(#4).putObject(PutObjectRequest(Bucket=test-bucket, ContentType=application/json; charset=UTF-8, Key="/test/key"), software.amazon.awssdk.core.sync.RequestBody@49322d04)
Firstly, is the call to s3Client.putObject(..)
what you are trying to test.. that the data that gets sent to that method is correct? I will assume so.
It'll be easier if you separate the Class under test versus the test class. So in the main code line you should have something like this:
import software.amazon.awssdk.core.sync.RequestBody
import software.amazon.awssdk.services.s3.S3Client
import software.amazon.awssdk.services.s3.model.PutObjectRequest
data class S3Object(
val bucket: String,
val key: String,
val contents: String,
val contentType: String,
)
class S3Service(
// always in inject your dependencies then we can override then you override them in a test
private val s3Client: S3Client = S3Client.create()
) {
fun putS3Object(s3Object: S3Object) {
val putObjectRequest = PutObjectRequest.builder()
.bucket(s3Object.bucket)
.key(s3Object.key.toString())
.contentType(s3Object.contentType)
.build()
s3Client.putObject(putObjectRequest, RequestBody.fromString(s3Object.contents))
}
}
And now the test class:
import io.kotest.matchers.shouldBe
import io.mockk.clearAllMocks
import io.mockk.every
import io.mockk.mockk
import io.mockk.slot
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import software.amazon.awssdk.core.sync.RequestBody
import software.amazon.awssdk.services.s3.S3Client
import software.amazon.awssdk.services.s3.model.PutObjectRequest
import software.amazon.awssdk.services.s3.model.PutObjectResponse
class S3ServiceTest {
// you need a mock the client since this is what you want to mock to check the invocation
private val s3Client = mockk<S3Client>()
private lateinit var s3Service: S3Service
@BeforeEach
fun beforeEach() {
// its supposedly marginally more efficient to define your mocks once and clear then not instantiate each time
clearAllMocks()
// inject the mock s3Client so you do not get the `S3Client.create()` one
s3Service = S3Service(s3Client)
}
@Test
fun `putObject call correctly formed`() {
val s3Object = S3Object(
"test-bucket",
"/test/key",
"test-event",
"application/json; charset=UTF-8",
)
every { s3Client.putObject(any<PutObjectRequest>(), any<RequestBody>()) } returns mockk()
s3Service.putS3Object(s3Object)
}
Now this is incomplete - it doesn't assert anything. But the point here is that in the line every { s3Client.putObject(any<PutObjectRequest>(),...
I am setting up that Mockk will capture ANY input to the putObject()
. It it wrong to instruct Mockk to expect a particular input in this case... rather you want to see what that in put is and then test it (see later).
I needed to use <PutObjectRequest>
because there are several s3Client.putObject()
so mockk needs to know which you are listening to.
And another change was the end of the line. You put this:
} answers {putObjectResponse}
this is ok, but two things
putObject()
returns? You might do if this is part of a more complex test. Use returns mock()
if you don't carereturns OBJECT
or answers { LAMBDA }
. The second case is useful when you don't know what to return under after later, in this case you output is static is it is more efficient to use returns OBJECT
...In the case of of needing a proper return do this:
val putObjectResponse = PutObjectResponse
.builder()
.build()
every { s3Client.putObject(any<PutObjectRequest>(), any<RequestBody>()) } returns putObjectResponse
Now how can we actually see if the call to putObject
was correctly made? For this you need a different feature called a slot
which you use like this:
@Test
fun `putObject call correctly formed2`() {
val s3Object = S3Object(
"test-bucket",
"/test/key",
"test-event",
"application/json; charset=UTF-8",
)
every { s3Client.putObject(any<PutObjectRequest>(), any<RequestBody>()) } returns mockk()
val pubObjectRequestSlot = slot<PutObjectRequest>()
every { s3Client.putObject(capture(pubObjectRequestSlot), any<RequestBody>()) } returns mockk()
s3Service.putS3Object(s3Object)
pubObjectRequestSlot.captured.bucket() shouldBe "test-bucket"
pubObjectRequestSlot.captured.key() shouldBe "/test/key"
// more assertions here...
// and of course you can capture the RequestBody argument too
}