Search

Dark theme | Light theme

November 3, 2024

Mastering Maven: Disable Logging Of Progress Downloading Artifacts

When Maven needs to download artifacts from a remote repository, it logs the progress of the download. This can lead to a lot of noise in the output. Luckily, we can suppress the logging of the download progress. Since Maven 3.6.1. we can use the command-line option --no-transfer-progress to disable the logging of the download progress. There is also a short version of the option: -ntp.

First look at an example where we do not disable the logging of the download progress. In the output, we see (a lot of) messages showing which artifacts are downloaded and the progress.

$ mvn package
[INFO] Scanning for projects...
Downloading from central: https://repo.maven.apache.org/maven2/org/springframework/boot/spring-boot-starter-parent/3.3.5/spring-boot-starter-parent-3.3.5.pom
Downloaded from central: https://repo.maven.apache.org/maven2/org/springframework/boot/spring-boot-starter-parent/3.3.5/spring-boot-starter-parent-3.3.5.pom (13 kB at 29 kB/s)
Downloading from central: https://repo.maven.apache.org/maven2/org/springframework/boot/spring-boot-dependencies/3.3.5/spring-boot-dependencies-3.3.5.pom
Downloaded from central: https://repo.maven.apache.org/maven2/org/springframework/boot/spring-boot-dependencies/3.3.5/spring-boot-dependencies-3.3.5.pom (100 kB at 1.7 MB/s)
...
[INFO]
[INFO] --------------------------< com.example:demo >--------------------------
[INFO] Building demo 0.0.1-SNAPSHOT
[INFO]   from pom.xml
[INFO] --------------------------------[ jar ]---------------------------------
Downloading from central: https://repo.maven.apache.org/maven2/org/springframework/boot/spring-boot-maven-plugin/3.3.5/spring-boot-maven-plugin-3.3.5.pom
Downloaded from central: https://repo.maven.apache.org/maven2/org/springframework/boot/spring-boot-maven-plugin/3.3.5/spring-boot-maven-plugin-3.3.5.pom (4.0 kB at 308 kB/s)
Downloading from central: https://repo.maven.apache.org/maven2/org/springframework/boot/spring-boot-maven-plugin/3.3.5/spring-boot-maven-plugin-3.3.5.jar
Downloaded from central: https://repo.maven.apache.org/maven2/org/springframework/boot/spring-boot-maven-plugin/3.3.5/spring-boot-maven-plugin-3.3.5.jar (137 kB at 6.5 MB/s)
...

We remove the downloaded dependencies from our local repository and run the same Maven command again, but now we add the command-line option --no-transfer-progress (or the short version -ntp). We no longer have the downloading progress messages in our output:

$ mvn --no-transfer-progress package
[INFO] Scanning for projects...
[INFO]
[INFO] --------------------------< com.example:demo >--------------------------
[INFO] Building demo 0.0.1-SNAPSHOT
[INFO]   from pom.xml
[INFO] --------------------------------[ jar ]---------------------------------
[INFO]
...

Instead of adding this option each time we run Maven, we can add it to the file .mvn/maven.config in our project:

--no-transfer-progress

Alternatively, since Maven 3.9.0, we can add the option to the environment variable MAVEN_ARGS.

$ export MAVEN_ARGS=-ntp 
$ mvn package
[INFO] Scanning for projects...
[INFO]
...

Written with Maven 3.9.9.

October 24, 2024

Helidon SE Helpings: Default Configuration Sources During Testing

In a previous blog post we learned about the default input sources that are used by Helidon SE. The list of input sources is different based on which artifacts are on the classpath of our application. When we write tests for code in our application that uses the default configuration created by Config.create() we must take into account that different input sources are used. Also here it is based on the artifacts that are on the classpath. That means that different files with configuration data are loaded, eg. a file application-test.conf when we have the artifact helidon-config-hocon and a file application-test.yml if the artifact helidon-config-yaml is on the classpath.

If we use the artifact helidon-config then the following input sources are searched with the following order of preference:

  1. System environment variables
  2. Java system properties
  3. a file META-INF/microprofile-config-test.properties on the classpath,
  4. a file META-INF/microprofile-config.properties on the classpath.

Notice that there is no input source that looks for a file application.properties on the classpath, but when we run our application that is a valid input source. Also the order of the input sources system environment variables and Java system properties switched. So the input sources for a default configuration created with Config.create() is different during run-time and when we test our application.

In the following test we create a default configuration using Config.create() and get the configuration property app.message that is set using a system environment variable, Java system property and the file META-INF/microprofile-config-test.properties placed in src/test/resources (which will be on the classpath). In order to test with setting a system environment variable in our test code we use the library com.github.stefanbirkner:system-lambda by adding the following dependency:

<dependency>
    <groupId>com.github.stefanbirkner</groupId>
    <artifactId>system-lambda</artifactId>
    <version>1.2.1</version>
    <scope>test</scope>
</dependency>

Our properties file looks like this:

# File: src/test/resources/META-INF/microprofile-config-test.properties
app.message=Hello from classpath:META-INF/microprofile-config-test.properties

Our test class has three test methods to check the value of the configuration property app.message:

// File: src/test/java/mrhaki/helidon/DefaultConfigTest.java
package mrhaki.helidon;

import io.helidon.config.Config;
import org.junit.jupiter.api.Test;

import static com.github.stefanbirkner.systemlambda.SystemLambda.restoreSystemProperties;
import static com.github.stefanbirkner.systemlambda.SystemLambda.withEnvironmentVariable;
import static org.assertj.core.api.Assertions.assertThat;

public class DefaultConfigTest {

    @Test
    void defaultConfig() throws Exception {
        // expect
        withEnvironmentVariable("APP_MESSAGE", "Hello from environment variable")
                .execute(() -> {
                    final Config config = Config.create();

                    assertThat(config.get("app.message").asString().asOptional())
                            .hasValue("Hello from environment variable");
                });
    }

    @Test
    void withSystemProperties() throws Exception {
        restoreSystemProperties(() -> {
            System.setProperty("app.message", "Hello from Java system property");
            assertThat(Config.create().get("app.message").asString().asOptional())
                    .hasValue("Hello from Java system property");
        });
    }

    @Test
    void withConfigTestProperties() {
        // given
        final Config config = Config.create();

        // expect
        assertThat(config.get("app.message").asString().asOptional())
                .hasValue("Hello from classpath:META-INF/microprofile-config-test.properties");
    }
}

When we apply the artifact helidon-config-hocon in our pom.xml file then the following input sources are searched with the following order of preference:

  1. System environment variables,
  2. Java system properties,
  3. a file application-test.json,
  4. a file application-test.conf,
  5. classpath:application-test.json,
  6. a file application-test.conf on the classpath,
  7. a file application.json,
  8. a file application.conf,
  9. a file application.json on the classpath,
  10. a file application.conf on the classpath,
  11. a file META-INF/microprofile-config-test.properties on the classpath,
  12. a file META-INF/microprofile-config.properties.` on the classpath

If we would use the artifact helidon-config-yaml then the following input sources are searched with the following order of preference:

  1. System environment variables,
  2. Java system properties,
  3. a file application-test.yml,
  4. a file application-test.yaml,
  5. a file application-test.yml on the classpath,
  6. a file application-test.yaml on the classpath,
  7. a file application.yml,
  8. a file application.yaml,
  9. a file application.yml on the classpath,
  10. a file application.yaml on the classpath,
  11. a file META-INF/microprofile-config-test.properties on the classpath,
  12. a file META-INF/microprofile-config.properties on the classpath.

Written with Helidon SE 4.2.1.

October 17, 2024

Helidon SE Helpings: Default Configuration Sources

When we use Helidon SE we can use the Config class to pass configuration properties to our application. The static method create() creates a default configuration. The Config class is then configured to support different input sources. This configuration reads configuration properties from the following sources in order:

  1. Java system properties,
  2. system environment variables,
  3. a file on the classpath that has the name application.properties (based on default config parser that is part of the artifact helidon-config).

The last input source behaves differently based on which classes that can parse a configuration file are on the classpath of our application. If we use the helidon-config artifact on the classpath then the configuration file read is application.properties. To read a JSON formatted configuration file we must add the helidon-config-hocon artifact to the classpath. The file that is read is application.json. With the same artifact we can read a HOCON formatted configuration file that is named application.conf. Finally if we add the helidon-config-yaml artifact to the classpath we can read a YAML formatted configuration file that is named application.yaml or application.yml. Helidon SE will only read one configuration file from the classpath with the following order of preference:

  1. application.yaml or application.yml,
  2. application.conf,
  3. application.json,
  4. application.properties.

In the following example class we create a default configuration using Config.create() and we show the contents of the configuration property app.message:

// File: src/main/java/mrhaki/helidon/Application.java
package mrhaki.helidon;

import io.helidon.config.Config;

public class Application {

    public static void main(String[] args) {
        // Create the default configuration.
        // Configuration properties are read from in order:
        // - from Java system properties
        // - from system environment variables
        // - from a file on the classpath that has the name
        //   'application.properties' (based on default config
        //   parser that is part of the artifact helidon-config).
        Config config = Config.create();

        // Get the configuration property app.message.
        // If the property is not set, the fallback value
        // is defined as "Hello from application code".
        String message = config.get("app.message")
                               .asString()
                               .orElse("Hello from application code");

        // Print the value of the configuration property to System.out.
        System.out.printf("app.message = %s%n", message);
    }
}

In our pom.xml we first only have the dependency for the artifact helidon-config:

...
<dependencies>
    <dependency>
        <groupId>io.helidon.config</groupId>
        <artifactId>helidon-config</artifactId>
    </dependency>
</dependencies>
...

Let’s build our application and run it without any configuration properties and rely on the default value that we defined in our code:

$ helidon build
[INFO] Scanning for projects...
[INFO] ------------------------------------------------------------------------
[INFO] Detecting the operating system and CPU architecture
[INFO] ------------------------------------------------------------------------
[INFO] os.detected.name: osx
[INFO] os.detected.arch: x86_64
[INFO] os.detected.version: 15.0
[INFO] os.detected.version.major: 15
[INFO] os.detected.version.minor: 0
[INFO] os.detected.classifier: osx-x86_64
[INFO]
[INFO] -----------------------< mrhaki.helidon:config >------------------------
[INFO] Building config 0.0.0-SNAPSHOT
[INFO]   from pom.xml
...

$ java -jar target/config.jar
app.message = Hello from application code

Next we add the file application.properties to the directory src/main/resources. This will put the file in the JAR file we build and make it available on the classpath:

# File: src/main/resources/application.properties
app.message=Hello from 'application.properties'

When we build and run our application again we see that the value of the configuration property app.message is read from the file application.properties on the classpath:

$ helidon build
...

$ java -jar target/config.jar
app.message = Hello from 'application.properties'

Our code also support setting the configuration property using environment variables. The value set by the environment variable APP_MESSAGE will overrule the value found in application.properties:

$ APP_MESSAGE="Hello from environment variable" java -jar target/config.jar
app.message = Hello from environment variable

We can overrule the value of the environment variable by setting the configuration property using the Java system properties:

$ APP_MESSAGE="Hello from environment variable" java -Dapp.message="Hello from Java system property" -jar target/config.jar
app.message = Hello from Java system property

If we replace the artifact helidon-config with helidon-config-hocon we can read a file named application.json from the classpath. First we change our dependency in the pom.xml:

...
<dependencies>
    <dependency>
        <groupId>io.helidon.config</groupId>
        <artifactId>helidon-config-hocon</artifactId>
    </dependency>
</dependencies>
...

Next we add the file application.json in src/main/resources:

{
  "app": {
    "message": "Hello from 'aplication.json'"
  }
}

We can rebuild our application and run it to see the following output:

$ helidon build
...

$ java -jar target/config.jar
app.message = Hello from 'aplication.json'

Instead of a JSON file we can also use file with the extension .conf written in HOCON format. The following example file application.conf in src/main/resources sets the configuration property app.message:

// File: src/main/resources/application.conf
app {
    message = Hello from 'application.conf'
}

When we build and run our application we see the following output:

$ helidon build
...

$ java -jar target/config.jar
app.message = Hello from 'application.conf'

To support a configuration file with the name application.yaml or application.yml in YAML format we must add the artifact helidon-config-yaml as dependency:

...
<dependencies>
    <dependency>
        <groupId>io.helidon.config</groupId>
        <artifactId>helidon-config-yaml</artifactId>
    </dependency>
</dependencies>
...

Our example application.yaml will look like this:

# File: src/amin/resources/application.yaml
app:
  message: Hello from 'application.yaml'

For the final time we build and run the application to show the output:

$ helidon build
...

$ java -jar target/config.jar
app.message = Hello from 'application.yaml'

Written with Helidon SE 4.1.2.

October 12, 2024

Helidon SE Helpings: Starting Web Server On A Random Port

Helidon SE provides a web server using Java virtual threads. When we configure the web server we can specify a specific port number the server will listen on for incoming request. If we want to use a random port number we must specify the value 0. Helidon will then start the web server on a random port number that is available on our machine.

The following example shows how to start a web server on a random port number. We use Helidon SE to write our code:

package mrhaki.helidon;

import io.helidon.logging.common.LogConfig;
import io.helidon.webserver.WebServer;

public class Application {
    public static void main(String[] args) {
        // Load logging configuration.
        LogConfig.configureRuntime();

        // Configure web server on a random port number.
        WebServer server = WebServer.builder()
            .port(0)  // Use random port number
            .build()
            .start();

        // Print port number the server is listening on.
        System.out.println("WEB server is up at http://localhost:" + server.port());
    }
}

When we start our application we see the following output:

2024.10.11 17:27:36.005 Logging at runtime configured using classpath: /logging.properties
2024.10.11 17:27:36.606 Helidon SE 4.1.2 features: [Config, Encoding, Media, WebServer]
2024.10.11 17:27:36.621 [0x2326f965] http://0.0.0.0:61685 bound for socket '@default'
2024.10.11 17:27:36.639 Started all channels in 28 milliseconds. 863 milliseconds since JVM startup. Java 21.0.4+7-LTS
WEB server is up at http://localhost:61685

The next time we start our application we see a different port number:

2024.10.11 17:28:11.283 Logging at runtime configured using classpath: /logging.properties
2024.10.11 17:28:11.852 Helidon SE 4.1.2 features: [Config, Encoding, Media, WebServer]
2024.10.11 17:28:11.873 [0x28b386dd] http://0.0.0.0:61698 bound for socket '@default'
2024.10.11 17:28:11.892 Started all channels in 41 milliseconds. 835 milliseconds since JVM startup. Java 21.0.4+7-LTS
WEB server is up at http://localhost:61698

We can also use the Helidon Configuration API to configure the web server to use a random port number. We can for example set the port number to 0 in the application.yaml file. In the following example we initialize standard configuration and use it configure the webserver:

package mrhaki.helidon;

import io.helidon.config.Config;
import io.helidon.logging.common.LogConfig;
import io.helidon.webserver.WebServer;

public class Application {
    public static void main(String[] args) {
        // Load logging configuration.
        LogConfig.configureRuntime();

        // Initialize the configuration.
        Config config = Config.create();
        Config.global(config);

        // Configure web server on a random port number.
        WebServer server = WebServer.builder()
            .config(config.get("server"))  // Get port number from configuration.
            .build()
            .start();

        // Print port number the server is listening on.
        System.out.println("WEB server is up at http://localhost:" + server.port());
    }
}

With our new configuration we can use an environment variable SERVER_PORT to set the port number to 0 for our web server. The configuration could also be defined in an application.yaml file:

server:
  port: 0

Written with Helidon SE 4.2.1.

October 10, 2024

Spring Sweets: Using Duration Type With Configuration Properties

With @ConfigurationProperties in Spring Boot we can bind configuration properties to Java classes. The class annotated with @ConfigurationProperties can be injected into other classes and used in our code. We can use the type Duration to configure properties that express a duration. When we set the value of the property we can use:

  • a long value with the unit to express milliseconds,
  • a value following the ISO-8601 duration format,
  • a special format supported by Spring Boot with the value and unit.

We can also use the @DurationUnit annotation to specify the unit for a long value. So instead of the default milliseconds we can specify the unit to be seconds or minutes for example. The unit is defined by the java.time.temporal.ChronoUnit enum and we pass it as an argument to the annotation.

The special format supported by Spring Boot supports the following units:

  • ns for nanoseconds,
  • us for microseconds,
  • ms for milliseconds,
  • s for seconds,
  • m for minutes,
  • h for hours,
  • d for days.

In the following example we define a record TimeoutProperties annotated with @ConfigurationProperties and four properties of type Duration. The property idleTimeout has the @DurationUnit annotation to specify the unit to be seconds.

package mrhaki;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.convert.DurationUnit;

import java.time.Duration;
import java.time.temporal.ChronoUnit;

@ConfigurationProperties(prefix = "timeout")
public record TimeoutProperties(
    Duration connectionTimeout,
    Duration readTimeout,
    Duration writeTimeout,
    @DurationUnit(ChronoUnit.SECONDS) Duration idleTimeout
) {}

In our application.properties file we can set the values of the properties:

# long value (in milliseconds)
timeout.connection-timeout=5000

# ISO-8601 format
timeout.read-timeout=PT30S

# Spring Boot's format
timeout.write-timeout=1m

# value in seconds (due to @DurationUnit annotation)
timeout.idle-timeout=300

In the following test we test the values of the properties in the class TimeoutProperties.

package mrhaki;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.TestPropertySource;

import java.time.Duration;

import static org.assertj.core.api.Assertions.assertThat;

@SpringBootTest
@TestPropertySource(properties = {
    "timeout.connection-timeout=5000", // use long value in milliseconds
    "timeout.read-timeout=PT30S", // use ISO-8601 duration format
    "timeout.write-timeout=1m", // use special format supported by Spring Boot
    "timeout.idle-timeout=300" // use long value in seconds (set by @DurationUnit)
})
class TimeoutPropertiesTest {

    @Autowired
    private TimeoutProperties timeoutProperties;

    @Test
    void testConnectionTimeout() {
        assertThat(timeoutProperties.connectionTimeout())
            .isEqualTo(Duration.ofMillis(5000))
            .isEqualTo(Duration.ofSeconds(5));
    }

    @Test
    void testReadTimeout() {
        assertThat(timeoutProperties.readTimeout())
            .isEqualTo(Duration.ofSeconds(30));
    }

    @Test
    void testWriteTimeout() {
        assertThat(timeoutProperties.writeTimeout())
            .isEqualTo(Duration.ofMinutes(1))
            .isEqualTo(Duration.ofSeconds(60));
    }


    @Test
    void testIdleTimeout() {
        assertThat(timeoutProperties.idleTimeout())
            .isEqualTo(Duration.ofSeconds(300))
            .isEqualTo(Duration.ofMinutes(5));
    }

    @Test
    void testTimeoutToMillis() {
        assertThat(timeoutProperties.connectionTimeout().toMillis()).isEqualTo(5000);
        assertThat(timeoutProperties.readTimeout().toMillis()).isEqualTo(30000);
        assertThat(timeoutProperties.writeTimeout().toMillis()).isEqualTo(60000);
        assertThat(timeoutProperties.idleTimeout().toMillis()).isEqualTo(300000);
    }
}

Written with Spring Boot 3.4.4.

October 8, 2024

Java Joy: Using JShell With Standard Input

The Java Development Kit (JDK) includes a tool called jshell that can be used to interactively test Java code. Normally we run jshell and type Java code to be executed from an interactive shell. But we can also use jshell as a tool on the command line to accept standard input containing Java code to be executed. The output of the code can be used as input for another tool on the command line. We run jshell - to run jshell and accept standard input. The simplest way to pass Java code to jshell is to use echo to print the Java code to standard output and pipe it to jshell.

The following example shows how to use jshell and echo to get the default time zone of our system:

$ echo 'System.out.println(TimeZone.getDefault().getID());' | jshell -
Europe/Amsterdam

In the following example we fetch all Java system properties using jshell and then use grep to find the system properties with file in the name:

$ echo 'System.getProperties().list(System.out)' | jshell - | grep file
file.separator=/
file.encoding=UTF-8

The next example uses multi-line standard input:

$ jshell - << EOF
int square(int n) {
    return n * n;
}

IntStream.range(1, 10).map(n -> square(n)).forEach(System.out::println);
EOF

1
4
9
16
25
36
49
64
81

Written with Java 21.0.4.

October 6, 2024

Spring Sweets: Using Configuration Properties With DataSize

With @ConfigurationProperties in Spring Boot we can bind configuration properties to Java classes. The class annotated with @ConfigurationProperties can be injected into other classes and used in our code. We can use the type DataSize to configure properties that express a size in bytes. When we set the value of the property we can use a long value. The size is then in bytes as that is the default unit. We can also add a unit to the value. Valid units are B for bytes, KB for kilobytes, MB for megabytes, GB for gigabytes and TB for terabytes.

We can also use the @DataSizeUnit annotation to specify the unit of the property in our class annotated with @ConfigurationProperties. In that case a the value without a unit assigned to the property is already in the specified unit.

In the following example we define a record StorageProperties annotated with @ConfigurationProperties and two properties maxFileSize and cacheSize. The property maxFileSize has no unit assigned to the value so it will be in the default unit of bytes. The property cacheSize has the unit MB assigned so the value will be in megabytes.

package mrhaki;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.convert.DataSizeUnit;
import org.springframework.util.unit.DataSize;
import org.springframework.util.unit.DataUnit;

@ConfigurationProperties(prefix = "storage")
public record StorageProperties(
    DataSize maxFileSize /* Default unit is bytes */,
    @DataSizeUnit(DataUnit.MEGABYTES) DataSize cacheSize /* Explicit unit is megabytes */
) {}

In the following test we test the values of the properties in the class StorageProperties. We set the value of the maxFileSize property to 10MB with an explicit unit and the value of the cacheSize property without an explicit unit. We also test the values of the properties in bytes.

package mrhaki;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.TestPropertySource;
import org.springframework.util.unit.DataSize;
import org.springframework.util.unit.DataUnit;

import static org.assertj.core.api.Assertions.assertThat;

@SpringBootTest
@TestPropertySource(properties = {
    "storage.max-file-size=10MB", // value with explicit unit
    "storage.cache-size=500" // value without explicit unit
})
class StoragePropertiesTest {

    @Autowired
    private StorageProperties storageProperties;

    @Test
    void testMaxFileSize() {
        assertThat(storageProperties.maxFileSize())
            .isNotNull()
            // The DataSize class has useful static methods
            // to get values for a certain unit.
            .isEqualTo(DataSize.ofMegabytes(10));
    }

    @Test
    void testCacheSize() {
        assertThat(storageProperties.cacheSize())
            .isNotNull()
            .isEqualTo(DataSize.of(500, DataUnit.MEGABYTES));
    }

    @Test
    void testValuesInBytes() {
        // We can use the toBytes() method to get the actual bytes.
        assertThat(storageProperties.maxFileSize().toBytes())
            .isEqualTo(10 * 1024 * 1024);
        assertThat(storageProperties.cacheSize().toBytes())
            .isEqualTo(500 * 1024 * 1024);
    }
}

Written with Spring Boot 3.3.4.

October 1, 2024

Awesome AssertJ: Using Our Own Assertions Class

AssertJ already provides many useful assertions for all kind of types. But sometimes we want to define our own assertions for our own types. We can define new assertions by extending the AbstractAssert class In this class we add methods that will check the values of our type. The names of the methods can reflect the domain model of our type. This can make our tests more readable and understandable.

The abstract class AbstractAssert has a constructor that takes two arguments. We must pass the object we want to test and the class of the assertion class. The object that is passed is assigned to the property actual of the class. It is good practice to add a static method assertThat to the class that will return a new instance of the assertion class. Next we can add our own methods to the class. Within these methods we write the code to assert the values of our type. If the assertion would fail we can use the failWithMessage method to provide a message to the user.

AbstractAssert has also useful methods like isNotNull and isNotEqualTo that we can use in our assertions.

In the following example we write a custom assertion class for the type Pirate. The class Pirate has a nested object of type Ship. We define these with the following two records:

// File: mrhaki/Ship.java
package mrhaki;

public record Ship(String name, String type, int crewSize) {}
// File: mrhaki/Pirate.java
package mrhaki;

public record Pirate(String name, String rank, Ship ship) {}

Next we create our custom assertion class PirateAssert. We extend the class AbstractAssert, add a static method assertThat and the two assertion methods hasName and belongsOnShipWithName:

// File: mrhaki/PirateAssert.java
package mrhaki;

import org.assertj.core.api.AbstractAssert;

import java.util.Objects;

/**
 * Custom assertion methods for a Pirate instance.
 */
public class PirateAssert extends AbstractAssert<PirateAssert, Pirate> {
    protected PirateAssert(Pirate pirate) {
        super(pirate, PirateAssert.class);
    }

    /**
     * Method to create a new instance of PirateAssert, using the
     * same naming as standard AssertJ methods.
     * @param pirate Pirate instance to assert.
     * @return New PirateAssert instance.
     */
    public static PirateAssert assertThat(Pirate pirate) {
        return new PirateAssert(pirate);
    }

    /**
     * Check if the given name is equal to the actual
     * pirate's name.
     * @param name Expected name of the pirate.
     * @return Current instance for fluent API.
     */
    public PirateAssert hasName(String name) {
        // First check the actual Pirate instance is not null.
        isNotNull();

        // The Pirate instance passed in the assertThat method
        // is assigned to a variable with the name actual.
        // For comparison, we need to use that name.
        if (!Objects.equals(actual.name(), name)) {
            // Create an assertion failure with a message
            // that will be shown when the name is not equal to the actual name.
            failWithMessage("Expected pirate's name to be <%s> but was <%s>", name, actual.name());
        }

        // For fluent API usage we return this.
        return this;
    }

    /**
     * Check if the given name is equal to the name of the ship
     * the pirate belongs to.
     * @param name Expected name of the ship.
     * @return Current instance for fluent API.
     */
    public PirateAssert belongsOnShipWithName(String name) {
        isNotNull();

        if (Objects.isNull(actual.ship())) {
            failWithMessage("Expected pirate to belong on ship with name <%s> but was not found on any ship", name);
        }

        if (!Objects.equals(actual.ship().name(), name)) {
            failWithMessage("Expected pirate to belong on ship with name <%s> but was <%s>", name, actual.ship().name());
        }

        return this;
    }
}

In the following test we use the custom assertion class PirateAssert to assert the values of a Pirate instance:

// File: mrhaki/CustomPirateAssertion.java
package mrhaki;

import org.junit.jupiter.api.Test;

import static mrhaki.PirateAssert.assertThat;

public class CustomPirateAssertion {

    @Test
    void checkPirate() {
        // given
        Ship ship = new Ship("Black Pearl", "Galleon", 100);
        Pirate pirate = new Pirate("Jack Sparrow", "Captain", ship);


        // expect
        assertThat(pirate).hasName("Jack Sparrow")
                          .belongsOnShipWithName("Black Pearl");
    }
}

Written with AssertJ 3.26.3.

September 30, 2024

Awesome AssertJ: Comparing Objects Recursively

To compare nested objects we can use the usingRecursiveComparison() method in AssertJ. We can set up the nested objects with values we expect, invoke a method that would return the actual nested objects, and then use the usingRecursiveComparison() method to compare the actual nested objects with the expected nested objects. This is a very clean way to compare nested objects. Also when we would add a new property to the nested objects our test would fail as we didn’t use that new property yet for our expected nested objects.

In the following example test we use the usingRecursiveComparison() method to compare actual nested objects with the expected nested objects. Our nested objects are of type Pirate and Ship.

// File: mrhaki/Ship.java
package mrhaki;

public record Ship(String name, String type, int crewSize) {}
// File: mrhaki/Pirate.java
package mrhaki;

public record Pirate(String name, String rank, Ship ship) {}

This is our class we want to test. The class PirateShipCreator creates the nested objects we want to write assertions for.

// File: mrhaki/PirateShipCreator.java
package mrhaki;

public class PirateShipCreator {
    public static Pirate createJackSparrow() {
        return new Pirate("Jack Sparrow", "Captain", createBlackPearl());
    }

    public static Pirate createDavyJones() {
        return new Pirate("Davy Jones", "Captain", createFlyingDutchmain());
    }

    private static Ship createBlackPearl() {
        return new Ship("Black Pearl", "Galleon", 100);
    }

    private static Ship createFlyingDutchmain() {
        return new Ship("Flying Dutchman", "Ghost ship", 199);
    }
}

The test class PirateShipCreatorTest uses the usingRecursiveComparison() method to compare actual nested objects with the expected nested objects:

// File: mrhaki/PirateShipCreatorTest.java
package mrhaki;

import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;

public class PirateShipCreatorTest {

    @Test
    public void testPirateEquality() {
        // given
        Ship expectedShip = new Ship("Black Pearl", "Galleon", 100);
        Pirate expectedPirate = new Pirate("Jack Sparrow", "Captain", expectedShip);

        // when
        Pirate actualPirate = PirateShipCreator.createJackSparrow();

        // then
        // assert equality using recursive comparison
        assertThat(actualPirate)
            .usingRecursiveComparison()
            .isEqualTo(expectedPirate);
    }
}

If we want to ignore a property we can use the ignoringFields(String…​) method. Or if we want to ignore properties of a certain type we can use the ignoringFieldsOfTypes(Class<?>…​) method. This can be very useful for properties that store dates we cannot setup properly in our tests.

Instead of ignoring fields we can also specify which fields we want to compare with the comparingOnlyFields(String…​) method. And there is a comparingOnlyFieldsOfTypes(Class<?>…​) method to specify which fields of a certain type we want to compare.

In the following tests we use all four methods to ignore or include fields in our comparison.

// File: mrhaki/PirateShipCreatorTest.java
package mrhaki;

import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;

public class PirateShipCreatorTest {

    @Test
    public void testPirateEqualityIgnoringShipCrewSize() {
        // given
        Ship expectedShip = new Ship("Flying Dutchman", "Ghost Ship", 100);
        Pirate expectedPirate = new Pirate("Davy Jones", "Captain", expectedShip);

        // when
        Pirate actualPirate = PirateShipCreator.createDavyJones();

        // then
        // assert equality using recursive comparison, ignoring crew size
        assertThat(actualPirate)
            .usingRecursiveComparison()
            .ignoringFields("ship.crewSize")
            .isEqualTo(expectedPirate);
    }

    @Test
    public void testPirateEqualityIgnoringIntegerFields() {
        // given
        Ship expectedShip = new Ship("Flying Dutchman", "Ghost Ship", 100);
        Pirate expectedPirate = new Pirate("Davy Jones", "Captain", expectedShip);

        // when
        Pirate actualPirate = PirateShipCreator.createDavyJones();

        // then
        // assert equality using recursive comparison, ignoring integer fields
        assertThat(actualPirate)
            .usingRecursiveComparison()
            .ignoringFieldsOfType(Integer.class)
            .isEqualTo(expectedPirate);
    }

    @Test
    public void testPirateEqualityComparingSelectedFields() {
        // given
        Ship expectedShip = new Ship("Flying Dutchman", "Ghost Ship", 100);
        Pirate expectedPirate = new Pirate("Davy Jones", "Captain", expectedShip);

        // when
        Pirate actualPirate = PirateShipCreator.createDavyJones();

        // then
        // assert equality using recursive comparison, comparing only selected fields
        assertThat(actualPirate)
            .usingRecursiveComparison()
            .comparingOnlyFields("name", "rank", "ship.name", "ship.type")
            .isEqualTo(expectedPirate);
    }

    @Test
    public void testPirateEqualityComparingSelectedTypeOfFields() {
        // given
        Ship expectedShip = new Ship("Flying Dutchman", "Ghost Ship", 100);
        Pirate expectedPirate = new Pirate("Davy Jones", "Captain", expectedShip);

        // when
        Pirate actualPirate = PirateShipCreator.createDavyJones();

        // then
        // assert equality using recursive comparison,
        // comparing only fields of type String and Ship
        assertThat(actualPirate)
            .usingRecursiveComparison()
            .comparingOnlyFieldsOfTypes(String.class, Ship.class)
            .isEqualTo(expectedPirate);
    }
}

Written with AssertJ 3.26.3.

May 21, 2024

Clojure Goodness: Extending is Macro With Custom Assertions

The is macro in the clojure.test namespace can be used to write assertions about the code we want to test. Usually we provide a predicate function as argument to the is macro. The prediction function will call our code under test and return a boolean value. If the value is true the assertion passes, if it is false the assertion fails. But we can also provide a custom assertion function to the is macro. In the clojure.test package there are already some customer assertions like thrown? and instance?. The assertions are implemented by defining a method for the assert-expr multimethod that is used by the is macro. The assert-expr multimethod is defined in the clojure.test namespace. In our own code base we can define new methods for the assert-expr multimethod and provide our own custom assertions. This can be useful to make tests more readable and we can use a language in our tests that is close to the domain or naming we use in our code.

The implementation of the custom assertion should call the function do-report with a map containing the keys :type, :message, :expected and :actual. The :type key can have the values :fail or :pass. Based on the code we write in our assertion we can set the value correctly. Mostly the :message key will have the value of the message that is defined with the is macro in our tests. The keys :expected and :actual should contain reference to what the assertion expected and the actual result. This can be a technical reference, but we can also make it a human readable reference.

In the following example we implement a new customer assertion jedi? that checks if a given name is a Jedi name. The example is based on an example that can be found in the AssertJ documentation.

(ns mrhaki.test
  (:require [clojure.test :refer [deftest is are assert-expr]]))

(defmethod assert-expr 'jedi?
  "Assert that a given name is a Jedi."
  [msg form]
  `(let [;; We get the name that is the second element in the form.
         ;; The first element is the symbol `'jedi?`.
         name# ~(nth form 1)
         ;; We check if the name is part of a given set of Jedi names.
         result# (#{"Yoda" "Luke" "Obiwan"} name#)
         ;; We create an expected value that is used in the assertion message.
         expected# (str name# " to be a jedi.")]
     (if result#
       (do-report {:type     :pass
                   :message  ~msg,
                   :expected expected#
                   :actual   (str name# " is actually a jedi.")})
       (do-report {:type     :fail
                   :message  ~msg,
                   :expected expected#
                   :actual   (str name# " is NOT a jedi.")}))
     result#))

;; We can use our custom assertion in our tests.
(deftest jedi
  (is (jedi? "Yoda")))

;; The custom assertion can also be used with
;; the are macro as it will expand into multiple
;; is macro calls.
(deftest multiple-jedi
  (are [name] (jedi? name)
    "Yoda" "Luke" "Obiwan"))

;; The following test will fail, so we can
;; see failure message with the :expected and :actual values.
(deftest fail-jedi
  (is (jedi? "R2D2") "Is it?"))

If we run our failing test we see in the output that the assertion message is using our definition of the expected and actual values:

...
 expected: "R2D2 to be a jedi."
   actual: "R2D2 is NOT a jedi."
...

Written with Clojure 1.11.3.