Friday, December 29, 2023

Testable Spring Boot Application main method with Docker Compose added - passing required arguments . . .

Java application's entry point is its main method which takes String array as argument. The command line arguments are passed by Java runtime system to the main method as an array of Strings.

Spring Boot application is a command line runnable Java application with a main method. The SpringApplication class is used to bootstrap and launch Spring app from Java's main method. Spring framework promotes POJO development and hence makes it easy to write testable code. A simple JUnit test can be written to test the main method.

This post is about writing a simple JUnit test case for Spring Boot application's main method in the context of Docker Compose dependency, and passing required arguments.

Environment: Java 21, Spring Boot 3.2.1, maven 3.9.6 on macOS Catalina 10.15.7

A typical, simple Spring Boot application class with a main method looks like:
import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class GraalVmApplication { public static void main(String[] args) { SpringApplication.run(GraalVmApplication.class, args); } }

A simple JUnit test case to test the application looks like:
import org.junit.jupiter.api.Test; public class ApplicationStartsTest { @Test void application_starts() { GraalVmApplication.main(new String[]{}); } }

The test case is simple. It just invokes application's main method which in turn launches the Spring boot application and exits. Note that there are no arguments passed, so it's launched with default profile, which is ok to make sure the application runs. 

The Scenario

Now, let's says that we want to add JPA and Postgres DB support to our application. Also want to try Spring support for Docker Compose by adding Postgres database service docker container for database. Spring docker support takes care of starting up the Postgres docker container and bringing it down when the app exists. All it requires is to to add spring-boot-docker-compose dependency and compose.yaml or docker-compose.yaml under the root project. Refer to this Sample Git project.

With the JPA, spring-boot-docker-compose dependencies and docker-compose.yaml added, the above JUnit test case fails to run with the following exception:
*************************** 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). [ERROR] Tests run: 1, Failures: 0, Errors: 1, Skipped: 0, Time elapsed: 3.731 s <<< FAILURE! -- in com.giri.boot.graalvm.ApplicationStartsTest [ERROR] com.giri.boot.graalvm.ApplicationStartsTest.applicationStarts -- Time elapsed: 3.705 s <<< ERROR! org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'dataSourceScriptDatabaseInitializer' defined in class path resource [org/springframework/boot/autoconfigure/sql/init/DataSourceInitializationConfiguration.class]: Unsatisfied dependency expressed through method 'dataSourceScriptDatabaseInitializer' parameter 0: Error creating bean with name 'dataSource' defined in class path resource [org/springframework/boot/autoconfigure/jdbc/DataSourceConfiguration$Hikari.class]: Failed to instantiate [com.zaxxer.hikari.HikariDataSource]: Factory method 'dataSource' threw exception with message: Failed to determine a suitable driver class at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:802) at org.springframework.beans.factory.support.ConstructorResolver.instantiateUsingFactoryMethod(ConstructorResolver.java:546) at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.instantiateUsingFactoryMethod(AbstractAutowireCapableBeanFactory.java:1334) at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1164) at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:561) at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:521) at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:325) at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234) at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:323) at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:199) at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:312) at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:199) at org.springframework.context.support.AbstractApplicationContext.getBean(AbstractApplicationContext.java:1232) at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:950) at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:625) at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:146) at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:762) at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:464) at org.springframework.boot.SpringApplication.run(SpringApplication.java:334) at org.springframework.boot.SpringApplication.run(SpringApplication.java:1358) at org.springframework.boot.SpringApplication.run(SpringApplication.java:1347) at com.giri.boot.graalvm.GraalVmApplication.main(GraalVmApplication.java:12) at com.giri.boot.graalvm.ApplicationStartsTest.applicationStarts(ApplicationStartsTest.java:17) at java.base/java.lang.reflect.Method.invoke(Method.java:580) at java.base/java.util.ArrayList.forEach(ArrayList.java:1596) at java.base/java.util.ArrayList.forEach(ArrayList.java:1596) Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'dataSource' defined in class path resource [org/springframework/boot/autoconfigure/jdbc/DataSourceConfiguration$Hikari.class]: Failed to instantiate [com.zaxxer.hikari.HikariDataSource]: Factory method 'dataSource' threw exception with message: Failed to determine a suitable driver class at org.springframework.beans.factory.support.ConstructorResolver.instantiate(ConstructorResolver.java:655) at org.springframework.beans.factory.support.ConstructorResolver.instantiateUsingFactoryMethod(ConstructorResolver.java:643) at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.instantiateUsingFactoryMethod(AbstractAutowireCapableBeanFactory.java:1334) at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1164) at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:561) at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:521) at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:325) at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234) at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:323) at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:199) at org.springframework.beans.factory.config.DependencyDescriptor.resolveCandidate(DependencyDescriptor.java:254) at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1443) at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1353) at org.springframework.beans.factory.support.ConstructorResolver.resolveAutowiredArgument(ConstructorResolver.java:911) at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:789)

The reason for the failure is -  by default docker compose support is disabled when running tests.

The Fix

The fix is to pass additional arguments like - enable docker compose support for this test, profile (local) for which datasource properties are configured etc. With these, when the unit test is run, docker compose is up, which brings up Postgres container and the application connects to the database through datasource configuration properties specified in application-local.yml file for local profile. With the fix, the test case looks like:

public class ApplicationStartsTest { @Test void applicationStarts() { GraalVmApplication.main( new String[]{ "--spring.profiles.active=local", "--spring.docker.compose.skip.in-tests=false", "--spring.docker.compose.file=docker/docker-compose.yaml" } ); } }

Now, the test case passes. Below is the output with docker compose highlighted:
[INFO] ------------------------------------------------------- [INFO] T E S T S [INFO] ------------------------------------------------------- [INFO] Running com.giri.boot.graalvm.ApplicationStartsTest arg: --spring.profiles.active=local arg: --spring.docker.compose.skip.in-tests=false arg: --spring.docker.compose.file=docker/docker-compose.yaml ____ _ ____ ___ __ | __ ) ___ ___ | |_ / ___|_ __ __ _ __ _| \ \ / / __ ___ | _ \ / _ \ / _ \| __| | | _| '__/ _` |/ _` | |\ \ / / '_ ` _ \ | |_) | (_) | (_) | |_ | |_| | | | (_| | (_| | | \ V /| | | | | | |____/ \___/ \___/ \__| \____|_| \__,_|\__,_|_| \_/ |_| |_| |_| :: Spring Boot :: 3.2.1 :: Running on Java :: 21 :: Application :: 0.0.1-SNAPSHOT 2023-12-28T20:09:11.059-05:00 INFO 65561 --- [boot-graalvm] [ main] c.giri.boot.graalvm.GraalVmApplication : Starting GraalVmApplication using Java 21 with PID 65561 (started by pottepalemg in /Users/pottepalemg/dev/boot-graalvm) 2023-12-28T20:09:11.065-05:00 INFO 65561 --- [boot-graalvm] [ main] c.giri.boot.graalvm.GraalVmApplication : The following 1 profile is active: "local" 2023-12-28T20:09:11.523-05:00 INFO 65561 --- [boot-graalvm] [ main] .s.b.d.c.l.DockerComposeLifecycleManager : Using Docker Compose file '/Users/pottepalemg/dev/boot-graalvm/docker/docker-compose.yaml' 2023-12-28T20:09:12.547-05:00 INFO 65561 --- [boot-graalvm] [utReader-stderr] o.s.boot.docker.compose.core.DockerCli : Container docker-postgres-1 Creating 2023-12-28T20:09:12.614-05:00 INFO 65561 --- [boot-graalvm] [utReader-stderr] o.s.boot.docker.compose.core.DockerCli : Container docker-postgres-1 Created 2023-12-28T20:09:12.618-05:00 INFO 65561 --- [boot-graalvm] [utReader-stderr] o.s.boot.docker.compose.core.DockerCli : Container docker-postgres-1 Starting 2023-12-28T20:09:13.169-05:00 INFO 65561 --- [boot-graalvm] [utReader-stderr] o.s.boot.docker.compose.core.DockerCli : Container docker-postgres-1 Started 2023-12-28T20:09:13.174-05:00 INFO 65561 --- [boot-graalvm] [utReader-stderr] o.s.boot.docker.compose.core.DockerCli : Container docker-postgres-1 Waiting 2023-12-28T20:09:13.682-05:00 INFO 65561 --- [boot-graalvm] [utReader-stderr] o.s.boot.docker.compose.core.DockerCli : Container docker-postgres-1 Healthy 2023-12-28T20:09:16.211-05:00 INFO 65561 --- [boot-graalvm] [ main] .s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data JPA repositories in DEFAULT mode. 2023-12-28T20:09:16.237-05:00 INFO 65561 --- [boot-graalvm] [ main] .s.d.r.c.RepositoryConfigurationDelegate : Finished Spring Data repository scanning in 17 ms. Found 0 JPA repository interfaces. 2023-12-28T20:09:17.133-05:00 INFO 65561 --- [boot-graalvm] [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port 8080 (http) 2023-12-28T20:09:17.157-05:00 INFO 65561 --- [boot-graalvm] [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat] 2023-12-28T20:09:17.157-05:00 INFO 65561 --- [boot-graalvm] [ main] o.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/10.1.17] 2023-12-28T20:09:17.233-05:00 INFO 65561 --- [boot-graalvm] [ main] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext 2023-12-28T20:09:17.234-05:00 INFO 65561 --- [boot-graalvm] [ main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 2082 ms 2023-12-28T20:09:17.648-05:00 INFO 65561 --- [boot-graalvm] [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Starting... 2023-12-28T20:09:17.895-05:00 INFO 65561 --- [boot-graalvm] [ main] com.zaxxer.hikari.pool.HikariPool : HikariPool-1 - Added connection org.postgresql.jdbc.PgConnection@24a99b1c 2023-12-28T20:09:17.897-05:00 INFO 65561 --- [boot-graalvm] [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Start completed. 2023-12-28T20:09:17.970-05:00 INFO 65561 --- [boot-graalvm] [ main] o.hibernate.jpa.internal.util.LogHelper : HHH000204: Processing PersistenceUnitInfo [name: default] 2023-12-28T20:09:18.102-05:00 INFO 65561 --- [boot-graalvm] [ main] org.hibernate.Version : HHH000412: Hibernate ORM core version 6.4.1.Final 2023-12-28T20:09:18.163-05:00 INFO 65561 --- [boot-graalvm] [ main] o.h.c.internal.RegionFactoryInitiator : HHH000026: Second-level cache disabled 2023-12-28T20:09:18.557-05:00 INFO 65561 --- [boot-graalvm] [ main] o.s.o.j.p.SpringPersistenceUnitInfo : No LoadTimeWeaver setup: ignoring JPA class transformer 2023-12-28T20:09:19.123-05:00 INFO 65561 --- [boot-graalvm] [ main] o.h.e.t.j.p.i.JtaPlatformInitiator : HHH000489: No JTA platform available (set 'hibernate.transaction.jta.platform' to enable JTA platform integration) 2023-12-28T20:09:19.129-05:00 INFO 65561 --- [boot-graalvm] [ main] j.LocalContainerEntityManagerFactoryBean : Initialized JPA EntityManagerFactory for persistence unit 'default' 2023-12-28T20:09:19.398-05:00 WARN 65561 --- [boot-graalvm] [ main] JpaBaseConfiguration$JpaWebConfiguration : spring.jpa.open-in-view is enabled by default. Therefore, database queries may be performed during view rendering. Explicitly configure spring.jpa.open-in-view to disable this warning 2023-12-28T20:09:20.474-05:00 INFO 65561 --- [boot-graalvm] [ main] o.s.b.a.e.web.EndpointLinksResolver : Exposing 13 endpoint(s) beneath base path '/actuator' 2023-12-28T20:09:20.559-05:00 INFO 65561 --- [boot-graalvm] [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port 8080 (http) with context path '' 2023-12-28T20:09:20.576-05:00 INFO 65561 --- [boot-graalvm] [ main] c.giri.boot.graalvm.GraalVmApplication : Started GraalVmApplication in 10.156 seconds (process running for 11.266) [INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 10.57 s -- in com.giri.boot.graalvm.ApplicationStartsTest [INFO] [INFO] Results: [INFO] [INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0 [INFO] [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------

💡 TIPS

It is also helpful to print arguments in the Spring boot application:
@SpringBootApplication public class GraalVmApplication { public static void main(String[] args) { for (String arg: args) { // let's examine arguments passed System.out.println("arg: " + arg); } SpringApplication.run(GraalVmApplication.class, args); } }

Conclusion

Much of the learning comes, not from working code, but from failing code. If everything works on the first go, there isn't much learned ;)
Keep exploring!
Happy Holidays!!

References

No comments:

Post a Comment