Monday, April 15, 2024
No menu items!
HomeCloud ComputingDevelop and Test Spring Boot Applications Consistently

Develop and Test Spring Boot Applications Consistently

You do not have to burden yourself anymore with a long list of manual steps required to setup an application locally, test with an in-memory database, sift through discrepancies between environments and SQL dialects or not being able to test network failures typical for a distributed system.

When building Twelve-factor Google Cloud applications, developers are looking for a consistent way to develop, debug and test locally with the same containerized application dependencies as they would run in production in GCP, the environment parity from Dev to Prod – factor Ten on the list of twelve-factor apps.

Google’s Java Cloud Client Libraries and Spring Cloud GCP make migrating, or building a new cloud-native, production-ready Spring Boot application a breeze, with developers able to quickly spin up the application and focus on building out the business logic.

In this blog post we’ll focus on significantly improving (and speeding up) your development experience for Google Cloud Spring Boot Java Applications, with a consistent way to develop, debug and test locally, following the goal of environment parity from Development-to-Production in Google Cloud.

Google Cloud Emulators, Testcontainers and Spring Boot to the rescue

Testcontainers is an open source framework for providing throwaway, lightweight instances of databases, message brokers, web browsers, or any other custom Docker container.  There is no more need for mocks or complicated environment configs. Testcontainers allow you to define your test dependencies as code, then simply run your tests and containers will be created and then deleted at the end of the test lifecycle.

Google’s Cloud SDK provides libraries and tools for interacting with the products and services provided by the Google Cloud Platform. Testcontainers modules are preconfigured containerized implementations of various dependencies that make writing your tests very easy.

Currently, Testcontainers Google Cloud Module supports Bigtable, Datastore, Firestore, Spanner, and Pub/Sub GCP Emulators in Java and other languages.

Spring Boot 3.1 has added Testcontainers support, allowing you to configure all application dependencies (databases, messaging systems, caching mechanisms, dependent services) to start automatically when you run or test the application locally.

If you are looking to learn more about how to test your Spring Boot applications with Testcontainers, follow this Getting started guide.

Development tasks

Before we dive into details, let’s revisit the some of the tasks you would usually address as a developer:

Write Unit Tests for individual pieces of functionality, say classes/methods

Write (Domain) Service Integration Tests to test domain operations and business rules, including the Integration with dependent applications or cloud services

Write Application Tests to functionally test your application and its web controllers 

Write Network Failure Tests to assess that the source code will handle any such failure, part of chaos engineering best practices

Local Development with app dependencies

Blog Source Code

This blog post is supported by a full set of Spring Boot sample apps, with tests highlighting the usage of Cloud Firestore and open-source Postgres (for Cloud SQL) Testcontainers.

Clone the Git repository and follow along in the Audit (Firestore) and Quotes (Postgres) services to gain hands-on experience with the concepts exposed herein. The following material will continue on the Firestore usage path.

Test configuration 

First, add the spring-boot-testcontainers as a test dependency to your Maven pom.xml:

code_block[StructValue([(u’code’, u'<dependency>rn <groupId>org.springframework.boot</groupId>rn <artifactId>spring-boot-testcontainers</artifactId>rn <scope>test</scope>rn</dependency>’), (u’language’, u”), (u’caption’, <wagtail.wagtailcore.rich_text.RichText object at 0x3e18ce3c7e50>)])]

Next, add the Google Client Libraries Bill-of-Materials(BOM) for compatibility across more than 200 Google Java libraries:

code_block[StructValue([(u’code’, u'<dependency>rn <groupId>com.google.cloud</groupId>rn <artifactId>libraries-bom</artifactId>rn <version>26.18.0</version>rn <type>pom</type>rn <scope>import</scope>rn</dependency>’), (u’language’, u”), (u’caption’, <wagtail.wagtailcore.rich_text.RichText object at 0x3e18ce3dc950>)])]

Last, add the dependency on the respective application dependent service:

code_block[StructValue([(u’code’, u'<!– Firestore –>rn<dependency>rn <groupId>com.google.cloud</groupId>rn <artifactId>google-cloud-firestore</artifactId>rn</dependency>rnrn<!– or … Postgres –> rn<dependency>rn <groupId>org.postgresql</groupId>rn <artifactId>postgresql</artifactId>rn <scope>runtime</scope>rn</dependency>’), (u’language’, u”), (u’caption’, <wagtail.wagtailcore.rich_text.RichText object at 0x3e18b8fb0fd0>)])]

To test with the testcontainer for the Firestore emulator, configure the @Container and provide its dynamic properties in the test class. 

Ex: Firestore Service Test [test]

code_block[StructValue([(u’code’, u’@Containerrnprivate static final FirestoreEmulatorContainer firestoreEmulator =rn new FirestoreEmulatorContainer(rn DockerImageName.parse(rn “gcr.io/google.com/cloudsdktool/cloud-sdk:438.0.0-emulators”));rnrn@DynamicPropertySourcernstatic void emulatorProperties(DynamicPropertyRegistry registry) {rn registry.add(“spring.cloud.gcp.firestore.host-port”, rn firestoreEmulator::getEmulatorEndpoint);rn}’), (u’language’, u”), (u’caption’, <wagtail.wagtailcore.rich_text.RichText object at 0x3e18b9a26590>)])]

Note that Spring Boot 3.1  has added the concept of Service Connections, which automatically configures the necessary Spring Boot properties for the supporting containers. 

A Postgres configuration would be reduced to:

code_block[StructValue([(u’code’, u’@Containerrn @ServiceConnectionrn private static final PostgreSQLContainer<?> postgres = new rn PostgreSQLContainer<>(“postgres:15.3-alpine”);’), (u’language’, u”), (u’caption’, <wagtail.wagtailcore.rich_text.RichText object at 0x3e18b9a34910>)])]

Writing (Domain) Service/Integration Tests

Testing your service against a containerized Firestore emulator provides the confidence that the code will run smoothly in Cloud Firestore: [test]

code_block[StructValue([(u’code’, u’@Testrnvoid testEventRepositoryStoreImage() throws ExecutionException, InterruptedException {rn ApiFuture<WriteResult> writeResult = eventService.auditQuote(“test quote”, rn “test author”, “test book”, UUID.randomUUID().toString());rn Assertions.assertNotNull(writeResult.get().getUpdateTime());rn}rn…rnpublic ApiFuture<WriteResult> auditQuote(String quote, String author, String book, String randomID) {rn DocumentReference doc = firestore.collection(“books”).document(author);rnrn Map<String, Object> data = new HashMap<>();rn data.put(“created”, new Date());rn data.put(“quote”,quote);rn data.put(“author”,author);rn data.put(“book”,book);rn data.put(“randomID”,randomID);rnrn return doc.set(data, SetOptions.merge());rn}’), (u’language’, u”), (u’caption’, <wagtail.wagtailcore.rich_text.RichText object at 0x3e18acb5c210>)])]

Writing Application Tests

Writing application tests allows you to test your app as you would run it in a cloud environment in Prod. 

For example, your service is running in a Serverless environment and is a component of an event-driven architecture. CloudEvents is a specification for describing event data in a common way and you can now test your HTTP call with CloudEvents in the web controller, while leveraging Firestore testcontainers: [test]

code_block[StructValue([(u’code’, u’@BeforeEachrnpublic void setup() throws JSONException {rn JSONObject message =rn new JSONObject()rn .put(“quote”, “test quote”)rn .put(“author”, “anonymous”)rn .put(“book”, “new book”)rn .put(“randomId”, UUID.randomUUID())rn .put(“attributes”, new JSONObject());rn mockBody = new JSONObject().put(“message”, message).toString();rn}rnrn@Testrnpublic void goodTest() throws Exception {rn mockMvcrn .perform(rn post(“/”)rn .contentType(MediaType.APPLICATION_JSON)rn .content(mockBody)rn .header(“ce-id”, “test id”)rn .header(“ce-source”, “test source”)rn .header(“ce-type”, “test type”)rn .header(“ce-specversion”, “test specversion”)rn .header(“ce-subject”, “test subject”))rn .andExpect(status().isOk());rn}’), (u’language’, u”), (u’caption’, <wagtail.wagtailcore.rich_text.RichText object at 0x3e18acb6d490>)])]

Writing Network Failure tests

Network failure conditions between services running in the cloud present one of the highest risks in distributed software, since they directly affect service delivery. Before running your services in cloud environments, you want to test the resiliency of your service client code in your codebase for any such random disruptions, which can cause applications to respond unpredictably and break under pressure.

These tests are part of Chaos Engineering best practices.

To simulate network failure conditions, you can place Shopify’s Toxiproxy container in between test code and a container, or in between containers. [test]

code_block[StructValue([(u’code’, u’private static final Network network = Network.newNetwork();rn@Containerrnprivate static final FirestoreEmulatorContainer firestoreEmulator =rn new FirestoreEmulatorContainer(rn DockerImageName.parse(rn “gcr.io/google.com/cloudsdktool/cloud-sdk:438.0.0-emulators”))rn .withNetwork(network).withNetworkAliases(“firestore”);rnrn@Containerrnprivate static final ToxiproxyContainer toxiproxy = new ToxiproxyContainer(“ghcr.io/shopify/toxiproxy:2.5.0”)rn .withNetwork(network);rn rn@DynamicPropertySourcernstatic void emulatorProperties(DynamicPropertyRegistry registry) throws IOException{rn var toxiproxyClient = new ToxiproxyClient(toxiproxy.getHost(), rn toxiproxy.getControlPort());rn firestoreProxy = toxiproxyClient.createProxy(“firestore”, “0.0.0.0:8666”, rn “firestore:5637”);rn registry.add(“spring.cloud.gcp.firestore.host-port”, firestoreEmulator::getEmulatorEndpoint);rn}’), (u’language’, u”), (u’caption’, <wagtail.wagtailcore.rich_text.RichText object at 0x3e18acb6d450>)])]

You can now simulate timeouts due to latency, retries or dropped network connections. 

For example, you can test with a simulated timeout as follows:

code_block[StructValue([(u’code’, u’@Testrn@DisplayName(“Test with latency, timeout encountered”)rnvoid testEventRepositoryStoreQuoteWithLatencyandTimeout() throws ExecutionException, InterruptedException, IOException {rn firestoreProxy.toxics().latency(“firestore-latency”, ToxicDirection.DOWNSTREAM, 1600).setJitter(100);rnrn try {rn assertTimeout(Duration.ofSeconds(1), () -> {rn ApiFuture<WriteResult> writeResult = eventService.auditQuote(“test quote”, “test author”, “test book”, UUID.randomUUID().toString());rn Assertions.assertNotNull(writeResult.get().getUpdateTime()); rn });rn }catch (AssertionFailedError e){rn System.out.println(“Test and encounter exception” + e.getMessage());rn }rn}’), (u’language’, u”), (u’caption’, <wagtail.wagtailcore.rich_text.RichText object at 0x3e18acb6d750>)])]

Local Development with application dependencies

Starting with Spring Boot 3.1, you can also use Testcontainers during your regular dev time for running and debugging the application locally with your respective dependent services! 

There is no need to do any extra work, just configure a TestAuditApplication and TestcontainersConfig class in your test classpath under /src/test/java and start your app.

Note that the app is started with the TestcontainersConfig class attached to the application launcher using .with(TestcontainersConfig):

code_block[StructValue([(u’code’, u’public class TestAuditApplication {rn public static void main(String[] args) {rn SpringApplicationrn .from(AuditApplication::main)rn .with(TestcontainersConfig.class)rn .run(args);rn }rn}’), (u’language’, u”), (u’caption’, <wagtail.wagtailcore.rich_text.RichText object at 0x3e18acb6d510>)])]

 You can run the TestAuditApplication from your IDE or simply with:

code_block[StructValue([(u’code’, u’./mvnw spring-boot:test-run’), (u’language’, u”), (u’caption’, <wagtail.wagtailcore.rich_text.RichText object at 0x3e18acb6dad0>)])]

Summary

By using Google Cloud Emulators, Testcontainers and Spring Boot 3.1, you can significantly improve (and speed-up) your development experience for Google Cloud Applications, with a consistent way to develop, debug and test locally with the same containerized application dependencies as you would run in production in Google Cloud — the environment parity from Dev to Prod.

Check out the codebase, if you want to get more hands-on experience with Spring Boot Applications using Google Cloud Emulators and Testcontainers. 

For questions or feedback, feel free to contact me on Twitter @ddobrin.

Cloud BlogRead More

RELATED ARTICLES

LEAVE A REPLY

Please enter your comment!
Please enter your name here

Most Popular

Recent Comments