├── .sdkmanrc
├── LICENSE-docs
├── assets
└── tcd-workshop-app.png
├── src
├── test
│ └── resources
│ │ ├── P101.jpg
│ │ └── logback-test.xml
└── main
│ ├── java
│ └── com
│ │ └── testcontainers
│ │ └── catalog
│ │ ├── clients
│ │ └── inventory
│ │ │ ├── ProductInventory.java
│ │ │ ├── InventoryServiceClient.java
│ │ │ └── RestClientConfig.java
│ │ ├── domain
│ │ ├── models
│ │ │ ├── ProductImageUploadedEvent.java
│ │ │ ├── Product.java
│ │ │ └── CreateProductRequest.java
│ │ ├── FileStorageService.java
│ │ ├── ProductNotFoundException.java
│ │ ├── ProductService.java
│ │ └── internal
│ │ │ ├── ProductRepository.java
│ │ │ ├── ProductEventPublisher.java
│ │ │ ├── S3FileStorageService.java
│ │ │ ├── ProductEntity.java
│ │ │ └── DefaultProductService.java
│ │ ├── Application.java
│ │ ├── ApplicationProperties.java
│ │ ├── config
│ │ └── GlobalExceptionHandler.java
│ │ ├── events
│ │ └── ProductEventListener.java
│ │ └── api
│ │ └── ProductController.java
│ └── resources
│ ├── db
│ └── migration
│ │ └── V1__catalog_tables.sql
│ ├── application.properties
│ └── catalog-openapi.yaml
├── .mvn
├── wrapper
│ ├── maven-wrapper.jar
│ └── maven-wrapper.properties
└── jvm.config
├── .github
└── workflows
│ └── maven.yml
├── .gitignore
├── LICENSE
├── README.md
├── step-2-exploring-the-app.md
├── step-4-connect-to-services.md
├── step-1-getting-started.md
├── mvnw.cmd
├── pom.xml
├── step-5-write-tests.md
├── mvnw
└── step-3-local-development-environment.md
/.sdkmanrc:
--------------------------------------------------------------------------------
1 | java=17.0.8-tem
2 | maven=3.9.5
3 |
--------------------------------------------------------------------------------
/LICENSE-docs:
--------------------------------------------------------------------------------
1 | Except where otherwise noted, this work is licensed under https://creativecommons.org/licenses/by-nc-nd/4.0
--------------------------------------------------------------------------------
/assets/tcd-workshop-app.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/testcontainers/java-local-development-workshop/HEAD/assets/tcd-workshop-app.png
--------------------------------------------------------------------------------
/src/test/resources/P101.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/testcontainers/java-local-development-workshop/HEAD/src/test/resources/P101.jpg
--------------------------------------------------------------------------------
/.mvn/wrapper/maven-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/testcontainers/java-local-development-workshop/HEAD/.mvn/wrapper/maven-wrapper.jar
--------------------------------------------------------------------------------
/src/main/java/com/testcontainers/catalog/clients/inventory/ProductInventory.java:
--------------------------------------------------------------------------------
1 | package com.testcontainers.catalog.clients.inventory;
2 |
3 | public record ProductInventory(String code, int quantity) {}
4 |
--------------------------------------------------------------------------------
/src/main/java/com/testcontainers/catalog/domain/models/ProductImageUploadedEvent.java:
--------------------------------------------------------------------------------
1 | package com.testcontainers.catalog.domain.models;
2 |
3 | public record ProductImageUploadedEvent(String code, String image) {}
4 |
--------------------------------------------------------------------------------
/.mvn/wrapper/maven-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.5/apache-maven-3.9.5-bin.zip
2 | wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar
3 |
--------------------------------------------------------------------------------
/src/main/java/com/testcontainers/catalog/domain/models/Product.java:
--------------------------------------------------------------------------------
1 | package com.testcontainers.catalog.domain.models;
2 |
3 | import java.math.BigDecimal;
4 |
5 | public record Product(
6 | Long id, String code, String name, String description, String imageUrl, BigDecimal price, boolean available) {}
7 |
--------------------------------------------------------------------------------
/.mvn/jvm.config:
--------------------------------------------------------------------------------
1 | --add-exports jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED
2 |
--------------------------------------------------------------------------------
/src/main/java/com/testcontainers/catalog/domain/FileStorageService.java:
--------------------------------------------------------------------------------
1 | package com.testcontainers.catalog.domain;
2 |
3 | import java.io.InputStream;
4 |
5 | public interface FileStorageService {
6 | void createBucket(String bucketName);
7 |
8 | void upload(String filename, InputStream inputStream);
9 |
10 | String getPreSignedURL(String filename);
11 | }
12 |
--------------------------------------------------------------------------------
/src/main/java/com/testcontainers/catalog/clients/inventory/InventoryServiceClient.java:
--------------------------------------------------------------------------------
1 | package com.testcontainers.catalog.clients.inventory;
2 |
3 | import org.springframework.web.bind.annotation.PathVariable;
4 | import org.springframework.web.service.annotation.GetExchange;
5 |
6 | public interface InventoryServiceClient {
7 |
8 | @GetExchange("/api/inventory/{code}")
9 | ProductInventory getInventory(@PathVariable String code);
10 | }
11 |
--------------------------------------------------------------------------------
/src/main/java/com/testcontainers/catalog/domain/ProductNotFoundException.java:
--------------------------------------------------------------------------------
1 | package com.testcontainers.catalog.domain;
2 |
3 | public class ProductNotFoundException extends RuntimeException {
4 | public ProductNotFoundException(String message) {
5 | super(message);
6 | }
7 |
8 | public static ProductNotFoundException withCode(String code) {
9 | return new ProductNotFoundException("Product with code " + code + " not found");
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/src/main/java/com/testcontainers/catalog/domain/models/CreateProductRequest.java:
--------------------------------------------------------------------------------
1 | package com.testcontainers.catalog.domain.models;
2 |
3 | import jakarta.validation.constraints.NotEmpty;
4 | import jakarta.validation.constraints.NotNull;
5 | import jakarta.validation.constraints.Positive;
6 | import java.math.BigDecimal;
7 |
8 | public record CreateProductRequest(
9 | @NotEmpty String code, @NotEmpty String name, String description, @NotNull @Positive BigDecimal price) {}
10 |
--------------------------------------------------------------------------------
/.github/workflows/maven.yml:
--------------------------------------------------------------------------------
1 | name: Build
2 |
3 | on:
4 | push:
5 | branches:
6 | - '**'
7 |
8 | jobs:
9 | test:
10 | name: Build
11 | runs-on: ubuntu-latest
12 | steps:
13 | - uses: actions/checkout@v4
14 |
15 | - name: Setup Java
16 | uses: actions/setup-java@v3
17 | with:
18 | java-version: '17'
19 | distribution: 'temurin'
20 | cache: 'maven'
21 |
22 | - name: Test
23 | run: ./mvnw test
24 |
--------------------------------------------------------------------------------
/src/test/resources/logback-test.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | %d{HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/src/main/java/com/testcontainers/catalog/Application.java:
--------------------------------------------------------------------------------
1 | package com.testcontainers.catalog;
2 |
3 | import org.springframework.boot.SpringApplication;
4 | import org.springframework.boot.autoconfigure.SpringBootApplication;
5 | import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
6 |
7 | @SpringBootApplication
8 | @ConfigurationPropertiesScan
9 | public class Application {
10 |
11 | public static void main(String[] args) {
12 | SpringApplication.run(Application.class, args);
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/main/java/com/testcontainers/catalog/ApplicationProperties.java:
--------------------------------------------------------------------------------
1 | package com.testcontainers.catalog;
2 |
3 | import jakarta.validation.constraints.NotEmpty;
4 | import org.springframework.boot.context.properties.ConfigurationProperties;
5 | import org.springframework.validation.annotation.Validated;
6 |
7 | @ConfigurationProperties(prefix = "application")
8 | @Validated
9 | public record ApplicationProperties(
10 | @NotEmpty String productImagesBucketName,
11 | @NotEmpty String productImageUpdatesTopic,
12 | @NotEmpty String inventoryServiceUrl) {}
13 |
--------------------------------------------------------------------------------
/src/main/resources/db/migration/V1__catalog_tables.sql:
--------------------------------------------------------------------------------
1 | create table products
2 | (
3 | id bigserial primary key,
4 | code varchar not null unique,
5 | name varchar not null,
6 | description varchar,
7 | image varchar,
8 | price numeric not null
9 | );
10 |
11 | insert into products(code, name, description, image, price) values
12 | ('P101','Product P101','Product P101 description', null, 34.0),
13 | ('P102','Product P102','Product P102 description', null, 25.0),
14 | ('P103','Product P103','Product P103 description', null, 15.0)
15 | ;
--------------------------------------------------------------------------------
/src/main/java/com/testcontainers/catalog/domain/ProductService.java:
--------------------------------------------------------------------------------
1 | package com.testcontainers.catalog.domain;
2 |
3 | import com.testcontainers.catalog.domain.models.CreateProductRequest;
4 | import com.testcontainers.catalog.domain.models.Product;
5 | import java.io.InputStream;
6 | import java.util.Optional;
7 |
8 | public interface ProductService {
9 |
10 | void createProduct(CreateProductRequest request);
11 |
12 | Optional getProductByCode(String code);
13 |
14 | void uploadProductImage(String code, String imageName, InputStream inputStream);
15 |
16 | void updateProductImage(String code, String image);
17 | }
18 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | target/
2 | !.mvn/wrapper/maven-wrapper.jar
3 | !**/src/main/**/target/
4 | !**/src/test/**/target/
5 |
6 | ### STS ###
7 | .apt_generated
8 | .classpath
9 | .factorypath
10 | .project
11 | .settings
12 | .springBeans
13 | .sts4-cache
14 |
15 | ### IntelliJ IDEA ###
16 | .idea
17 | *.iws
18 | *.iml
19 | *.ipr
20 |
21 | ### NetBeans ###
22 | /nbproject/private/
23 | /nbbuild/
24 | /dist/
25 | /nbdist/
26 | /.nb-gradle/
27 | build/
28 | !**/src/main/**/build/
29 | !**/src/test/**/build/
30 |
31 | ### VS Code ###
32 | .vscode/
33 |
34 | /src/test
35 | !/src/test/java/com/testcontainers/catalog/.keep
36 | !/src/test/resources/logback-test.xml
37 | !/src/test/resources/P101.jpg
38 |
--------------------------------------------------------------------------------
/src/main/java/com/testcontainers/catalog/domain/internal/ProductRepository.java:
--------------------------------------------------------------------------------
1 | package com.testcontainers.catalog.domain.internal;
2 |
3 | import java.util.Optional;
4 | import org.springframework.data.jpa.repository.JpaRepository;
5 | import org.springframework.data.jpa.repository.Modifying;
6 | import org.springframework.data.jpa.repository.Query;
7 | import org.springframework.data.repository.query.Param;
8 |
9 | interface ProductRepository extends JpaRepository {
10 | Optional findByCode(String code);
11 |
12 | @Modifying
13 | @Query("update ProductEntity p set p.image = :image where p.code = :code")
14 | void updateProductImage(@Param("code") String code, @Param("image") String image);
15 | }
16 |
--------------------------------------------------------------------------------
/src/main/java/com/testcontainers/catalog/domain/internal/ProductEventPublisher.java:
--------------------------------------------------------------------------------
1 | package com.testcontainers.catalog.domain.internal;
2 |
3 | import com.testcontainers.catalog.ApplicationProperties;
4 | import com.testcontainers.catalog.domain.models.ProductImageUploadedEvent;
5 | import org.springframework.kafka.core.KafkaTemplate;
6 | import org.springframework.stereotype.Component;
7 |
8 | @Component
9 | class ProductEventPublisher {
10 | private final KafkaTemplate kafkaTemplate;
11 | private final ApplicationProperties properties;
12 |
13 | public ProductEventPublisher(KafkaTemplate kafkaTemplate, ApplicationProperties properties) {
14 | this.kafkaTemplate = kafkaTemplate;
15 | this.properties = properties;
16 | }
17 |
18 | public void publish(ProductImageUploadedEvent event) {
19 | kafkaTemplate.send(properties.productImageUpdatesTopic(), event.code(), event);
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/main/java/com/testcontainers/catalog/clients/inventory/RestClientConfig.java:
--------------------------------------------------------------------------------
1 | package com.testcontainers.catalog.clients.inventory;
2 |
3 | import com.testcontainers.catalog.ApplicationProperties;
4 | import org.springframework.context.annotation.Bean;
5 | import org.springframework.context.annotation.Configuration;
6 | import org.springframework.web.client.RestClient;
7 | import org.springframework.web.client.support.RestClientAdapter;
8 | import org.springframework.web.service.invoker.HttpServiceProxyFactory;
9 |
10 | @Configuration
11 | class RestClientConfig {
12 |
13 | @Bean
14 | InventoryServiceClient inventoryServiceProxy(ApplicationProperties properties) {
15 | RestClient restClient = RestClient.create(properties.inventoryServiceUrl());
16 | HttpServiceProxyFactory factory = HttpServiceProxyFactory.builderFor(RestClientAdapter.create(restClient))
17 | .build();
18 | return factory.createClient(InventoryServiceClient.class);
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/main/resources/application.properties:
--------------------------------------------------------------------------------
1 | spring.application.name=java-local-development-workshop
2 | server.port=8080
3 | spring.servlet.multipart.max-file-size=10MB
4 | spring.servlet.multipart.max-request-size=10MB
5 |
6 | spring.kafka.producer.key-serializer=org.apache.kafka.common.serialization.StringSerializer
7 | spring.kafka.producer.value-serializer=org.springframework.kafka.support.serializer.JsonSerializer
8 |
9 | spring.kafka.consumer.group-id=catalog-service
10 | spring.kafka.consumer.auto-offset-reset=latest
11 | spring.kafka.consumer.key-deserializer=org.apache.kafka.common.serialization.StringDeserializer
12 | spring.kafka.consumer.value-deserializer=org.springframework.kafka.support.serializer.JsonDeserializer
13 | spring.kafka.consumer.properties.spring.json.trusted.packages=com.testcontainers.catalog.domain.models
14 |
15 | application.product-images-bucket-name=product-images
16 | application.product-image-updates-topic=product-image-updates
17 | application.inventory-service-url=http://localhost:8081
18 |
--------------------------------------------------------------------------------
/src/main/java/com/testcontainers/catalog/config/GlobalExceptionHandler.java:
--------------------------------------------------------------------------------
1 | package com.testcontainers.catalog.config;
2 |
3 | import com.testcontainers.catalog.domain.ProductNotFoundException;
4 | import java.time.Instant;
5 | import org.springframework.http.HttpStatus;
6 | import org.springframework.http.ProblemDetail;
7 | import org.springframework.web.bind.annotation.ExceptionHandler;
8 | import org.springframework.web.bind.annotation.RestControllerAdvice;
9 | import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
10 |
11 | @RestControllerAdvice
12 | class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
13 |
14 | @ExceptionHandler(ProductNotFoundException.class)
15 | ProblemDetail handleProductNotFoundException(ProductNotFoundException e) {
16 | ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, e.getMessage());
17 | problemDetail.setTitle("Product Not Found");
18 | problemDetail.setProperty("timestamp", Instant.now());
19 | return problemDetail;
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 testcontainers
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/main/java/com/testcontainers/catalog/events/ProductEventListener.java:
--------------------------------------------------------------------------------
1 | package com.testcontainers.catalog.events;
2 |
3 | import com.testcontainers.catalog.domain.ProductService;
4 | import com.testcontainers.catalog.domain.models.ProductImageUploadedEvent;
5 | import org.slf4j.Logger;
6 | import org.slf4j.LoggerFactory;
7 | import org.springframework.kafka.annotation.KafkaListener;
8 | import org.springframework.stereotype.Component;
9 | import org.springframework.transaction.annotation.Transactional;
10 |
11 | @Component
12 | @Transactional
13 | class ProductEventListener {
14 | private static final Logger log = LoggerFactory.getLogger(ProductEventListener.class);
15 |
16 | private final ProductService productService;
17 |
18 | ProductEventListener(ProductService productService) {
19 | this.productService = productService;
20 | }
21 |
22 | @KafkaListener(topics = "${application.product-image-updates-topic}", groupId = "catalog-service")
23 | public void handle(ProductImageUploadedEvent event) {
24 | log.info("Received a ProductImageUploaded with code:{}", event.code());
25 | productService.updateProductImage(event.code(), event.image());
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Java Local Development Workshop
2 |
3 | This workshop will explain how to use Testcontainers \([https://www.testcontainers.com](https://www.testcontainers.com)\) in your Java application development process.
4 |
5 | We will work with a Spring Boot application and explore how to:
6 | * Use Testcontainers for provisioning application dependent services like PostgreSQL, Kafka, LocalStack for local development
7 | * Use [Testcontainers Desktop](https://testcontainers.com/desktop/) for local development and debugging
8 | * Write tests using Testcontainers
9 |
10 | ## Table of contents
11 |
12 | * [Step 1: Getting Started](step-1-getting-started.md)
13 | * [Step 2: Exploring the app](step-2-exploring-the-app.md)
14 | * [Step 3: Local Development Environment with Testcontainers](step-3-local-development-environment.md)
15 | * [Step 4: Connect to Services](step-4-connect-to-services.md)
16 | * [Step 5: Write Tests](step-5-write-tests.md)
17 |
18 |
19 | ## License Summary
20 | The code in this repository is made available under the MIT license. See the [LICENSE](LICENSE) file for details.
21 |
22 | The documentation in this repository is available under the CC BY-NC-ND 4.0 license. See the [LICENSE-docs](LICENSE-docs) file for details.
23 | If you'd like to run this workshop at your company, please contact us at [hello@atomicjar.com](mailto:hello@atomicjar.com)
24 |
--------------------------------------------------------------------------------
/src/main/java/com/testcontainers/catalog/domain/internal/S3FileStorageService.java:
--------------------------------------------------------------------------------
1 | package com.testcontainers.catalog.domain.internal;
2 |
3 | import com.testcontainers.catalog.ApplicationProperties;
4 | import com.testcontainers.catalog.domain.FileStorageService;
5 | import io.awspring.cloud.s3.S3Template;
6 | import java.io.InputStream;
7 | import java.time.Duration;
8 | import org.slf4j.Logger;
9 | import org.slf4j.LoggerFactory;
10 | import org.springframework.stereotype.Service;
11 |
12 | @Service
13 | class S3FileStorageService implements FileStorageService {
14 | private static final Logger log = LoggerFactory.getLogger(S3FileStorageService.class);
15 | private final S3Template s3Template;
16 | private final ApplicationProperties properties;
17 |
18 | public S3FileStorageService(S3Template s3Template, ApplicationProperties properties) {
19 | this.s3Template = s3Template;
20 | this.properties = properties;
21 | }
22 |
23 | public void createBucket(String bucketName) {
24 | s3Template.createBucket(bucketName);
25 | }
26 |
27 | public void upload(String filename, InputStream inputStream) {
28 | log.debug("Uploading file with name {} to S3", filename);
29 | try {
30 | s3Template.upload(properties.productImagesBucketName(), filename, inputStream, null);
31 | log.debug("Uploaded file with name {} to S3", filename);
32 | } catch (Exception e) {
33 | log.error("IException: ", e);
34 | throw new RuntimeException(e);
35 | }
36 | }
37 |
38 | public String getPreSignedURL(String filename) {
39 | return s3Template
40 | .createSignedGetURL(properties.productImagesBucketName(), filename, Duration.ofMinutes(60))
41 | .toString();
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/step-2-exploring-the-app.md:
--------------------------------------------------------------------------------
1 | # Step 2: Exploring the app
2 |
3 | The application we are going to work on is a microservice based on Spring Boot for managing a catalog of products.
4 | It provides APIs to save and retrieve the product information.
5 |
6 | 
7 |
8 | ## SQL database with the products
9 |
10 | When a product is created, we will store the product information in our database.
11 |
12 | Our database of choice is PostgreSQL, accessed with Spring Data JPA.
13 |
14 | Check `com.testcontainers.catalog.domain.internal.ProductRepository`.
15 |
16 | ## LocalStack
17 |
18 | We would like to store the product images in AWS S3 Object storage.
19 | We will use [LocalStack](https://localstack.cloud/) to emulate the AWS cloud environment locally during local development and testing with [Spring Cloud AWS](https://awspring.io/).
20 |
21 | Check `com.testcontainers.catalog.domain.internal.S3FileStorageService`.
22 |
23 | ## Kafka
24 |
25 | When a product image is uploaded to AWS S3, an event will be published to Kafka.
26 | The kafka event listener will then consume the event and update the product information with the image URL.
27 |
28 | Check `com.testcontainers.catalog.domain.internal.ProductEventPublisher`
29 | and `com.testcontainers.catalog.events.ProductEventListener`.
30 |
31 | ## External Service Integrations
32 | Our application talks to `inventory-service` to fetch the product availability information.
33 | We will use [Microcks](https://microcks.io/) to mock the `inventory-service` during local development and testing.
34 |
35 | ## API Endpoints
36 |
37 | The API is a Spring Web REST controller \(`com.testcontainers.catalog.api.ProductController`\) and exposes the following endpoints:
38 |
39 | * `POST /api/products { "code": ?, "name": ?, "description": ?, "price": ? }` to create a new product
40 | * `GET /api/products/{code}` to get the product information by code
41 | * `POST /api/products/{code}/image?file=IMAGE` to upload the product image
42 |
43 | ###
44 | [Next](step-3-local-development-environment.md)
--------------------------------------------------------------------------------
/step-4-connect-to-services.md:
--------------------------------------------------------------------------------
1 | # Step 4: Connect to services
2 |
3 | In the previous step, we get our application running locally and invoked our API endpoints.
4 |
5 | What if you want to check the data in the database or the messages in Kafka?
6 |
7 | Testcontainers by default start the containers and map the exposed ports on a random available port on the host machine.
8 | Each time you restart the application, the mapped ports will be different.
9 | This is good for testing, but for local development and debugging, it would be convenient to be able to connect on fixed ports.
10 |
11 | This is where **Testcontainers Desktop** helps you.
12 |
13 | ## Testcontainers Desktop
14 | Testcontainers Desktop application provides several features that helps you with local development and debugging.
15 | To learn more about Testcontainers Desktop, check out the [Simple local development with Testcontainers Desktop](https://testcontainers.com/guides/simple-local-development-with-testcontainers-desktop/) guide.
16 |
17 | The Testcontainers Desktop app makes it easy to use fixed ports for your containers,
18 | so that you can always connect to those services using the same fixed port.
19 |
20 | ## Connect to PostgreSQL database
21 | Click on **Testcontainers Desktop → select Services → Open config location...**.
22 |
23 | In the opened directory there would be a `postgres.toml.example` file.
24 | Make a copy of it and rename it to `postgres.toml` file and update it with the following content:
25 |
26 | ```toml
27 | ports = [
28 | {local-port = 5432, container-port = 5432},
29 | ]
30 | selector.image-names = ["postgres"]
31 | ```
32 |
33 | We are mapping the PostgreSQL container's port 5432 onto the host's port 5432.
34 | Now you should be able to connect to the PostgreSQL database using any SQL client
35 | with the following connection properties:
36 |
37 | ```shell
38 | psql -h localhost -p 5432 -U test -d test
39 | ```
40 |
41 | If you authenticate with the default password `test` you should be able to and run the following SQL query to check the products:
42 |
43 | ```sql
44 | select * from test.public.products;
45 | ```
46 |
47 | Similarly, you can connect to any of your containers using the same approach by using the port-mapping feature of Testcontainers Desktop.
48 |
49 | ###
50 | [Next](step-5-write-tests.md)
51 |
--------------------------------------------------------------------------------
/src/main/java/com/testcontainers/catalog/domain/internal/ProductEntity.java:
--------------------------------------------------------------------------------
1 | package com.testcontainers.catalog.domain.internal;
2 |
3 | import jakarta.persistence.Column;
4 | import jakarta.persistence.Entity;
5 | import jakarta.persistence.GeneratedValue;
6 | import jakarta.persistence.GenerationType;
7 | import jakarta.persistence.Id;
8 | import jakarta.persistence.Table;
9 | import jakarta.validation.constraints.DecimalMin;
10 | import jakarta.validation.constraints.NotEmpty;
11 | import jakarta.validation.constraints.NotNull;
12 | import java.math.BigDecimal;
13 |
14 | @Entity
15 | @Table(name = "products")
16 | class ProductEntity {
17 | @Id
18 | @GeneratedValue(strategy = GenerationType.IDENTITY)
19 | private Long id;
20 |
21 | @Column(nullable = false, unique = true)
22 | @NotEmpty(message = "Product code must not be null/empty")
23 | private String code;
24 |
25 | @NotEmpty(message = "Product name must not be null/empty")
26 | @Column(nullable = false)
27 | private String name;
28 |
29 | private String description;
30 |
31 | private String image;
32 |
33 | @NotNull(message = "Product price must not be null") @DecimalMin("0.1")
34 | @Column(nullable = false)
35 | private BigDecimal price;
36 |
37 | public ProductEntity() {}
38 |
39 | public ProductEntity(Long id, String code, String name, String description, String image, BigDecimal price) {
40 | this.id = id;
41 | this.code = code;
42 | this.name = name;
43 | this.description = description;
44 | this.image = image;
45 | this.price = price;
46 | }
47 |
48 | public Long getId() {
49 | return id;
50 | }
51 |
52 | public void setId(Long id) {
53 | this.id = id;
54 | }
55 |
56 | public String getCode() {
57 | return code;
58 | }
59 |
60 | public void setCode(String code) {
61 | this.code = code;
62 | }
63 |
64 | public String getName() {
65 | return name;
66 | }
67 |
68 | public void setName(String name) {
69 | this.name = name;
70 | }
71 |
72 | public String getDescription() {
73 | return description;
74 | }
75 |
76 | public void setDescription(String description) {
77 | this.description = description;
78 | }
79 |
80 | public String getImage() {
81 | return image;
82 | }
83 |
84 | public void setImage(String image) {
85 | this.image = image;
86 | }
87 |
88 | public BigDecimal getPrice() {
89 | return price;
90 | }
91 |
92 | public void setPrice(BigDecimal price) {
93 | this.price = price;
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/src/main/java/com/testcontainers/catalog/api/ProductController.java:
--------------------------------------------------------------------------------
1 | package com.testcontainers.catalog.api;
2 |
3 | import com.testcontainers.catalog.domain.ProductNotFoundException;
4 | import com.testcontainers.catalog.domain.ProductService;
5 | import com.testcontainers.catalog.domain.models.CreateProductRequest;
6 | import com.testcontainers.catalog.domain.models.Product;
7 | import java.io.IOException;
8 | import java.net.URI;
9 | import java.util.Map;
10 | import org.springframework.http.ResponseEntity;
11 | import org.springframework.validation.annotation.Validated;
12 | import org.springframework.web.bind.annotation.GetMapping;
13 | import org.springframework.web.bind.annotation.PathVariable;
14 | import org.springframework.web.bind.annotation.PostMapping;
15 | import org.springframework.web.bind.annotation.RequestBody;
16 | import org.springframework.web.bind.annotation.RequestMapping;
17 | import org.springframework.web.bind.annotation.RequestParam;
18 | import org.springframework.web.bind.annotation.RestController;
19 | import org.springframework.web.multipart.MultipartFile;
20 | import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
21 |
22 | @RestController
23 | @RequestMapping("/api/products")
24 | class ProductController {
25 | private final ProductService productService;
26 |
27 | ProductController(ProductService productService) {
28 | this.productService = productService;
29 | }
30 |
31 | @PostMapping
32 | ResponseEntity createProduct(@Validated @RequestBody CreateProductRequest request) {
33 | productService.createProduct(request);
34 | URI uri = ServletUriComponentsBuilder.fromCurrentContextPath()
35 | .path("/api/products/{code}")
36 | .buildAndExpand(request.code())
37 | .toUri();
38 | return ResponseEntity.created(uri).build();
39 | }
40 |
41 | @GetMapping("/{code}")
42 | ResponseEntity getProductByCode(@PathVariable String code) {
43 | var product = productService.getProductByCode(code).orElseThrow(() -> ProductNotFoundException.withCode(code));
44 | return ResponseEntity.ok(product);
45 | }
46 |
47 | @PostMapping("/{code}/image")
48 | ResponseEntity