Sunday, September 15, 2024

Spring Code TIP-1: Get code coverage for the main method in Spring Boot application . . .

Setting up a new Spring boot project is made trivial by the Spring Initializer. IDEs like IntelliJ has integrated support for this as well. The application generated by this initializer contains three files under src.
    1) The application file (e.g. DemoAppliction.java) is Spring Boot main class that bootstraps the project.
    2) A properties file (application.properties) is the application configuration file.
    3) A test-case file (DemoApplictionTests.java). 

The main application file looks like:
package com.example.demo; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class DemoApplication { public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } }
The SpringApplication.run(DemoApplication.class, args) performs bootstrapping, creates application context, and runs the application.

The configuration file looks like:
spring.application.name=demo

The test-case looks like:
package com.example.demo; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; @SpringBootTest class DemoApplicationTests { @Test void contextLoads() { } }
This is in fact a simple yet very useful test class and I would keep it around. The is an integration test-case that ensures that the application context loads successfully. If there are any issues this test fails. So, this can be treated like a integration smoke-test for your application. I would rename that test method name to smokeTest_contextLoads().

Spring Code TIP - Code coverage

However, once you progress on with the application and have code coverage check applied using build system like maven and its plugins Surefire, Failsafe and JaCoCo plugins, the main method is left uncovered by this test-case. Little strange!

Having a main method in the application supported by integrated test-case, one expects that the main method is invoked by the test-case. The annotation @SpringBootTest by default has this turned off. Hence @SpringBootTest calls SpringApplication.run(DemoApplication.class, args) and not the main method. So, in order to get the main method called instead when the test-case is run, we need to explicitly set a property. Once this is set, the test-case invokes the main method and the main method gets the code coverage.

The property is shown below to get the test coverage for the main method.
@SpringBootTest(useMainMethod = SpringBootTest.UseMainMethod.ALWAYS)

Not just for the code coverage, you might also want to use the useMainMethod property in scenarios where your application's main method performs additional setup or configuration that is necessary for your tests as well.

Summary

The useMainMethod property of the @SpringBootTest annotation allows you to control whether the main method of your application is invoked to start the Spring Boot application context during testing. By default, useMainMethod is set to UseMainMethod.NEVER, but you can set it to UseMainMethod.ALWAYS or UseMainMethod.WHEN_AVAILABLE to ensure that the main method is called during testing.

Wednesday, August 21, 2024

Spring Data JPA limitation with LIMIT fetch . . .

In modern Java Spring based applications Spring Data JPA is quite common way to interface with database. Domain/Business objects carry persistable state of the business process. With few JPA annotations, POJOs can be enhanced to persistable domain objects. Unlike Grails framework that underpins HIBERNATE, leverages GORM and elevates and enriches domain objects to the higher level by making them persistence aware, Spring Data JPA keeps the persistence in another abstraction layer called Repository.

With Spring Data JPA, Repository is the central interface and it requires one be familiar with Repository abstractions. Queries can be defined as interface methods and implementation is provided by Spring Data JPA framework by 1) deriving from method naming conventions 2) using manually defined queries with @Query annotation by writing JPQL or native SQL queries. My first choice is interface method naming by following the conventions. Next is JPQL. I avoid native queries unless there is a strong reason for.

JPQL Limitation with LIMIT fetch

One of the limitations I ran into recently with JPQL was limiting query results to limit fetching to limited number of records, say one record from the query results. Typically in native SQL this is done by adding LIMIT clause by specifying LIMIT 1 to limit to the first result to fetch. JPQL lets you specify LIMIT which also works, but under the covers the LIMIT is applied in memory to the results fetched. In other words the LIMIT clause doesn't exist in the generated native SQL. So, the SQL fetches all the results that match the criteria and a collection of entity objects get created and then the LIMIT is applied to get one object. With this the JPQL query does it's job as specified but will incur into expensive query by fetching more than needed records and creating the objects in collection and then returning one object by considering the LIMIT 1.

So, an example Repository method annotated like the following would return one object, but fetches all records that match the criteria into memory and return the first one from the collection.
@Query(""" SELECT msg FROM Message msg WHERE msg.type = :type ORDER BY msg.createdOn DESC LIMIT 1 """) Optional<Message> findLatestByType(MessageType type);

In order to truly fetch the most recent message of a given type the JPQL needs to be optimized to fetch only one record.

With JPQL the query may need to be rewritten something like the following without using LIMIT, assuming id is primary key, and is a sequence. It is more performant with no additional index created than using createdOn auditable column if there is one.
@Query(""" SELECT msg FROM Message msg WHERE msg.id = ( SELECT MAX(m.id) FROM Message m WHERE m.type = :type ) """) Optional<Message> findLatestByType(MessageType type);

The last resort is by writing a native query and using LIMIT 1 to fetch one.

References


Monday, May 20, 2024

Spring Boot logs in JSON format, assert logged output in a test-case . . .

Development is fun and sometimes frustrating too. Everything comes with some kind or other issues attached.

Scenario

Lately, I had to switch Spring Boot out-of-the-box Logback to Log4j2 logging, and specifically to JSON format. One of the test-cases that I had written was to test a feature-flag-based conditional scenario. The conditional code that depends on the feature-flag which is exposed as an external property and injected via @ConfigurationProperties bean into a service, when disabled writes a log message at WARN level to indicate that the feature is disabled. The unit-test has a test case written to test the disabled scenario which also tests the expected log message by leveraging Spring provided OutputCaptureExtension. This annotation, when used at test class level or method level like: @ExtendWith(OutputCaptureExtension.class) makes output log available to the test-case for verification.

That test-case when I switched logging to JSON using Log4j2 failed due to output log unavailable.

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

This post is about few things learned along the way: Log4j2 JSON logging, Spring Boot's JUnit Jupiter extension to capture System output, and the Log4j2 JSON Layout property to let the output be captured and available.

JSON logs

To switch Spring boot application logging to JSON using Log4j2 the following dependencies need to be added to maven build file pom.xml. Also, make sure to run mvn dependency:tree and see if you see spring-boot-starter-logging. If you have that as a transitive dependency, exclude it from all dependencies that bring it in. In the following dependencies, I had Spring Modulith that I had to exclude it from.

<dependency> <groupId>org.springframework.modulith</groupId> <artifactId>spring-modulith-starter-core</artifactId> <exclusions> <exclusion> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-logging</artifactId> </exclusion> </exclusions> </dependency> <!-- logging --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-log4j2</artifactId> </dependency> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-layout-template-json</artifactId> </dependency> <dependency> <groupId>com.fasterxml.jackson.dataformat</groupId> <artifactId>jackson-dataformat-yaml</artifactId> </dependency>

Add the following configuration file: main/resources/log4j2.yml. Log4j2 supports XML and YML:

Configuration: name: default Appenders: Console: name: console_appender target: SYSTEM_OUT follow: true JSONLayout: compact: true objectMessageAsJsonObject: true eventEol: true stacktraceAsString: true properties: true KeyValuePair: - key: '@timestamp' value: '$${date:yyyy-MM-dd HH:mm:ss.SSS}' Loggers: Root: name: root.logger level: info AppenderRef: ref: console_appender

With the above changes, application logs will be in JSON format.

Assert Captured output in a test-case

If you have any test-case that was verifying the captured log output in a test-case, it would fail.

For instance, I had a test-case like the following which was verifying the log message logged by myService.method(). To make it work, the above highlighted property - follow: true needs to be added in the log4j2.yml configuration. The details about this console appender property is documented in the OutputCaptureExtension Java doc

import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.boot.test.system.CapturedOutput; import org.springframework.boot.test.system.OutputCaptureExtension; import org.springframework.test.util.ReflectionTestUtils; import static org.assertj.core.api.Assertions.*; import static org.junit.jupiter.api.Assertions.assertAll; import static org.mockito.Mockito.verify; @ExtendWith(MockitoExtension.class) @ExtendWith(OutputCaptureExtension.class) class MyFeatureFlagTest { @InjectMocks private MyService myService; @Test void call_should_not_handleEvent_when_featureFlag_is_disabled(CapturedOutput output) { // given: mocked behavior ReflectionTestUtils.setField(myService, "featureFlag", false); // when: method under test is called myService.method(); // verify: the output captured assertAll( () -> assertThat(output).contains("The featureFlag is DISABLED."); } }

That's it.

References