当前位置:   article > 正文

SpringBoot——单元测试实践总结_springboot集成test框架单元测试

springboot集成test框架单元测试

单元测试

概念

在计算机编程中,单元测试(英语:Unit Testing)又称为模块测试,是针对程序模块(软件设计的最小单位)来进行正确性检验的测试工作。 程序单元是应用的最小可测试部件。在过程化编程中,一个单元就是单个程序、函数、过程等;对于面向对象编程,最小单元就是方法,包括基类(超类)、抽象类、或者派生类(子类)中的方法。——维基百科

作用

  1. 提质 单测可以一定程度上减少潜在bug,提高代码质量。单测不仅解决覆盖率问题,也可以覆盖代码块中的一些边界和异常处理问题。
  2. 重构 单测可以为后续其他小伙伴修改、重构代码保驾护航,因为你只要敢乱改代码,单测就敢给你报错。
  3. 调试 单测有助于代码调试,我们可以按照需求进行依赖类、方法和参数的mock,无需下游类、方法的真实调用。
  4. CodeReview 单测也是一种自我CodeReview的过程。对功能单元的主流程、分支、边界以及异常情况进行分别测试,有助于复查代码的逻辑设计是否合理。反向督促自己提高编码质量意识。特别是,一个方法几百行,写单测时,你会发现这一坨,为何不拆一拆?

黑白盒

黑盒测试

  黑盒测试又称为功能测试数据驱动测试,测试过程中,程序看作成一个黑色盒子,看不到盒子内部代码结构。
  黑盒测试主要根据功能需求设计测试用例进行测试,是一种从软件外部实施的测试方式。多次输入参数,测试查看程序是否正常或达到预期。
  黑盒只知道软件的功能(能干什么),但是不知道软件的实现(怎么干的)。

白盒测试

  白盒测试又称为结构测试逻辑驱动测试,测试过程中,程序看作一个透明盒子,能够看清盒子内部的代码和结构,这样测试人员对程序代码的逻辑有所知晓。
  穷举路径的方式传参,检查代码所有结构是否正常或符合预期。单元测试属于白盒测试。
  白盒知道软件的实现(怎么干的),不需要管软件的功能(能干什么)。

逻辑覆盖

1、语句覆盖

  程序每条可执行语句至少执行一次,即测试用例覆盖所有语句。

2、判定覆盖

  也称为分支覆盖,针对判定表达式,true或false两种真假判定,程序中每一个判断的分支至少经历一次。
  比如,判定表达式:a > 0 && b > 0
  设计测试数据

  1. a > 0 && b > 0(判定表达式的值为“真”)
  2. a <= 0 && b <= 0(判定表达式的值为“假”)
  3. 复制代码

  满足判定的所有分支覆盖(此时真、假分支都覆盖)。

3、条件覆盖

  针对判断语句中的条件,程序中每个判断中的每个条件取值至少满足一次,针对条件语句。
  比如,判定表达式:a > 0 && b > 0
  设计测试数据

  1. a <= 0 && b <= 0(判定表达式的值为“假”)
  2. a > 0 && b <= 0(判定表达式的值为“假”)
  3. 复制代码

  保证每个条件取值一次,而不管分支是否覆盖全面(此时只覆盖假分支)。

4、条件/判定覆盖

  判定条件中所有可能条件成立与否至少执行一次取值、所有真假判断的可能结果至少执行一次。
  比如,判定表达式:a > 0 && b > 0
  设计测试数据

  1. a > 0 && b > 0(判定表达式的值为“真”)
  2. a <= 0 && b <= 0(判定表达式的值为“假”)
  3. 复制代码

5、条件组合覆盖

  所有可能的条件取值组合至少执行一次。
  比如,判定表达式:a > 0 && b > 0
  设计测试数据

  1. a > 0 && b > 0(判定表达式的值为“真”)
  2. a <= 0 && b <= 0(判定表达式的值为“假”)
  3. a > 0 && b <= 0(判定表达式的值为“假”)
  4. a <= 0 && b > 0(判定表达式的值为“假”)
  5. 复制代码

  判定中所有可能的条件组合。

6、路径覆盖


  所有可能执行的路径。

SpringBoot工程单测介绍

pom依赖

  1. <dependency>
  2. <groupId>org.springframework.boot</groupId>
  3. <artifactId>spring-boot-starter-test</artifactId>
  4. <scope>test</scope>
  5. </dependency>
  6. 复制代码

注意:

  1. 该依赖版本一般是跟着对应的SpringBoot版本走,不需要手动指定。
  2. 该依赖会自动引入相关依赖: JUnit Mockito Spring Test ... ...

Idea结构

创建路径

  与src/main同级目录,创建src/test,其他类路径与main包中保持一致。(可通过创建类的快捷方法自动创建路径)

创建类和方法

创建类对应单测类的快捷方法:只需将双击这个类,鼠标右键,然后选择go to到Test。

选择要测试的方法,同时,可以选择JUnit版本等,点击OK

Controller层单测

被测代码

  1. @Slf4j
  2. @RestController
  3. @RequestMapping("/demo")
  4. @Api(value = "DemoController", tags = "Demo管理模块")
  5. public class DemoController implements DemoApi {
  6. /**
  7. * service
  8. */
  9. @Autowired
  10. private DemoService demoService;
  11. @Override
  12. @ApiOperation(value = "新增", notes = "新增")
  13. @PostMapping("/create")
  14. public Boolean add(@RequestHeader("appCode") String appCode,
  15. @RequestBody DemoDTO demoDTO) {
  16. boolean addFlag = demoService.add(demoDTO);
  17. if (addFlag) {
  18. // 刷新资源
  19. demoService.refreshMap(appCode);
  20. }
  21. return addFlag;
  22. }
  23. @Override
  24. @ApiOperation(value = "修改", notes = "修改")
  25. @PostMapping("/update")
  26. public Boolean update(@RequestHeader("appCode") String appCode,
  27. @RequestBody DemoDTO demoDTO) {
  28. boolean addFlag = demoService.update(demoDTO);
  29. if (addFlag) {
  30. // 刷新资源
  31. demoService.refreshMap(appCode);
  32. }
  33. return addFlag;
  34. }
  35. @Override
  36. @ApiOperation(value = "删除", notes = "删除")
  37. @DeleteMapping("/delById")
  38. public Boolean deleteById(@RequestHeader("appCode") String appCode, @RequestParam Long id) {
  39. boolean deleteFlag = demoService.deleteById(id);
  40. if (deleteFlag) {
  41. // 刷新资源
  42. demoService.refreshMap(appCode);
  43. }
  44. return addFlag;
  45. }
  46. @Override
  47. @ApiOperation(value = "列表", notes = "列表")
  48. @GetMapping("/list")
  49. public List<DemoVO> list() {
  50. return demoService.list();
  51. }
  52. }
  53. 复制代码

测试代码

  1. 单独组件测试,无需使用@SpringBootTest进行整体上下文启动,使用@RunWith(SpringRunner.class)运行测试用例。
  2. 使用@InjectMock注入controller类。
  3. 使用@Mock注解mock service类。
  1. @RunWith(SpringRunner.class)
  2. public class DemoControllerTest {
  3. /**
  4. * mock mvc
  5. */
  6. private MockMvc mockMvc;
  7. /**
  8. * 注入实例
  9. */
  10. @InjectMocks
  11. private DemoController demoController;
  12. /**
  13. * service mock
  14. */
  15. @Mock
  16. private DemoService demoService;
  17. /**
  18. * appCode
  19. */
  20. private String appCode;
  21. /**
  22. * before设置
  23. */
  24. @Before
  25. public void setUp() {
  26. //初始化带注解的对象
  27. MockitoAnnotations.openMocks(this);
  28. //构造mockmvc
  29. mockMvc = MockMvcBuilders.standaloneSetup(demoController).build();
  30. //appCode
  31. appCode = "AppCode_test";
  32. }
  33. /**
  34. * 测试testAdd
  35. */
  36. @Test
  37. public void testAdd() throws Exception {
  38. //构建dto
  39. DemoDTO demoDTO = new DemoDTO();
  40. //setId
  41. demoDTO.setId(-1L);
  42. //setName
  43. demoDTO.setName("test");
  44. //mock service方法
  45. PowerMockito.when(demoService.add(demoDTO)).thenReturn(true);
  46. //构造body
  47. String body = JSONObject.toJSONString(demoDTO);
  48. //执行mockmvc
  49. this.mockMvc.perform(MockMvcRequestBuilders.post("/demo/create")
  50. //传参
  51. .header("appCode", appCode).content(body).contentType(MediaType.APPLICATION_JSON_VALUE))
  52. //mock返回
  53. .andExpect(status().isOk()).andDo(MockMvcResultHandlers.print()).andReturn();
  54. }
  55. /**
  56. * 测试testUpdate
  57. */
  58. @Test
  59. public void testUpdate() throws Exception {
  60. //构建dto
  61. DemoDTO demoDTO = new DemoDTO();
  62. //setId
  63. demoDTO.setId(-1L);
  64. //setName
  65. demoDTO.setName("test");
  66. //mock service方法
  67. PowerMockito.when(demoService.update(demoDTO)).thenReturn(true);
  68. //构造body
  69. String body = JSONObject.toJSONString(demoDTO);
  70. //执行mockmvc
  71. this.mockMvc.perform(MockMvcRequestBuilders.post("/demo/update")
  72. //传参
  73. .header("appCode", appCode).content(body).contentType(MediaType.APPLICATION_JSON_VALUE))
  74. //mock返回
  75. .andExpect(status().isOk()).andDo(MockMvcResultHandlers.print()).andReturn();
  76. }
  77. /**
  78. * 测试testDelete
  79. */
  80. @Test
  81. public void testDelete() throws Exception {
  82. //Id
  83. Long id = 1000L;
  84. //mock service方法
  85. PowerMockito.when(demoService.deleteById(id)).thenReturn(true);
  86. //执行mockmvc 方法一
  87. // this.mockMvc.perform(MockMvcRequestBuilders.delete("/demo/delById?id={id}",id)
  88. // //传参
  89. // .header("appCode", appCode).contentType(MediaType.APPLICATION_JSON_VALUE))
  90. // //mock返回
  91. //.andExpect(status().isOk()).andDo(MockMvcResultHandlers.print()).andReturn();
  92. // }
  93. //方法二
  94. this.mockMvc.perform(MockMvcRequestBuilders.delete("/demo/delById")
  95. //传参
  96. .header("appCode", appCode).param("id", "1000").contentType(MediaType.APPLICATION_JSON_VALUE))
  97. //mock返回
  98. .andExpect(status().isOk()).andDo(MockMvcResultHandlers.print()).andReturn();
  99. }
  100. /**
  101. * 测试testList
  102. */
  103. @Test
  104. public void testList() throws Exception {
  105. this.mockMvc.perform(MockMvcRequestBuilders.get("/demo/list")
  106. //传参
  107. .contentType(MediaType.APPLICATION_JSON_VALUE))
  108. //mock返回
  109. .andExpect(status().isOk()).andDo(MockMvcResultHandlers.print()).andReturn();
  110. }
  111. }
  112. 复制代码

Service层单测

被测代码

  1. @Service
  2. public class MenuService {
  3. @Autowired
  4. private MenuMapper menuMapper;
  5. @Transactional(readOnly = true)
  6. public List<Menu> listMenus() {
  7. final List<Menu> result = menuMapper.list();
  8. return result;
  9. }
  10. }
  11. 复制代码

测试代码

  1. 通过 @TestConfiguration 创建一个测试用配置,该配置中提供了一个 MenuService Bean的声明。
  2. 该注解的使用有以下几个注意点:被注解的类须是 static 的,且不能是 private 的。建议用在使用在内部类上,否则所定义的 Bean 将不会被自动加载。须通过以下方式之一进行加载
  1. @Import(MenuServiceTestConfig.class)
  2. @ContextConfiguration(classes =MenuServiceTestConfig.class)
  3. @Autowired
  4. //此处通过 @Autowired 自动注入的是上面通过 @TestConfiguration 声明的 Bean。
  5. @RunWith(SpringRunner.class)
  6. public class MenuServiceTest {
  7. @TestConfiguration
  8. static class MenuServiceTestConfig {
  9. @Bean
  10. public MenuService mockMenuService() {
  11. return new MenuService();
  12. }
  13. }
  14. @Autowired
  15. private MenuService MenuService;
  16. @MockBean
  17. private MenuMapper MenuMapper;
  18. @Test
  19. public void listMenus() {
  20. List<Menu> menus = new ArrayList<Menu>() {{
  21. this.add(new Menu());
  22. }};
  23. Mockito.when(menuMapper.list()).thenReturn(menus);
  24. List<Menu> result = menuService.listMenus();
  25. Assertions.assertThat(result.size()).isEqualTo(menus.size());
  26. }
  27. }
  28. 复制代码

单测规约

  1. 【强制】好的单元测试必须遵守AIR原则。 说明:单元测试在线上运行时,感觉像空气(AIR)一样并不存在,但在测试质量的保障上,却是非常关键的。好的单元测试宏观上来说,具有自动化、独立性、可重复执行的特点。
  1. AAutomatic(自动化)
  2. IIndependent(独立性)
  3. RRepeatable(可重复)
  4. 复制代码
  1. 【强制】单元测试应该是全自动执行的,并且非交互式的。 说明:测试用例通常是被定期执行的,执行过程必须完全自动化才有意义。输出结果需要人工检查的测试不是一个好的单元测试。单元测试中不准使用System.out来进行人肉验证,必须使用assert来验证。
  2. 【强制】保持单元测试的独立性。 说明:为了保证单元测试稳定可靠且便于维护,单元测试用例之间决不能互相调用,也不能依赖执行的先后次序。 反例:method2需要依赖method1的执行,将执行结果作为method2的输入。
  3. 【强制】单元测试是可以重复执行的,不能受到外界环境的影响。 说明:单元测试通常会被放到持续集成中,每次有代码check in时单元测试都会被执行。如果单测对外部环境(网络、服务、中间件等)有依赖,容易导致持续集成机制的不可用。 正例:为了不受外界环境影响,要求设计代码时就把SUT的依赖改成注入,在测试时用spring 这样的DI框架注入一个本地(内存)实现或者Mock实现。
  4. 【强制】对于单元测试,要保证测试粒度足够小,有助于精确定位问题。单测粒度至多是类级别,一般是方法级别。 说明:只有测试粒度小才能在出错时尽快定位到出错位置。单测不负责检查跨类或者跨系统的交互逻辑,那是集成测试的领域。
  5. 【强制】核心业务、核心应用、核心模块的增量代码确保单元测试通过。 说明:新增代码及时补充单元测试,如果新增代码影响了原有单元测试,请及时修正。
  6. 【强制】单元测试代码必须写在如下工程目录:src/test/java,不允许写在业务代码目录下。 说明:源码构建时会跳过此目录,而单元测试框架默认是扫描此目录。

PowerMock框架

mock介绍

mock概念

mock是指在测试过程中,创建一个虚拟的对象来模拟指定对象的行为。

mock作用

  1. service-A依赖于service-B,测试service-A时,service-B还未开发完,通过mock模拟service-B的一些行为达到测试效果。
  2. 创建私有构造方法的对象,没法直接构建,可以通过mock构建。
  3. 被测试的模块需连接数据库等操作,测试时无法保证一定能连接数据库,可通过mock实现。
  4. ... ...

PowerMock介绍

  1. PowerMock时一个Java单测模拟的框架,扩展了EasyMock和Mockito框架。
  2. PowerMock通过提供定制的类以及一些字节码篡改技巧进行模拟。
  3. PowerMock可模拟静态方法、私有方法、构造方法、final方法等。
  4. PowerMock支持JUnit和TestNG。

PowerMock pom依赖

  1. <properties>
  2. <powermock.version>2.0.9</powermock.version>
  3. </properties>
  4. <dependencies>
  5. <dependency>
  6. <groupId>org.powermock</groupId>
  7. <artifactId>powermock-module-junit4</artifactId>
  8. <version>${powermock.version}</version>
  9. <scope>test</scope>
  10. </dependency>
  11. <dependency>
  12. <groupId>org.powermock</groupId>
  13. <artifactId>powermock-api-mockito2</artifactId>
  14. <version>${powermock.version}</version>
  15. <scope>test</scope>
  16. </dependency>
  17. </dependencies>
  18. 复制代码

注意:

此依赖适合JUnit4.4及以上。

PowerMock 功能

  • mock constructors
  • mock final method or classes
  • mock private methods
  • mock static methods
  • mock java system classes 各位小伙伴有兴趣的自己对着官网研究,版本重点介绍一些常用的mock功能。

PowerMock 常用mock示例

mock模板

  1. @RunWith(PowerMockRunner.class)
  2. @PowerMockRunnerDelegate(SpringRunner.class)
  3. @PrepareForTest({XxxUtils.class})
  4. public class DemoServiceImplTest {
  5. /**
  6. * 注入service
  7. */
  8. @InjectMocks
  9. private DemoServiceImpl demoService;
  10. /**
  11. * mapper
  12. */
  13. @MockBean
  14. private DemoMapper demoMapper;
  15. @Before
  16. public void setUp() {
  17. //构建mybatis缓存
  18. TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new MybatisConfiguration(), ""), DemoEntity.class);
  19. }
  20. @Test
  21. public void testCreateDemo() {
  22. //todo
  23. }
  24. }
  25. 复制代码

mock私有构造方法

以部门封装的Result类为例,其中一些private构造方法无法构造参数,需mock构造。

  1. Result<List<UserInfoDTO>> result = Whitebox.invokeConstructor(Result.class, "200", "", true, userInfoDTOList);
  2. 复制代码

mock私有方法和私有属性

  1. //mock 当前service类
  2. ServiceA serviceMock= PowerMockito.mock(ServiceA.class);
  3. //内部调用其他service需设置非公共成员
  4. Whitebox.setInternalState(serviceMock, "serviceB", serviceB);
  5. //mock调用其他service方法
  6. PowerMockito.when(serviceB.getStr("xxx")).thenReturn("xxxxx");
  7. //调用内部私有方法
  8. Whitebox.invokeMethod(serviceMock, "privateMethod1", demoDto);
  9. 复制代码

说明:

  1. ServiceA类中注入了ServiceB类,并调用了getStr(String str)方法。
  2. ServiceA类中私有方法privateMethod1,传参DemoDto。

mock静态方法

  1. 添加需要被测试的静态方法注解
  1. @PrepareForTest({StaticXXX.class})
  2. 复制代码
  1. 测试mock调用
  1. //mock静态类
  2. PowerMockito.mockStatic(StaticXXX.class);
  3. //mock调用静态方法
  4. PowerMockito.when(StaticXXX.method01("param01", "param02")).thenReturn(xxx);
  5. 复制代码

mock配置

  1. @MockBean
  2. private DemoProperties demoProperties ;
  3. 复制代码
  1. 使用@MockBean引入配置类
  2. 调用塞值
  1. //构造demoProperties
  2. DemoProperties .DemoFirstProperties demoFirstProperties = new DemoProperties .DemoFirstProperties ();
  3. //塞值
  4. demoFirstProperties .setFirstParams("xxxx");
  5. //mock properties
  6. PowerMockito.when(demoProperties .getDemoFirstProperties ()).thenReturn(demoFirstProperties );
  7. 复制代码

mock对象类的公有方法值返回

  1. 对象类
  1. public class SmsResponse implements Serializable {
  2. /**
  3. * 编码 1-成功
  4. */
  5. private Integer code;
  6. /**
  7. * 返回内容
  8. */
  9. private String msg;
  10. public boolean isSuccess(){
  11. return null != code && 200 == code;
  12. }
  13. public Integer getCode() {
  14. return code;
  15. }
  16. public void setCode(Integer code) {
  17. this.code = code;
  18. }
  19. public String getMsg() {
  20. return msg;
  21. }
  22. public void setMsg(String msg) {
  23. this.msg = msg;
  24. }
  25. }
  26. 复制代码
  1. 此类中的isSuccess()方法调用返回值在代码中使用
  1. // 发送短信
  2. SmsResponse smsResponse = this.smsNoticeService.sendSms(sms);
  3. if (smsResponse.isSuccess()) {
  4. if (Objects.nonNull(noticeTimes) && Objects.nonNull(smsNotice)) {
  5. // 短信发送成功,更新通知标识
  6. noticeTimes.put(NoticeChannelEnum.SMS.getDesc(),smsNotice);
  7. }
  8. result.put(MSG,"");
  9. return result;
  10. }
  11. 复制代码

此时若想进入到if分支内,就需要mock操作

  1. SmsResponse smsResponse = PowerMockito.mock(SmsResponse.class);
  2. PowerMockito.when(smsResponse.isSuccess()).thenReturn(true);
  3. when(this.smsNoticeService.sendSms(any(Sms.class))).thenReturn(smsResponse);
  4. 复制代码

排雷

@SpringBootTest

问题:

  1. @SpringBootTest会扫描应用程序的Spring配置,并构建完整的Spring Context。每次执行一个类的单元测试,都需要启动整个上下文,单测速度慢!
  2. @SpringBootTest加载Spring上下文时,可能会因为服务中使用的数据库、MQ、缓存等配置加载失败而导致测试类启动。
  3. @SpringBootTest更适合功能集成测试。

解决方案:

使用@RunWith(SpringRunner.class)声明该测试是在Spring环境中进行,这样Spring相关注解就会被识别生效。

SpringBoot+PowerMock+Mockito结合使用

问题:

结合powermock使用时,容易造成一些类和方法的冲突,导致方法找不到。

解决方案:

依赖版本对应:

MockitoPowerMock
2.8.9+2.x
2.8.0-2.8.91.7.x
2.7.51.7.0RC4
2.4.01.7.0RC2
2.0.0-beta - 2.0.42-beta1.6.5-1.7.0RC
1.10.8 - 1.10.x1.6.2 - 2.0
1.9.5-rc1 - 1.9.51.5.0 - 1.5.6
1.9.0-rc1 & 1.9.01.4.10 - 1.4.12
1.8.51.3.9 - 1.4.9
1.8.41.3.7 & 1.3.8
1.8.31.3.6
1.8.1 & 1.8.21.3.5
1.81.3
1.71.2.5

构造函数注入类

问题:

有一些service类中注入的其他类,是通过构造函数注入,而非@Autowired或者@Resource注解。如

  1. @Service
  2. public class MenuService {
  3. private MenuMapper menuMapper;
  4. @Autowired
  5. public MenuService(final MenuMapper menuMapper) {
  6. this.menuMapper = menuMapper;
  7. }
  8. @Transactional(readOnly = true)
  9. public List<Menu> listMenus() {
  10. final List<Menu> result = menuMapper.list();
  11. return result;
  12. }
  13. }
  14. 复制代码

解决方案:

构造service

  1. @RunWith(SpringRunner.class)
  2. public class MenuServiceTest {
  3. private static final MenuMapper menuMapper = Mockito.mock(MenuMapper.class);
  4. @TestConfiguration
  5. static class MenuServiceTestConfig {
  6. @Bean
  7. public MenuService mockMenuService() {
  8. return new MenuService(menuMapper);
  9. }
  10. }
  11. @Autowired
  12. private MenuService MenuService;
  13. @MockBean
  14. private MenuMapper MenuMapper;
  15. @Test
  16. public void listMenus() {
  17. List<Menu> menus = new ArrayList<Menu>() {{
  18. this.add(new Menu());
  19. }};
  20. Mockito.when(menuMapper.list()).thenReturn(menus);
  21. List<Menu> result = menuService.listMenus();
  22. Assertions.assertThat(result.size()).isEqualTo(menus.size());
  23. }
  24. }
  25. 复制代码

静态方法mock时报错org.powermock.api.mockito.ClassNotPreparedExceptionnot

问题:

mock静态方法时,报错该静态类prepared for test

解决方案:

  1. 确认类上是否添加@PrepareForTest({Xxx.class})注解进行静态类的准备。
  2. 确认使用PowerMock时,是否使用了@RunWith(PowerMockRunner.class)运行环境。

mybatis报错MybatisPlusException

问题:

使用到lambda表达式有set方法的地方,单测报错com.baomidou.mybatisplus.core.exceptions.MybatisPlusException: can not find lambda cache for this entity

解决方案:

在单测类中@Before方法中加入代码手动触发缓存信息收集。

  1. @Before
  2. public void setUp() {
  3. //构建mybatis缓存
  4. TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new MybatisConfiguration(), ""), XxxEntity.class);
  5. }
  6. 复制代码

其中,XxxEntity.class为表实体类。

作者:Andya
链接:https://juejin.cn/post/7222577873793138747
 

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/Monodyee/article/detail/653895
推荐阅读
相关标签
  

闽ICP备14008679号