{"id":136,"date":"2024-09-24T05:00:00","date_gmt":"2024-09-24T05:00:00","guid":{"rendered":"https:\/\/imcodinggenius.com\/?p=136"},"modified":"2024-09-24T05:00:00","modified_gmt":"2024-09-24T05:00:00","slug":"test-spring-boot-aws-s3-with-localstack-and-testcontainers","status":"publish","type":"post","link":"https:\/\/imcodinggenius.com\/?p=136","title":{"rendered":"Test Spring Boot AWS S3 with Localstack and Testcontainers"},"content":{"rendered":"<p>Welcome to the <strong>last article<\/strong> in a series dedicated to integrating a Spring Boot Kotlin app with <strong>AWS S3<\/strong> Object Storage, in which we will focus on <strong>integration testing with LocalStack and Testcontainers<\/strong>. And although we will focus on Object Storage, the approach we will use can be easily replicated with other AWS services.<\/p>\n<p>I can guarantee that you will benefit from this tutorial regardless of whether you saw previous articles about S3Client or S3Template, or not. But, I definitely encourage you to take a look at them, too: <\/p>\n<p><a href=\"https:\/\/codersee.com\/spring-boot-aws-s3-s3client-kotlin\/\">Spring Boot with AWS S3, S3Client and Kotlin<\/a><\/p>\n<p><a href=\"https:\/\/codersee.com\/spring-boot-with-kotlin-aws-s3-and-s3template\/\">Spring Boot with Kotlin, AWS S3, and S3Template<\/a><\/p>\n<h2 class=\"wp-block-heading\">Prerequisites<\/h2>\n<p>Before heading to the guide, I just wanted to emphasize that today we will be working with <strong>Testcontainers<\/strong>. And this means that we must have a <strong>supported Docker environment<\/strong>.<\/p>\n<p>So, if you do not have Docker configured on your local and want to follow this article, please check out <a href=\"https:\/\/java.testcontainers.org\/supported_docker_environment\/\" target=\"_blank\" rel=\"noopener\">their documentation<\/a>.<\/p>\n<p>Of course, you must have Java, IDE, and Spring Boot project too, but I believe this is quite obvious  <\/p>\n<h2 class=\"wp-block-heading\">Testcontainers and LocalStack<\/h2>\n<p>Lastly, I would like to say a few words about the Testcontainers and LocalStack, which in my opinion are a great way to test Spring Boot S3 integration (and other AWS integrations, too). <\/p>\n<h3 class=\"wp-block-heading\">Testcontainers<\/h3>\n<p><strong>Testcontainers <\/strong>is a library for providing throwaway, lightweight instances of Docker containers. They are an excellent approach whenever need to test behavior dependent on external services, like AWS, or some external databases. <\/p>\n<p>Long story short, instead of mocking, or manual set up of some test environment, we define test dependencies as code. Then, we can run our test code and disposable containers will be started and deleted after they finish. <\/p>\n<p>Let\u2019s take a look at the example from <a href=\"https:\/\/docs.spring.io\/spring-boot\/reference\/testing\/testcontainers.html\" target=\"_blank\" rel=\"noopener\">Spring Boot docs<\/a>: <\/p>\n<p>@Testcontainers<br \/>\n@SpringBootTest<br \/>\nclass MyIntegrationTests {<\/p>\n<p>  @Test<br \/>\n  fun myTest() {<br \/>\n    \/\/ &#8230;<br \/>\n  }<\/p>\n<p>  companion object {<br \/>\n    @Container<br \/>\n    @JvmStatic<br \/>\n    val neo4j = Neo4jContainer(&#171;neo4j:5&#187;);<br \/>\n  }<br \/>\n}<\/p>\n<p>The above code runs a Neo4j docker container before the tests. Of course, this is just an example, so most of the time, we will need to add some more config. <\/p>\n<p>Nevertheless, we can clearly see that this Testcontainers JUnit integration allows us to achieve our goal in an easy and neat manner. <\/p>\n<h3 class=\"wp-block-heading\">Localstack <\/h3>\n<p><strong>Localstack<\/strong>, on the other hand, is a cloud service emulator that runs in a single container. In other words, we can run AWS applications or Lambdas <strong>without connecting to the remote cloud provider<\/strong>. <\/p>\n<p>And thanks to the Testcontainers module for LocalStack, we can test various AWS integrations with just a few lines of code. <\/p>\n<p>Again, let\u2019s take a look at the example, but this time from the LocalStack documentation: <\/p>\n<p>DockerImageName localstackImage = DockerImageName.parse(&#171;localstack\/localstack:3.5.0&#187;);<\/p>\n<p>@Rule<br \/>\npublic LocalStackContainer localstack = new LocalStackContainer(localstackImage)<br \/>\n        .withServices(S3);<\/p>\n<p>You will find links to both documentation at the end of this article. But for now, let\u2019s not distract ourselves and focus on what we came here for <\/p>\n<h2 class=\"wp-block-heading\">Configure Project<\/h2>\n<p>If you are following my S3 series, or you already have a Spring Boot project, then those are the necessary dependencies for us today: <\/p>\n<p>testImplementation(&#171;org.springframework.boot:spring-boot-starter-webflux&#187;)<br \/>\ntestImplementation(&#171;org.testcontainers:localstack&#187;)<br \/>\ntestImplementation(&#171;org.springframework.boot:spring-boot-testcontainers&#187;)<\/p>\n<p>As we can see, apart from LocalStack and TestContainers, we must provide the Spring Boot Starter WebFlux. <\/p>\n<p>But why? <\/p>\n<p>Well, this is necessary to work with WebTestClient- a client we will use to test our web servers (REST endpoints). <\/p>\n<p>On the other hand, if you would like to set up a project from scratch, then please navigate to the <a href=\"https:\/\/start.spring.io\/\" target=\"_blank\" rel=\"noopener\">Spring Initializr<\/a> and select the following:<\/p>\n<div class=\"wp-block-image\">\n<\/div>\n<p>However, please keep in mind that LocalStack is not provided out of the box in Spring, so we must add it manually. <\/p>\n<p>Moreover, as we have chosen the Spring Web, the WebFlux dependency is not present, too:<\/p>\n<p>testImplementation(&#171;org.springframework.boot:spring-boot-starter-webflux&#187;)<br \/>\ntestImplementation(&#171;org.testcontainers:localstack&#187;)<\/p>\n<h2 class=\"wp-block-heading\">Testcontainers Singleton Approach<\/h2>\n<p>With all of that being done, let\u2019s head to the practice part. <\/p>\n<p>When working with Testcontainers, we can configure them in various ways: <\/p>\n<p>we can use the<strong> JUnit extension<\/strong> (Jupiter integration)- which allows us to use <em>@Testcontainers<\/em> and <em>@Container<\/em> annotations and makes JUnit responsible for the automatic startup and stop of containers in our tests.<\/p>\n<p>we can configure them <strong>manually <\/strong>in every test case, <\/p>\n<p>or, alternatively, we can use the <strong>singleton approach<\/strong>\u2013 in which we control containers\u2019 lifecycle <a href=\"https:\/\/java.testcontainers.org\/test_framework_integration\/manual_lifecycle_control\/\" target=\"_blank\" rel=\"noopener\">manually<\/a>. But, thanks to that we can easily reuse them across multiple test classes. <\/p>\n<p>Of course, these are not all approaches, and based on your needs you may want to configure Testcontainers differently. Nevertheless, in this tutorial, we will focus on the <strong>manual, reusable approach<\/strong>.<\/p>\n<h3 class=\"wp-block-heading\">Introduce Base Class<\/h3>\n<p>Firstly, let\u2019s introduce the LocalStackIntegrationTest:<\/p>\n<p>@SpringBootTest(webEnvironment = RANDOM_PORT)<br \/>\nclass LocalStackIntegrationTest { }<\/p>\n<p>As we can see, we mark our class with <em>@SpringBootTest<\/em> \u2013 annotation necessary to run our integration tests and inject the instance of WebTestClient later in our subclasses. <\/p>\n<h3 class=\"wp-block-heading\">Add Testcontainer<\/h3>\n<p>Following, let\u2019s take a look at how to instantiate a LocalStack container:<\/p>\n<p>companion object {<br \/>\n  val localStack: LocalStackContainer = LocalStackContainer(<br \/>\n    DockerImageName.parse(&#171;localstack\/localstack:3.7.2&#187;)<br \/>\n  )<br \/>\n}<\/p>\n<p>Right here, we create an instance of <strong>LocalStackContainer<\/strong> and we pass the name of a Docker image \u2013 localstack\/localstack:3.7.2\u2013 to its constructor. Alternatively, if we are working only with AWS S3 Buckets service, then we can use a dedicated image- localstack:s3-latest. But personally, I am not a big fan of the <strong>latest<\/strong> tag, which can easily break our code.<\/p>\n<p>Additionally, we put the LocalStackContainer instance in the <strong>companion object<\/strong>. Why? Because in the next steps, we will reference it in a function annotated with <em>@DynamicPropertySource<\/em>\u2013 and it must be static.<\/p>\n<p>Note related to <strong>JUnit extension<\/strong>: <\/p>\n<p>This is not the case here, as we want to take care of the container lifecycle manually, but, when using the Jupiter integration, containers declared as static fields will be shared between test methods. They will be started only once before any test method is executed and stopped after the last test method has executed. So, if in your case you pick the JUnit extension and don\u2019t want that to happen, then you must not put the localstack in the companion object. <\/p>\n<h3 class=\"wp-block-heading\">Control Testcontainer lifecycle<\/h3>\n<p>As we already know, with this approach <strong>we are responsible<\/strong> for the container lifecycle control. And although this may sound complicated, it basically means that without the extension we must start the container manually.<\/p>\n<p>So the companion object after the update will look, as follows:  <\/p>\n<p>companion object {<br \/>\n  val localStack: LocalStackContainer = LocalStackContainer(<br \/>\n    DockerImageName.parse(&#171;localstack\/localstack:3.7.2&#187;)<br \/>\n  ).apply {<br \/>\n    start()<br \/>\n  }<br \/>\n}<\/p>\n<p>Basically, we use the Kotlin scope function (you can learn more about it in my <a href=\"https:\/\/codersee.com\/the-complete-kotlin-course\/\">Kotlin course<\/a>) to invoke the start() function on the localStack instance. And as the name suggests, this function will start the container (and pull the image, if necessary).<\/p>\n<p>And basically,<strong> that is all we need to do here.<\/strong> With the above code, the container will be started when the base class is loaded and shared across all inheriting test classes. <\/p>\n<p>Of course, there is also the stop() function that we can invoke to kill and remove the container. <\/p>\n<p>Nevertheless, <strong>we do not have to do it<\/strong>. Why? Let\u2019s figure out. <\/p>\n<h3 class=\"wp-block-heading\">Ryuk<\/h3>\n<p>Ryuk is a kind of \u201cgarbage collector\u201d in Testcontainers. <\/p>\n<p>Whenever we run integration tests, Testcontianers core starts <strong>one more container<\/strong>: <\/p>\n<p>Long story short, this container is responsible for removing containers\/networks\/volumes created by our test cases. So, even if we do not clean the environment ourselves- for example with the stop() function- the Ryuk container will take care of that. <\/p>\n<h3 class=\"wp-block-heading\">Test Properties With DynamicPropertySource<\/h3>\n<p>With that done, we need to update our environment configuration. <\/p>\n<p>If we try to run our Spring Boot application at this point, our logic responsible for communication with Amazon S3 will try to reach the <strong>actual AWS instance<\/strong>. It will use the defaults, or make use of the things we configured in the application.yaml. <\/p>\n<p>And this is not what we want, right? Instead, we would like to connect to the Testcontainer LocalStack instance.  <\/p>\n<p>In some examples, you might have seen the usage of application properties files. Nevertheless, if we want to be more flexible and make use of containers started on random ports, then the <strong>@DynamicPropertySource<\/strong> is our best friend here: <\/p>\n<p>companion object {<br \/>\n  val localStack: LocalStackContainer = LocalStackContainer(<br \/>\n    DockerImageName.parse(&#171;localstack\/localstack:3.7.2&#187;)<br \/>\n  ).apply {<br \/>\n    start()<br \/>\n  }<\/p>\n<p>  @JvmStatic<br \/>\n  @DynamicPropertySource<br \/>\n  fun overrideProperties(registry: DynamicPropertyRegistry) {<br \/>\n    registry.add(&#171;spring.cloud.aws.region.static&#187;) { localStack.region }<br \/>\n    registry.add(&#171;spring.cloud.aws.credentials.access-key&#187;) { localStack.accessKey }<br \/>\n    registry.add(&#171;spring.cloud.aws.credentials.secret-key&#187;) { localStack.secretKey }<br \/>\n    registry.add(&#171;spring.cloud.aws.s3.endpoint&#187;) { localStack.getEndpointOverride(S3).toString() }<br \/>\n  }<\/p>\n<p>}<\/p>\n<p>Thanks to that annotation, we can dynamically provide values to our test environment <strong>based on the LocalStack instance<\/strong>.<\/p>\n<p>Of course, we must remember that methods annotated with <em>@DynamicPropertySource<\/em> <strong>must be static<\/strong>! And that\u2019s why we use it with the @JvmStatic annotation.<\/p>\n<h2 class=\"wp-block-heading\">Utilize LocalStack AWS CLI<\/h2>\n<p>At this point, we have our base class ready, but before we head to the tests, I would like to show you the <strong>LocalStack AWS CLI<\/strong> and why and how to use it.  <\/p>\n<p>As the first step, let\u2019s create the util package and add the LocalStackUtil.kt file:<\/p>\n<p>import org.testcontainers.containers.localstack.LocalStackContainer<\/p>\n<p>fun LocalStackContainer.createBucket(bucketName: String) {<br \/>\n  this.execInContainer(&#171;awslocal&#187;, &#171;s3api&#187;, &#171;create-bucket&#187;, &#171;&#8212;bucket&#187;, bucketName)<br \/>\n}<\/p>\n<p>fun LocalStackContainer.deleteBucket(bucketName: String) {<br \/>\n  this.execInContainer(&#171;awslocal&#187;, &#171;s3api&#187;, &#171;delete-bucket&#187;, &#171;&#8212;bucket&#187;, bucketName)<br \/>\n}<\/p>\n<p>fun LocalStackContainer.deleteObject(bucketName: String, objectName: String) {<br \/>\n  this.execInContainer(&#171;awslocal&#187;, &#171;s3api&#187;, &#171;delete-object&#187;, &#171;&#8212;bucket&#187;, bucketName, &#171;&#8212;key&#187;, objectName)<br \/>\n}<\/p>\n<p>As we can see, we introduced 3 helper extension functions that we will later use to create and delete buckets and objects. This way, we can <strong>easily clean up<\/strong> between the tests (we use the shared approach, right?). Moreover, it will <strong>simplify the setup process<\/strong> for each test case.<\/p>\n<p>The above code combines the execInContainer \u2013 which will run the passed command in our running LocalStack container, just like with the docker exec, and the awslocal\u2013 a LocalStack wrapper around the AWS CLI. So, if you\u2019ve ever been working with the AWS command line interface, then you will see that this is 1:1. <\/p>\n<p>Unfortunately, we must provide the command as a separate String value, because otherwise, we will end up with: <\/p>\n<p>OCI runtime exec failed: exec failed: container_linux.go:380: starting container process caused: exec: \u201cawslocal s3api create-bucket \u2013bucket bucket-1\u201d: executable file not found in $PATH: unknown<\/p>\n<h2 class=\"wp-block-heading\">Write Integration Test Cases<\/h2>\n<p>With all of that LocalStack preparation done (I know, quite a bunch of things to learn, but once you learn this, it will be a simple copy-paste), we can finally write some integration tests for our Spring Boot S3 integration.<\/p>\n<p>Firstly, let\u2019s create the controller package and put the BucketControllerIntegrationTest class:<\/p>\n<p>class BucketControllerIntegrationTest(<br \/>\n  @Autowired private val webTestClient: WebTestClient<br \/>\n) : LocalStackIntegrationTest() { }<\/p>\n<p>As we can see, no annotations are required. We simply extend the LocalStackIntegrationTest class and inject the WebTestClient.<\/p>\n<h3 class=\"wp-block-heading\">Test No Buckets Exist<\/h3>\n<p>Nextly, let\u2019s introduce our first test case. If we do not do anything, we expect that no buckets exist in our S3 instance: <\/p>\n<p>@Test<br \/>\nfun `Given no existing buckets When getting list of buckets Then return an empty array`() {<br \/>\n  val buckets = webTestClient<br \/>\n    .get().uri(&#171;\/buckets&#187;)<br \/>\n    .exchange()<br \/>\n    .expectStatus().isOk()<br \/>\n    .expectBody(object : ParameterizedTypeReference&lt;List&lt;String&gt;&gt;() {})<br \/>\n    .returnResult()<br \/>\n    .responseBody<\/p>\n<p>  assertNotNull(buckets)<br \/>\n  assertTrue(buckets.isEmpty())<br \/>\n}<\/p>\n<p>As mentioned before, we use the <em>WebTestClient<\/em> to make a GET HTTP request to the \/buckets endpoint. Then, we use a small hack with ParameterizedTypeReference\u2013 because the endpoint returns a list of Strings and we use Kotlin- and we obtain the response body. <\/p>\n<p>Lastly, we have plain assertions. We verify that the response body is not null and that our S3 bucket list is empty.<\/p>\n<h3 class=\"wp-block-heading\">Verify S3 Bucket Exists In LocalStack<\/h3>\n<p>Following, let\u2019s see our helper functions in action:<\/p>\n<p>@Test<br \/>\nfun `Given one existing bucket When getting list of buckets Then return an array with expected bucket name`() {<br \/>\n  val bucketName = &#171;bucket-1&#187;<br \/>\n  localStack.createBucket(bucketName)<\/p>\n<p>  val expectedJson = &#171;&#187;&#187;<br \/>\n    [ &#171;Bucket #1: $bucketName&#187; ]<br \/>\n  &#171;&#187;&#187;<\/p>\n<p>  webTestClient<br \/>\n    .get().uri(&#171;\/buckets&#187;)<br \/>\n    .exchange()<br \/>\n    .expectStatus().isOk()<br \/>\n    .expectBody()<br \/>\n    .json(expectedJson)<\/p>\n<p>  localStack.deleteBucket(bucketName)<br \/>\n}<\/p>\n<p>This time, we utilize the createBucket and make sure that the \/buckets endpoint returns the expected JSON. Please note that this is another way to assert the response body.   <\/p>\n<p>After all, we delete the existing bucket, so it won\u2019t affect other test cases.<\/p>\n<h3 class=\"wp-block-heading\">Assert Bucket Created Successfully<\/h3>\n<p>As the next step, let\u2019s take a look at how we can check if our endpoint responsible for creating new S3 buckets works. And I see two paths we can go here.<\/p>\n<p>The first one, using the execInContainer:<\/p>\n<p>@Test<br \/>\nfun `Given no existing buckets When creating bucket Then create bucket successfully`() {<br \/>\n  val bucketName = &#171;bucket-2&#187;<\/p>\n<p>  webTestClient<br \/>\n    .post().uri(&#171;\/buckets&#187;)<br \/>\n    .bodyValue(BucketRequest(bucketName = bucketName))<br \/>\n    .exchange()<br \/>\n    .expectStatus().isOk()<\/p>\n<p>  val execResult = localStack.execInContainer(&#171;awslocal&#187;, &#171;s3api&#187;, &#171;list-buckets&#187;).stdout<\/p>\n<p>  assertTrue(execResult.contains(bucketName))<br \/>\n  localStack.deleteBucket(bucketName)<br \/>\n}<\/p>\n<p>The important thing to mention here is that the execInContainer returns the ExecResult. And thanks to that, we can read additional info, like stdout, stderr, or exitCode.<\/p>\n<p>And thanks to the stdout, we can get this JSON to verify it contains particular bucket name (or even we could parse that to an object): <\/p>\n<p>{<br \/>\n  &#171;Buckets&#187;: [<br \/>\n    {<br \/>\n      &#171;Name&#187;: &#171;bucket-2&#187;,<br \/>\n      &#171;CreationDate&#187;: &#171;2024-09-19T05:28:42.000Z&#187;<br \/>\n    }<br \/>\n  ],<br \/>\n  &#171;Owner&#187;: {<br \/>\n    &#171;DisplayName&#187;: &#171;webfile&#187;,<br \/>\n    &#171;ID&#187;: &#171;75aa57f09aa0c8caeab4f8c24e99d10f8e7faeebf76c078efc7c6caea54ba06a&#187;<br \/>\n  }<br \/>\n}<\/p>\n<p>Alternatively, we can use the \/buckets endpoint once again, too:<\/p>\n<p>@Test<br \/>\nfun `Given no existing buckets When creating bucket Then create bucket successfully`() {<br \/>\n  val bucketName = &#171;bucket-2&#187;<\/p>\n<p>  webTestClient<br \/>\n    .post().uri(&#171;\/buckets&#187;)<br \/>\n    .bodyValue(BucketRequest(bucketName = bucketName))<br \/>\n    .exchange()<br \/>\n    .expectStatus().isOk()<\/p>\n<p>  val expectedJson = &#171;&#187;&#187;<br \/>\n    [ &#171;Bucket #1: $bucketName&#187; ]<br \/>\n  &#171;&#187;&#187;<br \/>\n  webTestClient<br \/>\n    .get().uri(&#171;\/buckets&#187;)<br \/>\n    .exchange()<br \/>\n    .expectStatus().isOk()<br \/>\n    .expectBody()<br \/>\n    .json(expectedJson)<\/p>\n<p>  localStack.deleteBucket(bucketName)<br \/>\n}<\/p>\n<h3 class=\"wp-block-heading\">Test Remaining Cases<\/h3>\n<p>The remaining cases of our integration test use a more or less similar approach, so I will simply copy-paste them here so that you can analyze them. <\/p>\n<p>At this point, I am pretty sure you understand the general idea behind what I understand by testing of Spring Boot S3 integration with LocalStack, so I don\u2019t see the need for explaining them one- by one:<\/p>\n<p>@Test<br \/>\nfun `Given no objects existing in the bucket When getting objects of a bucket Then return an empty array`() {<br \/>\n  val bucketName = &#171;bucket-3&#187;<br \/>\n  localStack.createBucket(bucketName)<\/p>\n<p>  val objects = webTestClient<br \/>\n    .get().uri(&#171;\/buckets\/$bucketName\/objects&#187;)<br \/>\n    .exchange()<br \/>\n    .expectStatus().isOk()<br \/>\n    .expectBody(object : ParameterizedTypeReference&lt;List&lt;String&gt;&gt;() {})<br \/>\n    .returnResult()<br \/>\n    .responseBody<\/p>\n<p>  assertNotNull(objects)<br \/>\n  assertTrue(objects.isEmpty())<\/p>\n<p>  localStack.deleteBucket(bucketName)<br \/>\n}<\/p>\n<p>@Test<br \/>\nfun `Given no objects When creating example object Then return created object`() {<br \/>\n  val bucketName = &#171;bucket-4&#187;<br \/>\n  val objectName = &#171;example.json&#187;<br \/>\n  localStack.createBucket(bucketName)<\/p>\n<p>  val expectedJson = &#171;&#187;&#187;<br \/>\n    {<br \/>\n      &#171;id&#187;: &#171;123&#187;,<br \/>\n      &#171;name&#187;: &#171;Some name&#187;<br \/>\n    }<br \/>\n  &#171;&#187;&#187;<\/p>\n<p>  webTestClient<br \/>\n    .post().uri(&#171;\/buckets\/$bucketName\/objects&#187;)<br \/>\n    .exchange()<br \/>\n    .expectStatus().isOk()<br \/>\n    .expectBody()<br \/>\n    .json(expectedJson)<\/p>\n<p>  localStack.deleteObject(bucketName, objectName)<br \/>\n  localStack.deleteBucket(bucketName)<br \/>\n}<\/p>\n<p>@Test<br \/>\nfun `Given created object When getting list of objects Then return array with one object`() {<br \/>\n  val bucketName = &#171;bucket-5&#187;<br \/>\n  val objectName = &#171;example.json&#187;<br \/>\n  localStack.createBucket(bucketName)<\/p>\n<p>  val expectedJson = &#171;&#187;&#187;<br \/>\n    [ $objectName ]<br \/>\n  &#171;&#187;&#187;<\/p>\n<p>  webTestClient<br \/>\n    .post().uri(&#171;\/buckets\/$bucketName\/objects&#187;)<br \/>\n    .exchange()<br \/>\n    .expectStatus().isOk()<\/p>\n<p>  webTestClient<br \/>\n    .get().uri(&#171;\/buckets\/$bucketName\/objects&#187;)<br \/>\n    .exchange()<br \/>\n    .expectStatus().isOk()<br \/>\n    .expectBody()<br \/>\n    .json(expectedJson)<\/p>\n<p>  localStack.deleteObject(bucketName, objectName)<br \/>\n  localStack.deleteBucket(bucketName)<br \/>\n}<\/p>\n<p>@Test<br \/>\nfun `Given existing object When getting object by key Then return object content`() {<br \/>\n  val bucketName = &#171;bucket-6&#187;<br \/>\n  val objectName = &#171;example.json&#187;<br \/>\n  localStack.createBucket(bucketName)<\/p>\n<p>  val expected = &#171;&#187;&#187;<br \/>\n    {<br \/>\n      &#171;id&#187;: &#171;123&#187;,<br \/>\n      &#171;name&#187;: &#171;Some name&#187;<br \/>\n    }<br \/>\n  &#171;&#187;&#187;<\/p>\n<p>  webTestClient<br \/>\n    .post().uri(&#171;\/buckets\/$bucketName\/objects&#187;)<br \/>\n    .exchange()<\/p>\n<p>  webTestClient<br \/>\n    .get().uri(&#171;\/buckets\/$bucketName\/objects\/$objectName&#187;)<br \/>\n    .exchange()<br \/>\n    .expectStatus().isOk()<br \/>\n    .expectBody()<br \/>\n    .json(expected)<\/p>\n<p>  localStack.deleteObject(bucketName, objectName)<br \/>\n  localStack.deleteBucket(bucketName)<br \/>\n}<\/p>\n<p>@Test<br \/>\nfun `Given existing bucket with object When deleting bucket Then bucket is removed`() {<br \/>\n  val bucketName = &#171;bucket-7&#187;<br \/>\n  localStack.createBucket(bucketName)<\/p>\n<p>  webTestClient<br \/>\n    .post().uri(&#171;\/buckets\/$bucketName\/objects&#187;)<br \/>\n    .exchange()<br \/>\n    .expectStatus().isOk()<\/p>\n<p>  webTestClient<br \/>\n    .delete().uri(&#171;\/buckets\/$bucketName&#187;)<br \/>\n    .exchange()<br \/>\n    .expectStatus().isOk()<\/p>\n<p>  val buckets = webTestClient<br \/>\n    .get().uri(&#171;\/buckets&#187;)<br \/>\n    .exchange()<br \/>\n    .expectStatus().isOk()<br \/>\n    .expectBody(object : ParameterizedTypeReference&lt;List&lt;String&gt;&gt;() {})<br \/>\n    .returnResult()<br \/>\n    .responseBody<\/p>\n<p>  assertNotNull(buckets)<br \/>\n  assertTrue(buckets.isEmpty())<br \/>\n}<\/p>\n<h2 class=\"wp-block-heading\">Summary<\/h2>\n<p>And that is all for this tutorial, in which we learned how to implement <strong>integration tests <\/strong>for <strong>Spring Boot AWS S3 <\/strong>integration with <strong>LocalStack <\/strong>and <strong>Testcontainers.<\/strong><\/p>\n<p>I hope you enjoyed it and for the source code, please visit <a href=\"https:\/\/github.com\/codersee-blog\/spring-boot-3-kotlin-aws-s3-localstack-testcontainers\" target=\"_blank\" rel=\"noopener\">this GitHub repository<\/a>. <\/p>\n<p>Have a great day and see you in the next articles!  <\/p>\n<p>The post <a href=\"https:\/\/codersee.com\/test-spring-boot-aws-s3-with-localstack-and-testcontainers\/\">Test Spring Boot AWS S3 with Localstack and Testcontainers<\/a> appeared first on <a href=\"https:\/\/codersee.com\/\">Codersee | Kotlin, Ktor, Spring<\/a>.<\/p>","protected":false},"excerpt":{"rendered":"<p>Welcome to the last article in a series dedicated to integrating a Spring Boot Kotlin app with AWS S3 Object Storage, in which we will focus on integration testing with LocalStack and Testcontainers. And although we will focus on Object Storage, the approach we will use can be easily replicated &#8230; <\/p>\n<div><a class=\"more-link bs-book_btn\" href=\"https:\/\/imcodinggenius.com\/?p=136\">Read More<\/a><\/div>\n","protected":false},"author":0,"featured_media":137,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[1],"tags":[],"class_list":["post-136","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-news"],"_links":{"self":[{"href":"https:\/\/imcodinggenius.com\/index.php?rest_route=\/wp\/v2\/posts\/136"}],"collection":[{"href":"https:\/\/imcodinggenius.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/imcodinggenius.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"replies":[{"embeddable":true,"href":"https:\/\/imcodinggenius.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=136"}],"version-history":[{"count":0,"href":"https:\/\/imcodinggenius.com\/index.php?rest_route=\/wp\/v2\/posts\/136\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/imcodinggenius.com\/index.php?rest_route=\/wp\/v2\/media\/137"}],"wp:attachment":[{"href":"https:\/\/imcodinggenius.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=136"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/imcodinggenius.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=136"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/imcodinggenius.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=136"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}