Search code examples
javaspringspring-bootspring-boot-testjava-module

NoSuchBeanDefinitionException / UnsatisfiedDependencyException when testing Spring Boot app with Java modules


How can I mitigate that the NoSuchBeanDefinitionException and the related UnsatisfiedDependencyException occur when testing a multimode Spring Boot app that has been configured with Java 9 modules?

After adding module-info.java files to my multi module project, my Spring Boot application test started to fail:

test failure

contextLoads  Time elapsed: 0 s  <<< ERROR!
java.lang.IllegalStateException: Failed to load ApplicationContext
Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException: 
   Error creating bean with name 'second': 
   Unsatisfied dependency expressed through constructor parameter 0; 
   nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: 
   No qualifying bean of type 'com.example.first.First' available: 
   expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {}
Caused by: org.springframework.beans.factory.NoSuchBeanDefinitionException: 
   No qualifying bean of type 'com.example.first.First' available: 
   expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {}

folder structure

parent
  |
  + pom.xml
  |
  +-- first
  |     + pom.xml
  |     + src/main/java/module-info.java
  |     + src/main/java/com.example.first/First.java
  |
  +-- second
  |     + pom.xml
  |     + src/main/java/module-info.java
  |     + src/main/java/com.example.second/ApplicationConfig.java
  |     + src/main/java/com.example.second/Second.java
  |     + src/test/java/com.example.second/SecondTest.java

Parent Module

A Maven parent module containing just the parent pom file.

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.6.2</version>
        <relativePath/>
    </parent>

    <groupId>com.example</groupId>
    <artifactId>parent</artifactId>
    <version>0.1.0-SNAPSHOT</version>
    <packaging>pom</packaging>

    <properties>
        <java.version>11</java.version>
    </properties>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>com.example</groupId>
                <artifactId>first</artifactId>
                <version>${project.version}</version>
            </dependency>
            <dependency>
                <groupId>com.example</groupId>
                <artifactId>second</artifactId>
                <version>${project.version}</version>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <modules>
        <module>first</module>
        <module>second</module>
    </modules>
</project>

First Module

A simple Java module that only contains a single Java class and that does have any Spring dependencies.

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>com.example</groupId>
        <artifactId>parent</artifactId>
        <version>0.1.0-SNAPSHOT</version>
    </parent>

    <artifactId>first</artifactId>
</project>

First.java

package com.example.first;

public class First {
}

first module-info.java

module com.example.first {
    exports com.example.first;

    opens com.example.first;
}

Second Module

The application module that depends on the first module in addition to the required Spring dependencies. It its responsible for configuring Spring beans, running the application, etc.

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>com.example</groupId>
        <artifactId>parent</artifactId>
        <version>0.1.0-SNAPSHOT</version>
    </parent>

    <artifactId>second</artifactId>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

    <dependencies>
        <dependency>
            <groupId>com.example</groupId>
            <artifactId>first</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-autoconfigure</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
        </dependency>
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-engine</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-api</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.hamcrest</groupId>
            <artifactId>hamcrest-core</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
</project>

Second.java

package com.example.second;

import com.example.first.First;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Second {

    private final First first;

    public Second(First first) {
        this.first = first;
    }

    public static void main(String[] args) {
        SpringApplication.run(ApplicationConfig.class, args);
        System.out.println("started");
    }
}

ApplicationConfig.java

package com.example.second;

import com.example.first.First;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class ApplicationConfig {

    @Bean
    First first() {
        return new First();
    }
}

SecondTest.java

package com.example.second;

import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class SecondTest {

    @Test
    void contextLoads() {
    }
}

Observations

  • The test fail regardless if I use Maven, e.g. mvn test or if I attempt to debug them from within my IDE.
  • It is possible to start the application invoking the Second#main(), both from within the IDE as well as from command line using java -jar second/target/second-0.1.0-SNAPSHOT.jar so dependency injection and auto wiring works in this case (the jar file has to be built without tests in this case since they are failing, e.g. mvn package -DskipTests=true) so the First bean is created and autowired correctly in this case.
  • The test pass if both module-info.java files are deleted, so the error seem to be related to Java modules

Solution

  • Based on the feedback in the comments by @xerx593, I have implemented a workaround in which I have pulled out the @SpringBootApplication class to a separate Application class and updated the test accordingly:

      |
      +-- second
      |     + pom.xml
      |     + src/main/java/module-info.java
      |     + src/main/java/com.example.second/Application.java
      |     + src/main/java/com.example.second/ApplicationConfig.java
      |     + src/main/java/com.example.second/Second.java
      |     + src/test/java/com.example.second/ApplicationTest.java
    

    Application.java

    package com.example.second;
    
    import java.util.Arrays;
    
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.context.ConfigurableApplicationContext;
    
    
    @SpringBootApplication
    public class Application {
    
        public static void main(String[] args) {
            SpringApplication.run(Application.class, args);
        }
    }
    

    ApplicationConfig.java

    package com.example.second;
    
    import com.example.first.First;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    
    @Configuration
    public class ApplicationConfig {
    
        @Bean
        First first() {
            return new First();
        }
    }
    

    Second.java

    package com.example.second;
    
    import com.example.first.First;
    import org.springframework.stereotype.Service;
    
    @Service
    public class Second {
    
        private final First first;
    
        public Second(First first) {
            this.first = first;
        }
    }
    

    ApplicationTest.java

    package com.example.second;
    
    import org.junit.jupiter.api.Test;
    import org.springframework.boot.test.context.SpringBootTest;
    
    @SpringBootTest(classes = Application.class)
    class ApplicationTest {
    
        @Test
        void contextLoads() {
        }
    }