Spring 引导集成测试抛出错误并且无法在另一个 Maven 模块中获取现有 bean

Spring Boot Integration test throwing error and not able to pick up an existing bean in another maven module

我有以下基于maven的项目结构

product-app
    product-web
    product-service
    product-integration-tests

有一个 MailClientService 服务写在 product-service 模块中,我想对其进行集成测试。集成测试显然是在 product-integration-tests 模块中编写的。

product-web module 已经作为依赖添加到 product-integration-tests 模块的 pom 中。

然而,问题是在集成测试的运行期间,它无法构建 MailClientService bean 并抛出运行时异常。

异常

13:53:01.161 [main] ERROR org.springframework.test.context.TestContextManager - Caught exception while allowing TestExecutionListener [org.springframework.boot.test.autoconfigure.AutoConfigureReportTestExecutionListener@20b2475a] to prepare test instance [com.radial.hostedpayments.service.MailClientServiceIntegrationTest@7857fe2]
org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'com.radial.hostedpayments.service.MailClientServiceIntegrationTest': Unsatisfied dependency expressed through field 'mailClientService': No qualifying bean of type [com.radial.hostedpayments.service.MailClientService] found for dependency [com.radial.hostedpayments.service.MailClientService]: expected at least 1 bean which qualifies as autowire candidate for this dependency. Dependency annotations: {@org.springframework.beans.factory.annotation.Autowired(required=true)}; nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type [com.radial.hostedpayments.service.MailClientService] found for dependency [com.radial.hostedpayments.service.MailClientService]: expected at least 1 bean which qualifies as autowire candidate for this dependency. Dependency annotations: {@org.springframework.beans.factory.annotation.Autowired(required=true)}
        at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.inject(AutowiredAnnotationBeanPostProcessor.java:569) ~[spring-beans-4.3.2.RELEASE.jar:4.3.2.RELEASE]
        at org.springframework.beans.factory.annotation.InjectionMetadata.inject(InjectionMetadata.java:88) ~[spring-beans-4.3.2.RELEASE.jar:4.3.2.RELEASE]
        at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.postProcessPropertyValues(AutowiredAnnotationBeanPostProcessor.java:349) ~[spring-beans-4.3.2.RELEASE.jar:4.3.2.RELEASE]
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.populateBean(AbstractAutowireCapableBeanFactory.java:1214) ~[spring-beans-4.3.2.RELEASE.jar:4.3.2.RELEASE]
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.autowireBeanProperties(AbstractAutowireCapableBeanFactory.java:385) ~[spring-beans-4.3.2.RELEASE.jar:4.3.2.RELEASE]
        at org.springframework.test.context.support.DependencyInjectionTestExecutionListener.injectDependencies(DependencyInjectionTestExecutionListener.java:118) ~[spring-test-4.3.2.RELEASE.jar:4.3.2.RELEASE]
        at org.springframework.test.context.support.DependencyInjectionTestExecutionListener.prepareTestInstance(DependencyInjectionTestExecutionListener.java:83) ~[spring-test-4.3.2.RELEASE.jar:4.3.2.RELEASE]
        at org.springframework.boot.test.autoconfigure.AutoConfigureReportTestExecutionListener.prepareTestInstance(AutoConfigureReportTestExecutionListener.java:46) ~[spring-boot-test-autoconfigure-1.4.0.RELEASE.jar:1.4.0.RELEASE]
        at org.springframework.test.context.TestContextManager.prepareTestInstance(TestContextManager.java:230) [spring-test-4.3.2.RELEASE.jar:4.3.2.RELEASE]
        at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.createTest(SpringJUnit4ClassRunner.java:228) [spring-test-4.3.2.RELEASE.jar:4.3.2.RELEASE]
        at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runReflectiveCall(SpringJUnit4ClassRunner.java:287) [spring-test-4.3.2.RELEASE.jar:4.3.2.RELEASE]
        at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12) [junit-4.12.jar:4.12]
        at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.methodBlock(SpringJUnit4ClassRunner.java:289) [spring-test-4.3.2.RELEASE.jar:4.3.2.RELEASE]
        at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:247) [spring-test-4.3.2.RELEASE.jar:4.3.2.RELEASE]
        at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:94) [spring-test-4.3.2.RELEASE.jar:4.3.2.RELEASE]
        at org.junit.runners.ParentRunner.run(ParentRunner.java:290) [junit-4.12.jar:4.12]
        at org.junit.runners.ParentRunner.schedule(ParentRunner.java:71) [junit-4.12.jar:4.12]
        at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288) [junit-4.12.jar:4.12]
        at org.junit.runners.ParentRunner.access[=12=]0(ParentRunner.java:58) [junit-4.12.jar:4.12]
        at org.junit.runners.ParentRunner.evaluate(ParentRunner.java:268) [junit-4.12.jar:4.12]
        at org.springframework.test.context.junit4.statements.RunBeforeTestClassCallbacks.evaluate(RunBeforeTestClassCallbacks.java:61) [spring-test-4.3.2.RELEASE.jar:4.3.2.RELEASE]
        at org.springframework.test.context.junit4.statements.RunAfterTestClassCallbacks.evaluate(RunAfterTestClassCallbacks.java:70) [spring-test-4.3.2.RELEASE.jar:4.3.2.RELEASE]
        at org.junit.runners.ParentRunner.run(ParentRunner.java:363) [junit-4.12.jar:4.12]
        at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.run(SpringJUnit4ClassRunner.java:191) [spring-test-4.3.2.RELEASE.jar:4.3.2.RELEASE]
        at org.apache.maven.surefire.junit4.JUnit4Provider.execute(JUnit4Provider.java:367) [surefire-junit4-2.19.1.jar:2.19.1]
        at org.apache.maven.surefire.junit4.JUnit4Provider.executeWithRerun(JUnit4Provider.java:274) [surefire-junit4-2.19.1.jar:2.19.1]
        at org.apache.maven.surefire.junit4.JUnit4Provider.executeTestSet(JUnit4Provider.java:238) [surefire-junit4-2.19.1.jar:2.19.1]
        at org.apache.maven.surefire.junit4.JUnit4Provider.invoke(JUnit4Provider.java:161) [surefire-junit4-2.19.1.jar:2.19.1]
        at org.apache.maven.surefire.booter.ForkedBooter.invokeProviderInSameClassLoader(ForkedBooter.java:290) [surefire-booter-2.19.1.jar:2.19.1]
        at org.apache.maven.surefire.booter.ForkedBooter.runSuitesInProcess(ForkedBooter.java:242) [surefire-booter-2.19.1.jar:2.19.1]
        at org.apache.maven.surefire.booter.ForkedBooter.main(ForkedBooter.java:121) [surefire-booter-2.19.1.jar:2.19.1]
Caused by: org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type [com.radial.hostedpayments.service.MailClientService] found for dependency [com.radial.hostedpayments.service.MailClientService]: expected at least 1 bean which qualifies as autowire candidate for this dependency. Dependency annotations: {@org.springframework.beans.factory.annotation.Autowired(required=true)}
        at org.springframework.beans.factory.support.DefaultListableBeanFactory.raiseNoMatchingBeanFound(DefaultListableBeanFactory.java:1406) ~[spring-beans-4.3.2.RELEASE.jar:4.3.2.RELEASE]
        at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1057) ~[spring-beans-4.3.2.RELEASE.jar:4.3.2.RELEASE]
        at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1019) ~[spring-beans-4.3.2.RELEASE.jar:4.3.2.RELEASE]
        at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.inject(AutowiredAnnotationBeanPostProcessor.java:566) ~[spring-beans-4.3.2.RELEASE.jar:4.3.2.RELEASE]
        ... 30 more
Tests run: 1, Failures: 0, Errors: 1, Skipped: 0, Time elapsed: 1.318 sec <<< FAILURE! - in com.radial.hostedpayments.service.MailClientServiceIntegrationTest
shouldSendMail(com.radial.hostedpayments.service.MailClientServiceIntegrationTest)  Time elapsed: 0.002 sec  <<< ERROR!
org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'com.radial.hostedpayments.service.MailClientServiceIntegrationTest': Unsatisfied dependency expressed through field 'mailClientService': No qualifying bean of type [com.radial.hostedpayments.service.MailClientService] found for dependency [com.radial.hostedpayments.service.MailClientService]: expected at least 1 bean which qualifies as autowire candidate for this dependency. Dependency annotations: {@org.springframework.beans.factory.annotation.Autowired(required=true)}; nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type [com.radial.hostedpayments.service.MailClientService] found for dependency [com.radial.hostedpayments.service.MailClientService]: expected at least 1 bean which qualifies as autowire candidate for this dependency. Dependency annotations: {@org.springframework.beans.factory.annotation.Autowired(required=true)}
Caused by: org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type [com.radial.hostedpayments.service.MailClientService] found for dependency [com.radial.hostedpayments.service.MailClientService]: expected at least 1 bean which qualifies as autowire candidate for this dependency. Dependency annotations: {@org.springframework.beans.factory.annotation.Autowired(required=true)}


Results :

Tests in error:
  MailClientServiceIntegrationTest.shouldSendMail » UnsatisfiedDependency Error ...

Tests run: 1, Failures: 0, Errors: 1, Skipped: 0

MailClientServiceIntegrationTest.java(产品集成测试 maven 模块)

@RunWith(SpringRunner.class)
public class MailClientServiceIntegrationTest {

    @Autowired
    private MailClientService mailClientService;
    private GreenMail smtpServer;

    @Before
    public void setUp() throws Exception {
        smtpServer = new GreenMail(new ServerSetup(25, null, "smtp"));
        smtpServer.start();
    }

    @Test
    public void shouldSendMail() throws Exception {
        //given
        String recipient = "name@dolszewski.com";
        String message = "Test message content";
        //when
        mailClientService.prepareAndSend(recipient, message);
        //then
        assertReceivedMessageContains(message);
    }

    private void assertReceivedMessageContains(String expected) throws IOException, MessagingException {
        MimeMessage[] receivedMessages = smtpServer.getReceivedMessages();
        assertEquals(1, receivedMessages.length);
        String content = (String) receivedMessages[0].getContent();
        assertTrue(content.contains(expected));
    }

    @After
    public void tearDown() throws Exception {
        smtpServer.stop();
    }

}

MailClientService.java(产品服务 Maven 模块)

@Service
public class MailClientService {

    @Autowired
    private JavaMailSender mailSender;
    private MailContentBuilder mailContentBuilder;

    @Value("${error.email.from.address}")
    private String emailAddressFrom;
    @Value("${error.email.to.address}")
    private String emailRecipientAddress;

    private String[] emailRecipientAddresses;
    private static final boolean HTML_FLAG = true;
    private static final String ERROR_ALERT_EMAIL_SUBJECT = "Hosted Payment Service - Error Alert on ";
    private static final Logger LOGGER = LoggerFactory.getLogger(MailClientServiceImpl.class);

    public MailClientServiceImpl(final JavaMailSender javaMailSender, final MailContentBuilder emailContentBuilder) {
        this.mailSender = javaMailSender;
        this.mailContentBuilder = emailContentBuilder;
    }

    @Override
    public void prepareAndSend(final String message, final String emailSubject) {
        emailRecipientAddresses = StringUtils.split(emailRecipientAddress, ',');
        MimeMessagePreparator messagePreparator = mimeMessage -> {
            MimeMessageHelper messageHelper = new MimeMessageHelper(mimeMessage);
            messageHelper.setFrom(emailAddressFrom);
            messageHelper.setTo(emailRecipientAddresses);
            if (StringUtils.isNotBlank(emailSubject)) {
                messageHelper.setSubject(emailSubject);
            } else {
                messageHelper.setSubject(ERROR_ALERT_EMAIL_SUBJECT + CommonUtils.getHostName());
            }

            // Create the HTML body using Thymeleaf
            String htmlContent = mailContentBuilder.buildHtmlTemplating(message);
            messageHelper.setText(htmlContent, HTML_FLAG);
        };

        try {
            // Send email
            if (LOGGER.isDebugEnabled()) {
                LOGGER.debug("Sending email '{}' to: {} ", ERROR_ALERT_EMAIL_SUBJECT, emailRecipientAddress);
            }
            mailSender.send(messagePreparator);
        } catch (MailException e) {
                e.printStackTrace();
                LOGGER.error("Problem with sending alert email to: {}, error message: {}", emailRecipientAddress, e.getMessage());
        }
    }

    @Override
    public void prepareAndSend(final String message) {
        prepareAndSend(message, null);
    }

}     

product-web/pom.xml

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-war-plugin</artifactId>
    <version>3.1.0</version>
    <configuration>
        <attachClasses>true</attachClasses>
    </configuration>
</plugin>

product-integration-tests/pom.xml

<dependency>
    <groupId>${project.groupId}</groupId>
    <artifactId>hosted-payments</artifactId>
    <version>${project.version}</version>
    <classifier>classes</classifier>
</dependency>

如果仔细观察,您可以创建 MailClientService 的一个实例,但是Spring 就是不能注入通过 @Autowired 注释在您的测试 class 中创建一个实例:

No qualifying bean of type [com.radial.hostedpayments.service.MailClientService] found for dependency [com.radial.hostedpayments.service.MailClientService]: expected at least 1 bean which qualifies as autowire candidate for this dependency. Dependency annotations: {@org.springframework.beans.factory.annotation.Autowired(required=true)}; nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException

您需要提供 MailClientService class 的 Spring 个 bean 实例供您的测试 class 使用。这可以通过 @ContextConfiguration 注释来完成。

第 1 步 - 创建一个 @Configuration class 公开您的 MailServiceClient :

@Configuration
public class TestConfig {

    @Bean
    public JavaMailSender mailSender() {
       // example of returning a mock object 
       return Mockito.mock(JavaMailSender.class);
    }    

    @Bean
    public GreenMail smtpServer() {
       // another mock
       return Mockito.mock(GreenMail.class);
    }

    @Bean
    public MailClientService mailClientService(){
        // this could also be used to return a Mock object
        return new MailClientService();
    }
}

第 2 步 - 将配置 class 添加到您的测试中:

...
@ContextConfiguration(classes = {TestConfig.class})
public class MailClientServiceIntegrationTest {
...

我对它进行了编辑,以展示您如何 return 从您的配置 class 进行模拟。请注意,在您的实现中,您自动装配了 JavaMailSender,但没有自动装配 GreenMail 变量。

另一个(可能更简单的选项)是使用 @MockBean 注释而不是 Spring Boot testing doc (section 41.3.4)

中描述的自动装配