Spring Boot maven 多模块项目——单元测试(应用上下文)

Posted

技术标签:

【中文标题】Spring Boot maven 多模块项目——单元测试(应用上下文)【英文标题】:Spring Boot maven multimodule project - unit testing (application context) 【发布时间】:2017-12-06 14:45:39 【问题描述】:

我是 Spring 框架的初学者。我在 spring boot 中配置单元测试时遇到问题,更准确地说是在运行单元测试时加载 spring 上下文。我使用 maven 多模块项目(在团队中)并寻找正确的解决方案来做到这一点。 我的部分项目结构如下:

commons(模块、包装:jar、utils 模块) +--- 源 +--- pom.xml 提案(模块,包装:pom) proposal-api(子模块:接口,dto,包装:jar) 提案映射(子模块:实体) proposal-service(子模块:服务,spring 数据存储库,dto - entitydto 映射器,依赖于proposal-api 和proposal-mapping 打包:jar) +--- 源   +---主要     +--- java       +---com.company.proposal.service         +--- DeviceRepositoryService.java         +--- DeviceMapper.java         +--- ProposalRepositoryService.java         +--- 提案映射器.java         +--- 还有更多课程...   +--- 测试     +--- java       +---com.company.proposal.service         +--- DeviceRepositoryServiceTest.java         +--- ProposalRepositoryServiceTest.java         +--- ... +--- pom.xml proposal-starter(子模块:自动配置类,包装:jar) +--- 源   +---主要     +--- java       +---com.company.proposal.configuration         +--- ProposalAutoConfiguration.java         +--- RemoteReportProcessorAutoConfiguration.java         +---其他配置类...     +---资源       +---META-INF         +--- spring.factories       +---application.properties+--- pom.xml 入口点(模块,包装:pom) entry-point-api(子模块,打包:jar) 入口点服务(子模块,包装:jar) entry-point-starter(子模块,打包:war部署在wildfly上) 其他模块... pom.xml(根 pom)

我编写的示例单元测试(DeviceRepositoryServiceTest.java):

@RunWith(SpringRunner.class)
public class DeviceRepositoryServiceTest 

    @Rule
    public ExpectedException thrown = ExpectedException.none();

    @MockBean
    private DeviceRepository deviceRepository;

    @Autowired
    private DeviceMapper deviceMapper;

    private DeviceRepositoryService deviceRepositoryService;

    private final String imei = "123456789123456";
    private final String producer = "samsung";
    private final String model = "s5";

    @Before
    public void setUp() 
        MockitoAnnotations.initMocks(this);
        deviceRepositoryService = new DeviceRepositoryService(deviceRepository, deviceMapper);
    

    @org.springframework.boot.test.context.TestConfiguration
    static class TestConfiguration 
        @Bean
        public DeviceMapper deviceMapper() 
            return new DeviceMapperImpl();
        
    

    @Test
    public void test_should_create_device() 
        given(deviceRepository.findByImei(imei)).willReturn(null);
        when(deviceRepository.save(any(Device.class))).thenAnswer((Answer) invocation -> invocation.getArguments()[0]);
        DeviceSnapshot device = deviceRepositoryService.createOrFindDeviceByImei(imei, producer, model);
        assertThat(device.getImei()).isEqualTo(imei);
        assertThat(device.getProducer()).isEqualTo(producer);
        assertThat(device.getModel()).isEqualTo(model);
        verify(deviceRepository, times(1)).save(any(Device.class));
    

    @Test
    public void test_should_return_device() 
        Device testDevice = createTestDevice();
        given(deviceRepository.findByImei(imei)).willReturn(testDevice);
        DeviceSnapshot actualDevice = deviceRepositoryService
                .createOrFindDeviceByImei(testDevice.getImei(), testDevice.getProducer(), testDevice.getModel());
        assertThat(actualDevice.getImei()).isEqualTo(testDevice.getImei());
        assertThat(actualDevice.getProducer()).isEqualTo(testDevice.getProducer());
        assertThat(actualDevice.getModel()).isEqualTo(testDevice.getModel());
        verify(deviceRepository, times(0)).save(any(Device.class));
        verify(deviceRepository, times(1)).findByImei(testDevice.getImei());
    

    @Test
    public void test_should_find_device() 
        Device device = createTestDevice();
        given(deviceRepository.findOne(device.getId())).willReturn(device);
        DeviceSnapshot actualDevice = deviceRepositoryService.findDeviceById(device.getId());
        DeviceSnapshot expectedDevice = deviceMapper.toDeviceSnapshot(device);
        assertThat(actualDevice).isEqualTo(expectedDevice);
        verify(deviceRepository, times(1)).findOne(device.getId());
    

    @Test
    public void test_should_find_device_by_pparams() 
        Device device = createTestDevice();
        Long proposalId = 1L, providerConfigId = 2L;
        given(deviceRepository.findByProposalParams(proposalId, providerConfigId)).willReturn(device);
        DeviceSnapshot actualDevice = deviceRepositoryService.findDeviceByProposalParams(proposalId, providerConfigId);
        DeviceSnapshot expectedDevice = deviceMapper.toDeviceSnapshot(device);
        assertThat(actualDevice).isEqualTo(expectedDevice);
        verify(deviceRepository, times(1)).findByProposalParams(proposalId, providerConfigId);
    

    @Test
    public void test_should_throw_not_found_1() 
        given(deviceRepository.findOne(anyLong())).willReturn(null);
        this.thrown.expect(DeviceNotFoundException.class);
        deviceRepositoryService.findDeviceById(1L);
    

    @Test
    public void test_should_throw_not_found_2() 
        given(deviceRepository.findByProposalParams(anyLong(), anyLong())).willReturn(null);
        this.thrown.expect(DeviceNotFoundException.class);
        deviceRepositoryService.findDeviceByProposalParams(1L, 1L);
    

    private Device createTestDevice() 
        return Device.builder()
                .id(1L)
                .imei(imei)
                .model(model)
                .producer(producer)
                .build();
    

如您所见,我使用@TestConfiguration 注解来定义上下文,但是因为DeviceRepositoryService 类非常简单——只有2 个依赖项,所以上下文定义也很简单。我还必须测试类ProposalRepositoryService,简而言之如下:

@Slf4j
@Service
@AllArgsConstructor
@Transactional
public class ProposalRepositoryService implements ProposalService 

    private final ProposalRepository proposalRepository;
    private final ProposalMapper proposalMapper;
    private final ProposalRepositoryProperties repositoryProperties;
    private final ImageProposalRepository imageProposalRepository;
    private final ProviderConfigService providerConfigService;
    ...

在上面的类中有更多的依赖项,问题是我不想为每个测试编写一堆配置代码(TestConfiguration 注释)。例如。如果我向某些服务添加一些依赖项,我将不得不更改一半的单元测试类,而且很多代码也会重复。当单元测试代码由于配置定义而变得丑陋时,我也有示例:

@TestPropertySource("classpath:application-test.properties")
public class RemoteReportProcessorRepositoryServiceTest 

    @Autowired
    private RemoteReportProcessorRepositoryService remoteReportProcessorRepositoryService;

    @TestConfiguration //here, I don't want to write bunch of configuration code for every test
    static class TestConfig 

        @Bean
        @Autowired
        public RemoteReportProcessorRepositoryService remoteReportProcessorRepositoryService(RemoteReportMailService remoteReportMailService,
                                                                                             FtpsService ftpsService,
                                                                                             RemoteDailyReportProperties remoteDailyReportProperties,
                                                                                             RemoteMonthlyReportProperties remoteMonthlyReportProperties,
                                                                                             DeviceRepository deviceRepository,
                                                                                             ProposalRepository proposalRepository) 
            return new RemoteReportProcessorRepositoryService(ftpsService, remoteReportMailService, remoteDailyReportProperties, remoteMonthlyReportProperties, deviceRepository, proposalRepository);
        

        @Bean
        @Autowired
        public FtpsManagerService ftpsManagerService(FTPSClient ftpsClient, MailService mailService, FtpsProperties ftpsProperties) 
            return new FtpsManagerService(ftpsClient, ftpsProperties, mailService);
        

        @Bean
        public FTPSClient ftpsClient() 
            return new FTPSClient();
        

        @Bean
        @Autowired
        public MailService mailService(MailProperties mailProperties, JavaMailSender javaMailSender, PgpProperties pgpProperties) 
            return new MailManagerService(mailProperties, javaMailSender, pgpProperties);
        

        @Bean
        public JavaMailSender javaMailSender() 
            return new JavaMailSenderImpl();
        

        @Bean
        @Autowired
        public RemoteReportMailService remoteReportMailService(RemoteReportMailProperties remoteReportMailProperties,
                                                               JavaMailSender javaMailSender,
                                                               Session session,
                                                               PgpProperties pgpProperties) 
            return new RemoteReportMailManagerService(remoteReportMailProperties, javaMailSender, session, pgpProperties);
        

        @Bean
        @Autowired
        public Session getJavaMailReceiver(RemoteReportMailProperties remoteReportMailProperties) 
            Properties properties = new Properties();
            properties.put("mail.imap.host", remoteReportMailProperties.getImapHost());
            properties.put("mail.imap.port", remoteReportMailProperties.getImapPort());
            properties.setProperty("mail.imap.socketFactory.class", "javax.net.ssl.SSLSocketFactory");
            properties.setProperty("mail.imap.socketFactory.fallback", "false");
            properties.setProperty("mail.imap.socketFactory.port", remoteReportMailProperties.getImapPort().toString());
            properties.put("mail.imap.debug", "true");
            properties.put("mail.imap.ssl.trust", "*");
            return Session.getDefaultInstance(properties);
        
    
...

所以,我的问题是如何在 Spring Boot Maven 多模块项目中以正确的方式配置 Spring 上下文以进行单元测试,而无需编写一堆配置代码? 当详细描述如何处理 maven 多模块项目时,我也将感谢文章的链接。

【问题讨论】:

您可以为类似的测试用例创建一个包含配置代码的公共父类。 你能举个例子吗?我可以编写一个包含所有配置代码的类(例如在 commons 模块中),但该类必须依赖于所有其他模块(循环依赖)。 请尝试使用父类的方式,我认为不会有循环依赖的问题。我会尝试发布一个例子 那么,你能举一些例子或例子的链接吗? 【参考方案1】:

阅读各种文章和帖子后,例如。 Is it OK to use SpringRunner in unit tests? 我意识到在运行测试时我不需要整个应用程序上下文,相反我应该使用普通的 @Mock 注释来模拟 bean 依赖项,如果测试甚至不涉及和加载 Spring 应用程序上下文(这样更快)。但是,如果我需要一些应用程序上下文(例如,自动加载测试属性或仅用于集成测试) 然后我使用为此准备的spring boot注释:@WebMvcTest@JpaTest@SpringBootTest等等。

例子:

普通模拟测试(不涉及弹簧):

public class UserServiceImplTest 

    @Mock
    private UserRepository userRepository;

    private UserServiceImpl userService;

    @Before
    public void setUp() 
        MockitoAnnotations.initMocks(this);
        userService = new UserServiceImpl(userRepository);
    

    /* Some tests here */


使用 spring 上下文切片进行测试:

@RunWith(SpringRunner.class)
@ActiveProfiles("test")
@EnableConfigurationProperties(value = DecisionProposalProperties.class)
@SpringBootTest(classes = 
        DecisionProposalRepositoryService.class,
        DecisionProposalMapperImpl.class
)
public class DecisionProposalRepositoryServiceTest 

    @MockBean
    private DecisionProposalRepository decisionProposalRepository;

    @MockBean
    private CommentRepository commentRepository;

    @Autowired
    private DecisionProposalRepositoryService decisionProposalRepositoryService;

    @Before
    public void setUp() 
        MockitoAnnotations.initMocks(this);
    

    /* Some tests here */


数据 jpa 测试:

@RunWith(SpringRunner.class)
@DataJpaTest
public class ImageProposalRepositoryTest 

    @Autowired
    private TestEntityManager entityManager;

    @Autowired
    private ImageProposalRepository imageProposalRepository;

    @Test
    public void testFindOne() throws Exception 
        ImageProposal imageProposal = ImageProposal.builder()
                .size(1024)
                .filePath("/test/file/path").build();
        entityManager.persist(imageProposal);
        ImageProposal foundImageProposal = imageProposalRepository.findOne(imageProposal.getId());
        assertThat(foundImageProposal).isEqualTo(imageProposal);
    

【讨论】:

以上是关于Spring Boot maven 多模块项目——单元测试(应用上下文)的主要内容,如果未能解决你的问题,请参考以下文章

带有 Spring Data 的 Spring Boot Maven 多模块项目

了解多模块项目中的 Spring Boot Maven 插件

Spring-Boot构建多模块项目

Spring Boot maven 多模块项目——单元测试(应用上下文)

嵌套的maven多模块spring boot项目

Spring boot application.properties maven 多模块项目