Our application uses PostgreSQL, Kafka, and LocalStack.
Currently, if you run the Application.java
from your IDE, you will see the following error:
***************************
APPLICATION FAILED TO START
***************************
Description:
Failed to configure a DataSource: 'url' attribute is not specified and no embedded datasource could be configured.
Reason: Failed to determine a suitable driver class
Action:
Consider the following:
If you want an embedded database (H2, HSQL or Derby), please put it on the classpath.
If you have database settings to be loaded from a particular profile you may need to activate it (no profiles are currently active).
Process finished with exit code 0
To run the application locally, we need to have these services up and running.
Instead of installing these services on our local machine, or using Docker to run these services manually, we will use Spring Boot support for Testcontainers at Development Time to provision these services automatically.
NOTE
Before Spring Boot 3.1.0, Testcontainers libraries are mainly used for testing. Spring Boot 3.1.0 introduced out-of-the-box support for Testcontainers which not only simplified testing, but we can use Testcontainers for local development as well.
To learn more, please read Spring Boot Application Testing and Development with Testcontainers
First, make sure you have the following Testcontainers dependencies in your pom.xml
:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-testcontainers</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>postgresql</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>kafka</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>localstack</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
We will also use RestAssured for API testing and Awaitility for testing asynchronous processes.
So, add the following dependencies as well:
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.awaitility</groupId>
<artifactId>awaitility</artifactId>
<scope>test</scope>
</dependency>
Let's create com.testcontainers.catalog.ContainersConfig
class under src/test/java
to configure the required containers.
package com.testcontainers.catalog;
import static org.testcontainers.utility.DockerImageName.parse;
import com.testcontainers.catalog.domain.FileStorageService;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.DependsOn;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.testcontainers.containers.KafkaContainer;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.containers.localstack.LocalStackContainer;
@TestConfiguration(proxyBeanMethods = false)
public class ContainersConfig {
@Bean
@ServiceConnection
PostgreSQLContainer<?> postgresContainer() {
return new PostgreSQLContainer<>(parse("postgres:16-alpine"));
}
@Bean
@ServiceConnection
KafkaContainer kafkaContainer() {
return new KafkaContainer(parse("confluentinc/cp-kafka:7.5.0"));
}
@Bean("localstackContainer")
LocalStackContainer localstackContainer(DynamicPropertyRegistry registry) {
LocalStackContainer localStack = new LocalStackContainer(parse("localstack/localstack:2.3"));
registry.add("spring.cloud.aws.credentials.access-key", localStack::getAccessKey);
registry.add("spring.cloud.aws.credentials.secret-key", localStack::getSecretKey);
registry.add("spring.cloud.aws.region.static", localStack::getRegion);
registry.add("spring.cloud.aws.endpoint", localStack::getEndpoint);
return localStack;
}
@Bean
@DependsOn("localstackContainer")
ApplicationRunner awsInitializer(ApplicationProperties properties, FileStorageService fileStorageService) {
return args -> fileStorageService.createBucket(properties.productImagesBucketName());
}
}
Let's understand what this configuration class does:
@TestConfiguration
annotation indicates that this configuration class defines the beans that can be used for Spring Boot tests.- Spring Boot provides
ServiceConnection
support forJdbcConnectionDetails
andKafkaConnectionDetails
out-of-the-box. So, we configuredPostgreSQLContainer
andKafkaContainer
as beans with@ServiceConnection
annotation. This configuration will automatically start these containers and register the DataSource and Kafka connection properties automatically. - Spring Cloud AWS doesn't provide ServiceConnection support out-of-the-box yet.
But there is support for Contributing Dynamic Properties at Development Time.
So, we configured
LocalStackContainer
as a bean and registered the Spring Cloud AWS configuration properties usingDynamicPropertyRegistry
. - We also configured an
ApplicationRunner
bean to create the AWS resources like S3 bucket upon application startup.
Next, let's create a com.testcontainers.catalog.TestApplication
class under src/test/java
to start the application with the Testcontainers configuration.
package com.testcontainers.catalog;
import org.springframework.boot.SpringApplication;
public class TestApplication {
public static void main(String[] args) {
SpringApplication
//note that we are starting our actual Application from within our TestApplication
.from(Application::main)
.with(ContainersConfig.class)
.run(args);
}
}
Run the TestApplication
from our IDE and verify that the application starts successfully.
Now, you can invoke the APIs using CURL or Postman or any of your favourite HTTP Client tools.
curl -v -X "POST" 'http://localhost:8080/api/products' \
--header 'Content-Type: application/json' \
--data '{
"code": "P201",
"name": "Product P201",
"description": "Product P201 description",
"price": 24.0
}'
You should get a response similar to the following:
< HTTP/1.1 201
< Location: http://localhost:8080/api/products/P201
< Content-Length: 0
curl -X "POST" 'http://localhost:8080/api/products/P101/image' \
--form 'file=@"src/test/resources/P101.jpg"'
You should see a response similar to the following:
{"filename":"P101.jpg","status":"success"}
curl -X "GET" 'http://localhost:8080/api/products/P101'
You should be able to see the response similar to the following:
{
"id":1,
"code":"P101",
"name":"Product P101",
"description":"Product P101 description",
"imageUrl":"http://127.0.0.1:60739/product-images/P101.jpg?X-Amz-Algorithm=AWS4-HMAC-SHA256&...",
"price":34.0,
"available":true
}
If you check the application logs, you should see the following error in logs:
com.testcontainers.catalog.domain.internal.DefaultProductService - Error while calling inventory service
org.springframework.web.client.ResourceAccessException: I/O error on GET request for "http://localhost:8081/api/inventory/P101": null
at org.springframework.web.client.DefaultRestClient$DefaultRequestBodyUriSpec.createResourceAccessException(DefaultRestClient.java:489)
at org.springframework.web.client.DefaultRestClient$DefaultRequestBodyUriSpec.exchangeInternal(DefaultRestClient.java:414)
at org.springframework.web.client.DefaultRestClient$DefaultRequestBodyUriSpec.retrieve(DefaultRestClient.java:380)
...
...
at jdk.proxy4/jdk.proxy4.$Proxy179.getInventory(Unknown Source)
at com.testcontainers.catalog.domain.internal.DefaultProductService.isProductAvailable(DefaultProductService.java:68)
at com.testcontainers.catalog.domain.internal.DefaultProductService.toProduct(DefaultProductService.java:84)
at java.base/java.util.Optional.map(Optional.java:260)
When we invoke the GET /api/products/{code}
API endpoint,
the application tried to call the inventory service to get the inventory details.
As the inventory service is not running, we get the above error.
Let's use Microcks to mock the inventory service APIs for our local development and testing.
Add the following dependency to your pom.xml
:
<dependency>
<groupId>io.github.microcks</groupId>
<artifactId>microcks-testcontainers</artifactId>
<version>0.2.4</version>
<scope>test</scope>
</dependency>
Imagine you got an OpenAPI definition for the inventory service from the service provider.
Create src/test/resources/inventory-openapi.yaml
with this content; this will define the Mocks behaviour:
openapi: 3.0.2
info:
title: Inventory Service
version: 1.0
description: API definition of Inventory Service
license:
name: MIT License
url: https://opensource.org/licenses/MIT
paths:
/api/inventory/{code}:
get:
parameters:
- name: code
description: product code
schema:
type: string
in: path
required: true
examples:
P101:
value: P101
P102:
value: P102
P103:
value: P103
responses:
"200":
content:
application/json:
schema:
$ref: '#/components/schemas/Product'
examples:
P101:
value:
code: P101
quantity: 25
P103:
value:
code: P103
quantity: 0
"500":
content:
application/json:
schema:
type: string
examples:
P102:
value: ""
components:
schemas:
Product:
title: Root Type for Product
type: object
properties:
code:
description: Code of this product
type: string
quantity:
description: Remaining quantity for this product
type: number
required:
- code
- quantity
additionalProperties: false
Next, update the ContainersConfig
class to configure the MicrocksContainer
as follows:
package com.testcontainers.catalog;
import io.github.microcks.testcontainers.MicrocksContainer;
@TestConfiguration(proxyBeanMethods = false)
public class ContainersConfig {
// [...]
@Bean
MicrocksContainer microcksContainer(DynamicPropertyRegistry registry) {
MicrocksContainer microcks = new MicrocksContainer("quay.io/microcks/microcks-uber:1.8.1")
.withMainArtifacts("inventory-openapi.yaml")
.withAccessToHost(true);
registry.add(
"application.inventory-service-url", () -> microcks.getRestMockEndpoint("Inventory Service", "1.0"));
return microcks;
}
}
Once the Microcks server is started, we are registering the Microcks provided mock endpoint as application.inventory-service-url
.
So, when we make a call to inventory-service
from our application, it will call the Microcks endpoint instead.
Now restart the TestApplication
and invoke the GET /api/products/P101
API again.
curl -X "GET" 'http://localhost:8080/api/products/P101'
You should see the response similar to the following:
{
"id":1,
"code":"P101",
"name":"Product P101",
"description":"Product P101 description",
"imageUrl":null,
"price":34.0,
"available":true
}
And there should be no error in the console logs.
Try curl -X "GET" 'http://localhost:8080/api/products/P103'
.
You should get the following response with "available":false
because we mocked inventory-service such that the quantity for P103 to be 0.
{
"id":3,
"code":"P103",
"name":"Product P103",
"description":"Product P103 description",
"imageUrl":null,
"price":15.0,
"available":false
}
Now we have a working local development environment with PostgreSQL, Kafka, LocalStack, and Microcks.