Friday, September 20, 2024

Java - Gotcha - Sealed interface and mocking in unit tests . . .

Seal(noun)
dictionary meaning - a device or substance that is used to join two things together so as to prevent them from coming apart or to prevent anything from passing between them. 

Prevent anything from passing between them. That's exactly what sometimes you want to put in place. When you have an interface and want to restrict other interfaces to extend or classes to implement to have a control on, you ned to seal your interface by specifying all those are permitted to extend or implement.

Java sealed interfaces is a feature introduced in Java 15 as a preview feature and became a standard feature in Java 17. Sealed interface restricts which classes or interfaces can implement or extend it. Classes that implement a sealed interface must be declared as final, sealed, or non-sealed. This provides more control over the inheritance hierarchy and helps to enforce certain design constraints.
 
To declare a sealed interface, use the sealed keyword followed by the permits clause, which lists the permitted subtypes.

E.g.
public sealed interface Shape permits Circle, Rectangle, Triangle { double area(); }

Each permitted subtype must be declared as one of the following:
  • Final: Cannot be extended further.
  • Sealed: Can specify its own permitted subtypes.
  • Non-Sealed: Removes the sealing restriction, allowing any class to extend it.
// Final class public final class Circle implements Shape { ... } // Sealed class public sealed class Rectangle implements Shape permits Square { ... } // Non-sealed class, additional permitted sub-type public final Square extends Rectangle { ... }

Benefits of Sealed Interfaces

Enhanced Control: Provide more control over the inheritance hierarchy, ensuring that only specific classes can implement the interface.
Improved Maintainability: By restricting the set of permitted subtypes, you can make your codebase easier to understand and maintain.
Better Exhaustiveness Checking: Sealed interfaces improve exhaustiveness checking in switch statements, especially when used with pattern matching (introduced in later Java versions).

The exhaustive checking in switch statement itself is very useful feature to have that makes your code not to miss handling a case of interface type in switch which otherwise is prone to bugs. The compiler would not let your code compile until all possible cases are handled in a switch statement making your code robust.

Shape aShape; ... switch(aShape) { case Circle circle -> circle.radius(); case Rectangle rectangle -> // do something // handle all remaining cases or provide default case, otherwise you code fails compilation }

Gotcha - Mockito, mocking sealed interface

Mocking is common in unit testing. If you are writing unit test for an object A that depends on object B, you will not be interested in B and can simply mock it's behavior. If Mockito is your mocking framework, and B happens to be a sealed interface with some permitted implementations, then you will not be able to mock like usually you do as follows:

class ATest { ... @Mock private B objB; ... }

Your test fails with the following error when it is run:
org.mockito.exceptions.base.MockitoException: Mockito cannot mock this class: interface B. If you're not sure why you're getting this error, please open an issue on GitHub. Java : 22 JVM vendor name : Amazon.com Inc. JVM vendor version : 22.0.1+8-FR JVM name : OpenJDK 64-Bit Server VM JVM version : 22.0.1+8-FR JVM info : mixed mode, sharing OS name : Mac OS X OS version : 13.6.6 You are seeing this disclaimer because Mockito is configured to create inlined mocks. You can learn about inline mocks and their limitations under item #39 of the Mockito class javadoc. Underlying exception : org.mockito.exceptions.base.MockitoException: Unsupported settings with this type 'B'

Solution
Change mock to a specific implementation of the interface.
private final B objB = mock(BImpl.class); // sealed interface, specify specific implementation class to be mocked

Monday, September 16, 2024

Spring Code TIP-2: Assert expected log message in a test-case . . .

Logging is helpful to know the insights of a running application. It is also useful in investigating issues. Sometimes, we would like to log certain configuration details after the application starts up. This can be achieved by implementing functional interface CommandLineRunner. Any bean that implements this interface is a special type of bean that runs after the application context is fully loaded.

CommandLineRunner is a functional interface with a single method run(String... args). Beans implementing this interface are executed after the application context is loaded and before the Spring Boot application starts.

For example the following application class defines a CommandLineRunner bean. The bean returns a lambda expression which defines the behavior of the CommandLineRunner. As an altervative to defining a bean, the MyApplication class can also implement this interface and provide implementation for run method. As shown below, this bean checks and if the jdbcClient is available, it executes a SQL query to get the database version and logs the result. This is run after the application context is fully loaded so that we know the version of the Database that the application is using. This is one such very useful information.

@SpringBootApplication @Slf4j public class MyApplication { public static void main(String[] args) { SpringApplication.run(MyApplication.class, args); } @Bean public CommandLineRunner initializationChecks(@Autowired(required = false) JdbcClient jdbcClient) { return args -> { if (jdbcClient != null) { log.info("Database check: {}", jdbcClient.sql("SELECT version()").query(String.class).single()); } }; } }

Now, say we want to assert this log message in a test-case. That way we ensure that the log message contains the expected version of the Database, and the Database won't get changed/upgraded without a test-case catching it.

Spring Code TIP - test log message

JUnit's @ExtendsWith annotation and Spring Boot's OutputCaptureExtension can be leveraged to  achieve this.

The following is an integration test, that does this:
@SpringBootTest(useMainMethod = SpringBootTest.UseMainMethod.ALWAYS) @ExtendWith(OutputCaptureExtension.class) class MyApplicationIntegrationTest { @Autowired ApplicationContext applicationContext; @Autowired MyService myService; @Test @DisplayName("An integration Smoke Test to ensure that the application context loads, autowiring works, and checks DB version.") void smokeTest_context_loads_and_autowiring_works(CapturedOutput output) { var otherService = applicationContext.getBean(OtherService.class); assertThat(otherService).isInstanceOf(OtherService.class); assertThat(myService).isNotNull(); assertThat(output).contains("Database version: PostgreSQL 15.3"); } }

Highlighted is the code in focus, assuming that the Database used is PostgreSQL 15.3.

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.