文章目录
- 单测的定义
- 方法的单测
- 几种生成工具的对比
- 生成步骤
- 接口的单测
- 场景的单测
- 总结
- 参考
单测的定义
- 单元测试(Unit Testing)是一种软件开发中的测试方法,它的主要目的是确保软件中的最小可测试单元(通常是函数、方法或类)在被单独测试和验证时能够按照预期工作。尽管单元测试有很多优点,如提高代码质量、减少Bug、简化调试过程等,但它也存在一些缺点:
- 增加开发时间:如要求覆盖率到80%甚至90%,或者入参几十个难以构造,单测时间占比可能超过30%。
- 需要维护:随着代码的改变,特别是大规模的重构,单元测试也需要相应地更新和维护,增加开发的负担。
- 无法发现对其他类的影响:单元测试主要关注单个单元的行为,无法发现与多个单元交互或整个系统相关的问题。
- 所以部分公司会要求写接口维度、场景维度的单测,覆盖率在50-60%,甚至不强制要求覆盖率。
方法的单测
推荐用更智能的squaretest生成单测模板后,手工调整。
几种生成工具的对比
- diffblue
- 优点:
- 与IntelliJ IDEA集成良好,使用方便。
- 支持多种编程语言和框架。
- 缺点:
- 商用版本收费较高,对于个人用户或小型团队可能不太友好。
- 在处理某些特定写法或框架时可能不够灵活。
- 优点:
- squaretest
- 优点:
- 生成测试用例,自动覆盖部分if分支,减轻测试负担。
- 缺点:
- 只有30天的免费试用期,之后需要付费使用。事实上点掉remind后可以继续使用。
- 没有社区版支持,对于开源项目或个人用户可能不太友好。
- 优点:
- EvoSuite
- 优点:
- 作为Maven插件使用,方便集成到Java项目中。
- 支持生成多样化的测试用例,有助于发现潜在的缺陷。
- 缺点:
- 社区支持相对较少,遇到问题时可能难以得到及时帮助。
- 配置和使用可能相对复杂,需要一定的学习成本。
- 在处理某些特定场景或框架时可能不够灵活或有效。
- 优点:
- TestMe
- 优点:
- 简单易用,适合初学者或小型项目使用。
- 缺点:
- 需要手动填充输入参数和逻辑,自动化程度较低。
- 生成的测试用例可能不够全面或深入,需要额外补充和完善。
- 优点:
生成步骤
安装插件
引入依赖
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><version>2.1.1.RELEASE</version></dependency><dependency><groupId>org.junit.jupiter</groupId><artifactId>junit-jupiter</artifactId><version>5.8.2</version></dependency>
- 编写业务代码
@Servicepublic class TestServiceImpl implements TestService {@Resourceprivate TestRepository testRepository;@Resourceprivate TestThird testThird;@Overridepublic void start(InputDTO inputDTO) {InputEntity entity = testRepository.select(inputDTO.getId());if (entity == null) {testRepository.insert(entity = new InputEntity());}testThird.callThird(entity);}}
- 生成单测
- 单测生成结果
/** * squaretest */class TestServiceImplTest {@Mockprivate TestRepository mockTestRepository;@Mockprivate TestThird mockTestThird;@InjectMocksprivate TestServiceImpl testServiceImplUnderTest;@BeforeEachvoid setUp() {initMocks(this);}@Testvoid testStart() {// Setupfinal InputDTO inputDTO = new InputDTO();inputDTO.setName("name");inputDTO.setId(0);final InputDetail inputDetail = new InputDetail();inputDetail.setName("name");inputDTO.setInputDetail(inputDetail);// Configure TestRepository.select(...).final InputEntity inputEntity = new InputEntity();inputEntity.setId(0);inputEntity.setName("name");when(mockTestRepository.select(0)).thenReturn(inputEntity);when(mockTestRepository.insert(any(InputEntity.class))).thenReturn(0);// Run the testtestServiceImplUnderTest.start(inputDTO);// Verify the resultsverify(mockTestRepository).insert(any(InputEntity.class));verify(mockTestThird).callThird(any(InputEntity.class));}}
/** * testme */class TestServiceImplTestTestMe {@MockTestRepository testRepository;@MockTestThird testThird;@InjectMocksTestServiceImpl testServiceImpl;@BeforeEachvoid setUp() {MockitoAnnotations.initMocks(this);}@Testvoid testStart() {when(testRepository.select(anyInt())).thenReturn(new InputEntity());when(testRepository.insert(any())).thenReturn(Integer.valueOf(0));testServiceImpl.start(new InputDTO());}}
接口的单测
mock外部依赖,启动容器,调用接口
- 编写外部依赖的mock类
@Servicepublic class TestThirdImpl implements TestThird {@Overridepublic void callThird(InputEntity entity) {System.out.println("TestThirdImpl callThird");}}//mockpublic class TestThirdMockImpl implements TestThird {public void callThird(InputEntity entity) {System.out.println("TestThirdMockImpl callThird");}}
- 替换容器中的bean,mock外部依赖
@Configurationpublic class MockConfig {@Beanpublic BeanDefinitionRegistryPostProcessor beanDefinitionRegistryPostProcessor() {return new BeanDefinitionRegistryPostProcessor() {@Overridepublic void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {//移除依赖的beanregistry.removeBeanDefinition("testThirdImpl");//获取Mockbean的定义BeanDefinition beanDe = BeanDefinitionBuilder.rootBeanDefinition(TestThirdMockImpl.class).getBeanDefinition();//注册mockbeanregistry.registerBeanDefinition("testThirdImpl", beanDe);}@Overridepublic void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {}};}}
- test模块中启动容器,并调用入口方法
@RunWith(SpringJUnit4ClassRunner.class)@SpringBootTest(classes = TestApplication.class)public class TestApplicationTest {@Resourceprivate TestService testService;@Testpublic void start() {testService.start(new InputDTO());}}
场景的单测
将接口单测组合
@RunWith(SpringJUnit4ClassRunner.class)@SpringBootTest(classes = TestApplication.class)public class TestApplicationTest {@Resourceprivate TestService testService;@Testpublic void start() {testService.start(new InputDTO());testService.end(new InputDTO());}}
总结
- 方法的单测:覆盖入参少、业务分支多的场景。
- 接口、场景的单测:覆盖主干流程。
参考
- 告别加班/解放双手提高单测覆盖率之Java 自动生成单测代码神器推荐
- JUnit 5 User Guide
- 关于testNG和JUnit的对比
- JUnit 5 单元测试教程
- 单元测试自动生成工具EvoSuite的简单使用
- 使用BeanDefinitionRegistryPostProcessor动态注入BeanDefinition