简体   繁体   中英

Spring Boot maven multimodule project - unit testing (application context)

I am beginner with spring framework. I have a problem with configuring unit tests in spring boot, more precisely with loading spring context while running unit tests. I work with maven multimodule project (in team) and looking for the right solution to do this. Part of my project structure is as follows:

  • commons (module, packaging:jar, utils module)
    +--- src
    +--- pom.xml
  • proposal (module, packaging:pom)
    • proposal-api (submodule: interfaces, dto, packaging:jar)
    • proposal-mapping (submodule: entities)
    • proposal-service (submodule: services, spring data repositories, dto - entity<->dto mappers, depends on proposal-api and proposal-mapping packaging:jar)
      +--- src
      +---main
      +--- java
      +---com.company.proposal.service
      +--- DeviceRepositoryService.java
      +--- DeviceMapper.java
      +--- ProposalRepositoryService.java
      +--- ProposalMapper.java
      +--- and much more classes...
      +--- test
      +--- java
      +---com.company.proposal.service
      +--- DeviceRepositoryServiceTest.java
      +--- ProposalRepositoryServiceTest.java
      +--- ...
      +--- pom.xml
    • proposal-starter (submodule: autoconfiguration classes, packaging:jar)
      +--- src
      +---main
      +--- java
      +---com.company.proposal.configuration
      +--- ProposalAutoConfiguration.java
      +--- RemoteReportProcessorAutoConfiguration.java
      +--- other configuration classes...
      +---resources
      +---META-INF
      +--- spring.factories
      +---application.properties

      +--- pom.xml
  • entry-point (module, packaging: pom)
    • entry-point-api (submodule, packaging: jar)
    • entry-point-service (submodule, packaging: jar)
    • entry-point-starter (submodule, packaging: war deployed on wildfly)
  • other-modules ...
  • pom.xml (root pom)

Example unit test written by me (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();
    }
}

As you can see I use @TestConfiguration annotation to define context, but because class DeviceRepositoryService is quite simple - only 2 dependencies so context definition is also simple. I also have to test class ProposalRepositoryService which looks as follows in short:

@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;
    ...
}

In above class is more dependencies and the thing is I don't want to write bunch of configuration code for every test (TestConfiguration annotation). Eg. If I add some dependency to some service I would have to change half of my unit tests classes, also a lot of code repeats itself. I have also example when unit test code is getting ugly because of configuration definition:

@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);
        }
    }
...
}

So, my question is how to configure spring context for unit testing in spring boot maven multimodule project the right way, without writing bunch of configuration code? I also will be grateful for the links to the articles when is describe in detail how to deal with maven multimodule projects.

After reading various articles and posts eg. Is it OK to use SpringRunner in unit tests? I realized that I don't need the entire application context when running tests, instead I should mock bean dependencies using plain @Mock annotation if testing without even involving and loading spring application context (which is faster). However, If I need some slice of application context (eg. to automatically load test properties or just for integration tests) then I use spring boot annotations prepared for that: @WebMvcTest @JpaTest @SpringBootTest and so on.

Examples:

Plain Mock Test (without involving spring):

public class UserServiceImplTest {

    @Mock
    private UserRepository userRepository;

    private UserServiceImpl userService;

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

    /* Some tests here */

}

Test with slice of spring context:

@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 */

}

Data jpa test:

@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);
    }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM