背景介绍
为了长期持续高质量、高效率的迭代,必须遵守一定的研发规范,其中主要包括静态代码的扫描和单元测试两个部分。
风险认知
行业的普遍共识,风险识别暴露的越晚,修复的成本越高。
好代码不是一蹴而就的,是持续重构出来的
目标
技术选型
最佳实践
安装sonarLint
本地IntelliJ IDEA安装sonarlint,根据提示重启IntelliJ IDEA即可
有两个选项:根据本地修改扫描和全量扫描。可以根据实际情况开启。扫描结果如下:
此处扫描出范型约束丢失的可能运行时异常,风险等级为Major。Blocker级别为必须修复,否则打包无法通过。
可以通过右键展开全部:
在代码编写过程中,也可同步识别风险:
在Sonar中展示的代码规则违反分类数量统计,分为5个级别:Blocker、Critical、Major、Minor、Info。其中Blocker、Critical是需要重点避免的。
负责人&核心链路
应用负责人
应用 负责人(待定)
质量标准
应用 代码质量要求/核心链路情况
单测
单元测试是保证持续迭代的重要手段,下面提供应用负责人,与质量标准。
几个要点:
不要为了单测而单测,单测可以发现代码结构的不合理处,反向推动代码优化。
单测需要快速、可重复、不依赖外部数据接口(外部数据不稳定)。
不要滥用mock,外部调用(包括数据库)尽量用个Facade封装,只需要 mock Facade即可。
sqlmap的验证需要启动容器和数据库连接,需要单独测试(后面会写到)。
实战
业务单元测试
我们使用powermock + junit进行mock ,关于使用可以参考我之前写过的一篇文章 点我查看使用,为了避免不可预知的兼容性问题,建议maven版本如下
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>3.3.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-api-mockito2</artifactId>
<version>1.7.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-module-junit4</artifactId>
<version>1.7.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.1</version>
<scope>test</scope>
</dependency>
我们来测试一个接口
UserController#add:
其主要的流程图
单测的结构
@RunWith(PowerMockRunner.class)
@PrepareForTest({RequestContextHolder.class}) // 需要 mock 类静态方法,需要在此声明
public class BaseUnitTest {
@Before
public void init() {
System.out.println("start run unit test");
}
/**
* API rpc mock
*/
@Mock // 声明这是 mock bean
private BackendUserApi backendUserApi;
@Mock
private RedisService redisService;
@Mock
private BCryptPasswordEncoder passwordEncoder;
@Before
public void before() {
/*
mock rpc result
*/
BaseResult<Integer> baseResult = new BaseResult<>();
baseResult.setCode(BaseResult.SUCCESS);
baseResult.setData(1);
PowerMockito.when(backendUserApi.addBackendUser(notNull(), notNull())).thenReturn(baseResult);
/*
mock static method http request
*/
PowerMockito.mockStatic(RequestContextHolder.class);
MockHttpServletRequest mockHttpServletRequest = new MockHttpServletRequest();
mockHttpServletRequest.setParameter(AuthConstants.AUTH_HEADER, "test");
ServletRequestAttributes requestAttributes = new ServletRequestAttributes(mockHttpServletRequest);
PowerMockito.when(RequestContextHolder.getRequestAttributes()).thenReturn(requestAttributes);
/*
mock user info
*/
PowerMockito.when(redisService.getCacheObject(anyString())).thenReturn(DataBuilder.buildUserCacheJson("unitTest"));
/*
mock encode
*/
PowerMockito.when(passwordEncoder.encode(notNull())).thenReturn("encoded");
}
/**
* 测试数据的组装
*/
public static class DataBuilder {
public static AddUserDTO buildUserDTO(String name,
String phone,
Long enrollmentDate,
String email,
String sex,
Long birthday,
String freezeStatus,
String remark,
List<Long> orgIds) {
AddUserDTO addUserDTO = new AddUserDTO();
addUserDTO.setName(name);
addUserDTO.setPhone(phone);
addUserDTO.setEnrollmentDate(enrollmentDate);
addUserDTO.setEmail(email);
addUserDTO.setSex(sex);
addUserDTO.setBirthday(birthday);
addUserDTO.setFreezeStatus(freezeStatus);
addUserDTO.setRemark(remark);
addUserDTO.setOrgIds(orgIds);
return addUserDTO;
}
/**
* 快速组装测试数据
* @param name
* @return
*/
public static AddUserDTO buildUserDTO(String name) {
// 调用重载
return buildUserDTO(name, "13011111111", 11111111111L, "test@mail.com", "1", 111L, "1", "remark", Lists.newArrayList(1L));
}
/**
* mock redis cache user info
* @param name
* @return
*/
public static String buildUserCacheJson(String name) {
com.alibaba.fastjson.JSONObject jsonObject = new com.alibaba.fastjson.JSONObject().fluentPut("userId", 1)
.fluentPut("userName", name)
.fluentPut("token", "token")
.fluentPut("orgIds", Lists.newArrayList(1))
.fluentPut("roleIds", Lists.newArrayList(1));
return jsonObject.toString();
}
}
}
public class UserControllerTest extends BaseUnitTest {
@Spy // 为一个真实的实例
@InjectMocks // 依赖的bean如果是mock注入mock实例,如果是真实bean,注入真实bean
private BackendUserController backendUserController = new BackendUserController();
@Spy
@InjectMocks
private BackendUserService backendUserService = new BackendUserServiceImpl();
@Spy
@InjectMocks
private BackendUserManager backendUserManager = new BackendUserManager();
/**
* 这里是test case,assert 结果
*/
@Test
public void testQueryByUserId() {
BaseResult<Integer> unitTest = backendUserController.add(DataBuilder.buildUserDTO("unitTest"));
Assert.assertTrue(unitTest.isSuccess());
}
}
运行junit测试
可以点击idea覆盖率统计,即可输出单测的覆盖率情况,也能高亮代码覆盖情况。
sqlmap的测试
上面的测试是真正的单元测试,没有启动spring容器(因为耗时并且没必要),很多时候我们写完sqlmap需要去测试,这个时候需要启动容器加载最小的数据源组件,下面是实践参考。
配置方式如下:
@Configuration
@ActiveProfiles("test")
public class MybatisConfiguration {
/**
* datasource
*/
@Bean
public DataSource dataSource() {
DruidDataSource dataSource = new DruidDataSource();
dataSource.setUrl("xxxxxx");
dataSource.setUsername("xxx");
dataSource.setPassword("xxxx");
dataSource.setDriverClassName("com.mysql.jdbc.Driver");
dataSource.setMinIdle(0);
dataSource.setInitialSize(0);
dataSource.setQueryTimeout(3);
dataSource.setMaxWait(3000);
dataSource.setPhyTimeoutMillis(3000);
dataSource.setMaxActive(2);
return dataSource;
}
/**这里配置SqlSessionFactory */
@Bean
public SqlSessionFactory sqlSessionFactoryBean(DataSource dataSource) throws Exception {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dataSource);
/**
* 这里值得注意的是我的一个Mapper接口对应了两个sqlmap的文件,这里试用通配符的方式加载。
* 如果不确定自己的通配符是否正确,可以动态debug到此处动态调用解析结果看是否加载到Resource[]
*/
ResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver();
Resource[] resources = resourcePatternResolver.getResources("classpath*:**/*Mapper.xml");
sqlSessionFactoryBean.setMapperLocations(resources);
return sqlSessionFactoryBean.getObject();
}
@Bean
public DataSourceTransactionManager dataSourceTransactionManager(DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
@Bean
public MapperScannerConfigurer mapperScannerConfigurer(SqlSessionFactory sqlSessionFactory) {
MapperScannerConfigurer mapperScannerConfigurer = new MapperScannerConfigurer();
mapperScannerConfigurer.setBasePackage("com.xxxx.mappaer");
mapperScannerConfigurer.setSqlSessionFactory(sqlSessionFactory);
return mapperScannerConfigurer;
}
/*
* 提供一个直接执行SQL的方式
*/
@Bean
@Autowired
public JdbcTemplate jdbcTemplate(DataSource dataSource) {
JdbcTemplate jdbcTemplate = new JdbcTemplate();
jdbcTemplate.setDataSource(dataSource);
return jdbcTemplate;
}
}
Mapper的测试
@Slf4j
@Transactional
@Rollback // 测试完毕自动回滚测试数据
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = MybatisConfiguration.class)
public class UserMapperTest {
@Autowired
private UserMappaer userMappaer;
@Autowired
private JdbcTemplate jdbcTemplate;
@Test
public void testQueryByCode() {
// 测试写入
jdbcTemplate.execute("INSERT INTO user(id, name) VALUES(1, 'micro')");
UserDO userDO = userMappaer.queryByCode("micro");
// 测试断言mapper查询的正确性
Assert.assertTrue(userDO != null && userDO.getName().equals("micro"));
}
}
h2内存数据库
上面的方案DAO的测试需要连接远程DB,这里存在的风险是测试环境数据的不稳定问题与数据的初始化问题,同时无法做到真正的“本地测试”。
可以引入H2数据库,其可以在内存构建数据库实例并完成单测运行。
下面提供使用方式
需要的maven依赖:
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.4</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>1.4.197</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<version>2.4.5</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.1</version>
<scope>test</scope>
</dependency>
application.yml
spring:
datasource:
driver-class-name: org.h2.Driver
schema: classpath:schema.sql #,每次启动程序,程序都会运行schema.sql文件,对数据库的数据操作
data: classpath:data.sql #,每次启动程序,程序都会运行data.sql文件,对数据库的数据操作
url: jdbc:h2:mem:db_users;MODE=MYSQL #配置h2数据库的连接地址
username: sa
password:
h2:
console:
enabled: true #开启web console功能
mybatis:
mapper-locations: classpath*:mappers/**/*.xml
type-aliases-package: xxx.com.entity
表结构定义DDL schema.sql
CREATE TABLE `user`
(
`id` bigint NOT NULL,
`name` varchar(20)
primary key(`id`)
);
初始化数据data.sql
INSERT INTO `user` (`id`, `name`) VALUES
(0, 'micro'),
(1, 'micro1'),
(2, 'micro2');
单测编写
@SpringBootTest(classes = {DataSourceAutoConfiguration.class, MybatisAutoConfiguration.class, MybatisConfiguration.class})
@RunWith(SpringRunner.class)
public class UserMapperTest {
@Autowired
private UserMapper userMapper;
/**
* test case
*/
@Test
public void testQueryByCode() {
UserDO userDO = userMapper.getByName("micro");
Assert.assertTrue(userDO != null && userDO.getName().equals("micro"));
}
}
总结
单元测试的威力是在持续重构中,重构是改变既有代码设计的必要手段。
不要盲目自信自己的代码, QA资源是宝贵的,开发自测是大的趋势。
评审需求的时间应该包括自测时间。