Spring 启动 maven 多模块项目 - 单元测试(应用上下文)
Spring Boot maven multimodule project - unit testing (application context)
我是 spring 框架的初学者。我在 spring 引导中配置单元测试时遇到问题,更准确地说是在 运行 单元测试时加载 spring 上下文。我使用 Maven 多模块项目(在团队中)并寻找正确的解决方案来做到这一点。
我的部分项目结构如下:
- commons(模块,packaging:jar,utils 模块)
+--- 源码
+--- pom.xml
- 提案(模块,packaging:pom)
- 提案-api(子模块:接口,dto,packaging:jar)
- 提案映射(子模块:实体)
- 提案服务(子模块:服务,spring 数据存储库,dto - 实体<->dto 映射器,取决于提案-api 和提案映射packaging:jar)
+--- 源码
+---主要
+--- java
+---com.company.proposal.service
+--- DeviceRepositoryService.java
+---
DeviceMapper.java
+---
ProposalRepositoryService.java
+---
ProposalMapper.java
+---
还有更多 classes...
+---测试
+--- java
+---com.company.proposal.service
+---
DeviceRepositoryServiceTest.java
+---
ProposalRepositoryServiceTest.java
+---
...
+--- pom.xml
- proposal-starter(子模块:自动配置 classes,packaging:jar)
+--- 源码
+---主要
+--- java
+---com.company.proposal.configuration
+--- ProposalAutoConfiguration.java
+--- RemoteReportProcessorAutoConfiguration.java
+---其他配置class是...
+---资源
+---META-INF
+--- spring.factories
+---application.properties
+--- pom.xml
- 入口点(模块,包装:pom)
- 入口点-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 注释来定义上下文,但是因为class DeviceRepositoryService
非常简单——只有2 个依赖项,所以上下文定义也很简单。我还必须测试 class 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;
...
}
上面的 class 是更多的依赖项,问题是我不想为每个测试(TestConfiguration 注释)编写一堆配置代码。例如。如果我向某些服务添加一些依赖项,我将不得不更改一半的单元测试 classes,而且很多代码会重复出现。我还有一个例子,当单元测试代码由于配置定义而变得丑陋时:
@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 多模块项目时,我也会感谢文章的链接。
阅读各种文章和帖子后,例如。 我意识到在 运行 测试时我不需要整个应用程序上下文,相反,如果测试甚至不涉及和加载 spring 应用程序上下文(更快)。但是,如果我需要一些应用程序上下文(例如,自动加载测试属性或仅用于集成测试)
然后我使用 spring 为此准备的引导注释:@WebMvcTest
@JpaTest
@SpringBootTest
等等。
示例:
普通模拟测试(不涉及 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 */
}
使用 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 框架的初学者。我在 spring 引导中配置单元测试时遇到问题,更准确地说是在 运行 单元测试时加载 spring 上下文。我使用 Maven 多模块项目(在团队中)并寻找正确的解决方案来做到这一点。 我的部分项目结构如下:
- commons(模块,packaging:jar,utils 模块)
+--- 源码
+--- pom.xml - 提案(模块,packaging:pom)
- 提案-api(子模块:接口,dto,packaging:jar)
- 提案映射(子模块:实体)
- 提案服务(子模块:服务,spring 数据存储库,dto - 实体<->dto 映射器,取决于提案-api 和提案映射packaging:jar)
+--- 源码
+---主要
+--- java
+---com.company.proposal.service
+--- DeviceRepositoryService.java
+--- DeviceMapper.java
+--- ProposalRepositoryService.java
+--- ProposalMapper.java
+--- 还有更多 classes...
+---测试
+--- java
+---com.company.proposal.service
+--- DeviceRepositoryServiceTest.java
+--- ProposalRepositoryServiceTest.java
+--- ...
+--- pom.xml - proposal-starter(子模块:自动配置 classes,packaging:jar)
+--- 源码
+---主要
+--- java
+---com.company.proposal.configuration
+--- ProposalAutoConfiguration.java
+--- RemoteReportProcessorAutoConfiguration.java
+---其他配置class是...
+---资源
+---META-INF
+--- spring.factories
+---application.properties
+--- pom.xml
- 入口点(模块,包装:pom)
- 入口点-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 注释来定义上下文,但是因为class DeviceRepositoryService
非常简单——只有2 个依赖项,所以上下文定义也很简单。我还必须测试 class 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;
...
}
上面的 class 是更多的依赖项,问题是我不想为每个测试(TestConfiguration 注释)编写一堆配置代码。例如。如果我向某些服务添加一些依赖项,我将不得不更改一半的单元测试 classes,而且很多代码会重复出现。我还有一个例子,当单元测试代码由于配置定义而变得丑陋时:
@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 多模块项目时,我也会感谢文章的链接。
阅读各种文章和帖子后,例如。 @WebMvcTest
@JpaTest
@SpringBootTest
等等。
示例:
普通模拟测试(不涉及 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 */
}
使用 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);
}
}