网站登录密码保存在哪里设置,网站充值提现公司账务怎么做,专业网站建设组织,h5建设网站公司本文介绍了一种秒级启动的集成测试框架#xff0c;使用该框架可以方便的修改和完善测试用例#xff0c;使得测试用例成为测试过程的产物。 背景 传统的单元测试#xff0c;测试的范围往往非常有限#xff0c;常常覆盖的是一些工具类、静态方法或者较为底层纯粹的类实现使用该框架可以方便的修改和完善测试用例使得测试用例成为测试过程的产物。 背景 传统的单元测试测试的范围往往非常有限常常覆盖的是一些工具类、静态方法或者较为底层纯粹的类实现但是一般整个应用代码是比较复杂的存在不同分层。在DDD中一般包括防腐层、领域层、服务层、应用层。越到上层的类其依赖关系越复杂这些上层的类对象往往不太适合用单测来覆盖。 但是集成测试的启动速度较慢随着工程的增大启动速度会越来越慢。这就导致修改和 Debug 测试用例变得非常耗时一部分人甚至会放弃写测试而通过系统界面或手动接口测试Postman等方式来保证功能正确性。但这样做之后在未来重构或者开发新需求时很难完整回归已有功能。完整回归已有功能将是每次发布的负担回归遗漏可能引发线上故障。 测试的执行速度至关重要这往往会影响人们是否自觉地完成测试覆盖。在实际开发过程中我们可能需要本地反复执行某些测试用例并不断修改如果应用能在10秒内启动完成那么开发是高效的否则可能会让人试图通过其他方式来测试功能的正确性。 解决方案 针对这一问题一个比较直观的想法是让集成测试执行速度和单元测试一个数量级。 一般的Java工程都使用了Spring框架其应用启动慢往往是一些涉及网络通信的Bean的初始化过程比较耗时比如RPC框架、缓存、数据库等这些中间件的Bean对象初始化都需要和外部建立网络连接等待数据推送等有的涉及多次网络通信将这些Bean完全Mock掉可大大加快应用启动速度。 很直观的解法是将这些耗时的Bean替换为MockBean有两种方式 使用Spring的Primary注解并禁止耗时Bean的初始化Mock Spring容器 第一种方式的困难在于Bean初始化的方式多种多样有的在init方法中有的通过BeanPostProcessor动态创建要精准的禁止这类逻辑的执行是比较困难的。 第二种方式则是自己实现一个Mock的Spring框架基于约定的方式实现Mock对象的自动加载以及普通Bean对象和Spring一样的方式初始化从而实现应用的快速启动。 Mock Spring容器 我们基于第二种思路实现了Mock的Spring容器但仅仅实现其了基础功能因为通常我们的工程没有用到Spring比较复杂的能力大多数工程都是如此。工程中采用约定大于配置的方式可以减少Mock的工作量。在Mock Spring框架时其实最需要的是自动构建依赖树的能力即根据当前Bean对象的依赖关系按需动态创建一系列其关联的Bean。而且对于外部依赖可以基于某种约定来优先加载Mock对象保证所有对象Bean创建是按需的且不需要网络等待这样可以实现对象依赖树秒级创建集测秒级启动。其他特殊的功能可以通过其他方式来绕过本方案也在不断完善中。从实践来看启动大约需要1-10秒。 本方案的基本思路如下 记录接口与实现类的关系是为了根据接口查找实现类实现按需加载Mock对象相当于加了Primary注解在同类型中会优先被注入保证覆盖中间件等外部依赖Bean对象初始化基础Bean对象是优先加载Configuration修饰的类中定义的Bean对象 以下是工程中定义的扫包代码片段每个测试执行Bean都是按需加载不会将所有Bean全部创建。 // 确定扫包路径扫包规则只有Component等注解修饰的类才会被注册为Bean
PredicateClass? classFilter clazz - !clazz.getSimpleName().endsWith(Test);
SetClass? beanClasses ClassScanUtil.scanPackages(classFilter, // 应用包路径com.nbf.gateway,
); 以上的应用包路径和Spring Boot应用的扫包路径一致。 以下是Bean初始化简化后的逻辑 protected T T getBeanObject(ClassT requiredType) {// 首先查找Bean的真实类型SetClass? beanClassList implClassMap.get(requiredType);int size CollectionUtils.size(beanClassList);Class? beanClass;if (size 1) {throw new BusinessException(CommonErrorCode.UNKNOWN_EXCEPTION, requiredType.getName() 包含多个实现类);} else if (size 1){beanClass beanClassList.iterator().next();} else {beanClass requiredType;}T bean;Constructor? constructor ListUtils.firstElementOf(beanClass.getConstructors());if (null constructor) {throw new BusinessException(new Exception(class: beanClass.getName() 构造器为null.));}Class?[] classes constructor.getParameterTypes();Object[] params new Object[classes.length];for (int i 0; i classes.length; i) {params[i] this.getBean(classes[i]);}try {//noinspection uncheckedbean (T)constructor.newInstance(params);} catch (Exception e) {throw new BusinessException(e);}// 处理Autowired和Resourcethis.processMemberBean(bean);// 执行初始化逻辑Method[] methods beanClass.getDeclaredMethods();for (Method method : methods) {if (method.getAnnotation(PostConstruct.class) ! null) {try {method.invoke(bean);} catch (Exception e) {throw new BusinessException(e);}}}return bean;
} 以下是在测试类中获取Bean对象的方法类似Autowired。MockApplicationContext即是我们Mock Spring容器类的名字。 public class GroupVersionRepositoryUnitTest {private final static GroupTunnel groupTunnel MockApplicationContext.getBeanOfType(GroupTunnel.class);
} Mock数据库 基于以上的思路我们还需要Mock数据库、外部依赖、中间件。下面小节将重点介绍Mock数据库的一种实现。 ▐ 第一层MockExample Mock数据库最直观的想法就是使用HashMap也在很多的工程中有用到。看到很多的实现是在测试中我们调用DAO层相关代码替换为在HashMap中操作对应数据。这样的实现有两个比较明显的缺点 每个数据操作都需要手动翻译为对Map的数据操作费时费力容易存在翻译偏差每次翻译过程需要case by case处理 当然能做到这种替换还有个前提是我们将DAO层的操作都统一封装到了一层这样才能实现使用Mock对象替换的方式实现整体替换。 解决方案 目前大多数的Java工程都使用Mybatis解决思路是实现一套类似Mybatis的查询工具类让写Mock实现和真实的DAO层方法调用类似让翻译过程尽量简单直观。 为此我们定义了MockExample对应Mybatis的查询参数ParamMockCriteria对应Criteria用户暂时不感知MockTunnelUtil对应DAOMock对象和Mybatis真实对象映射关系如下图所示 MockExample、MockCiteria都以DOData Object作为泛型参数用于指定操作哪张表 原始某段真实Mybatis查询代码如下 Override
public ApiInfoDO get(String apiInfoId) { ApiInfoQuery query new ApiInfoQuery();query.createCriteria().andApiInfoIdEqualTo(apiInfoId);ListApiInfoDO apiInfoDOList apiInfoDao.selectByQueryWithBLOBs(query);return firstElementOf(apiInfoDOList);
} 翻译后的Mock实现如下 Override
public ApiInfoDO get(String apiInfoId) { MockExampleSlsJobDO example new MockExample();example.createCriteria() .andEqualTo(apiInfoId, ApiInfoDO::getApiInfoId);ListApiInfoDO apiInfoDOList MockTunnelUtil.selectByExample(this, example);return firstElementOf(apiInfoDOList);
} 这里有三点需要对照修改 创建查询参数比如ApiInfoQuery需要替换为创建MockExample查询条件增加属性的方法引用比如ApiInfoDO::getApiInfoId使用MockTunnelUtil代替DAO进行查询 MockExample实现主要使用了断言Predicate以下是In条件的实现 public F MockCriteriaDO andIn(ListF field, FunctionDO, F getter) {PredicateDO predicate obj - field.contains(getter.apply(obj));return this.addCondition(predicate);
} ▐ 第二层MockDAO 上述实现大大简化了Mock 数据库的难度但仍然存在如下缺点 查询 修改逻辑变更Mock逻辑需要跟着变更存在比较严重的一致性问题 很多时候会忘记修改导致Mock结果和实际运行不一致 如果Mybatis调用逻辑散落各处没有统一收敛到一层则Mock比较困难 为此我们需要将Mock的层再向下降一层直接Mock DAO在测试中调用DAO则会调用到我们的Mock实现做到Mock实现不依赖业务代码变化。 思路 一个比较直观的解决方案是实现一套通用逻辑将Mybatis的Param直接转换为MockExample则不需要再手动去写那段翻译逻辑即可自动将业务实现转换为Mock实现。 难点 这里的一个难点是Mybatis生成的查询Criteria缺乏公共的父类每个方法的名称都是和用户参数名相关的比如andApiInfoIdEqualTo。 解决方案 通过分析我们可以发现其实问题的根源在于Mybatis的Example、Criteria、Criterion缺乏公共的接口或基类。为了解决这个问题我们定义了SqlParam、SqlCriterion、SqlCriteria用来抽象这三个层次的对象。以下是这三个类的定义 public interface SqlCriteriaCriterion {ListCriterion getCriteria();
} public interface SqlCriterion {String getCondition();Object getValue();Object getSecondValue();
} public interface SqlParamCriteria {/*** 是否分页*/boolean isPage();/*** 获取页码1开始*/Integer getPageIndex();/*** 获取页大小*/Integer getPageSize();/*** 获取排序语句*/String getOrderByClause();/*** 获取查询条件*/ListCriteria getOredCriteria();
} 我们在Mock DAO层的实现中定义不同DO的这三个接口实现即可这样我们就可以基于这些信息将Mybatis Param转换为MockExample了。以下是Mock DAO实现的样例 NoArgsConstructor(access AccessLevel.PRIVATE)
public class MockApiInfoDaoImpl
extends AbstractMockDaoImplApiInfoDO, ApiInfoQuery, Criteria, Criterion
implements ApiInfoDao {private static final MockApiInfoDaoImpl INSTANCE new MockApiInfoDaoImpl();public static MockApiInfoDaoImpl getInstance() {return INSTANCE;}Overridepublic FunctionApiInfoDO, Object getIdGetter() {return ApiInfoDO::getId;}Overrideprotected SqlParamCriteria getSqlParam(ApiInfoQuery query) {return new SqlParamCriteria() {Overridepublic boolean isPage() {return query.getRows() ! null;}Overridepublic Integer getPageIndex() {return query.getOffset() / query.getRows() 1;}Overridepublic Integer getPageSize() {return query.getRows();}Overridepublic String getOrderByClause() {return query.getOrderByClause();}Overridepublic ListCriteria getOredCriteria() {return query.getOredCriteria();}};}Overrideprotected SqlCriteriaCriterion getSqlCriteria(Criteria criteria) {return criteria::getCriteria;}Overrideprotected SqlCriterion getSqlCriterion(Criterion criterion) {return new SqlCriterion() {Overridepublic String getCondition() {return criterion.getCondition();}Overridepublic Object getValue() {return criterion.getValue();}Overridepublic Object getSecondValue() {return criterion.getSecondValue();}};}
} 以下是Mybatis查询条件Param转换为MockExample的转换逻辑 protected MockExampleDO convert(Param param) {MockExampleDO example new MockExample();// 设置条件boolean first true;SqlParamCriteria sqlParam getSqlParam(param);for (Criteria criteria : sqlParam.getOredCriteria()) {MockCriteriaDO mockCriteria;if (first) {mockCriteria example.createCriteria();first false;} else {mockCriteria example.or();}SqlCriteriaCriterion sqlCriteria getSqlCriteria(criteria);for (Criterion criterion : sqlCriteria.getCriteria()) {SqlCriterion sqlCriterion getSqlCriterion(criterion);String condition sqlCriterion.getCondition();int index condition.indexOf(NbfSymbolConstants.SPACE);String property NbfStringUtils.underLineToCamel(condition.substring(0, index).trim());String getterMethod get StringUtils.capitalize(property);String operator condition.substring(index 1).trim();// 添加属性ListObject valueList new ArrayList();Object value sqlCriterion.getValue();if (value ! null) {valueList.add(value);}Object secondValue sqlCriterion.getSecondValue();if (secondValue ! null) {valueList.add(secondValue);}FunctionDO, Object getter obj - {try {Method method getDoClass().getDeclaredMethod(getterMethod);return method.invoke(obj);} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {throw new BusinessException(e);}};// 操作符OperatorEnum operatorEnum OperatorEnum.of(operator);mockCriteria.and(operatorEnum, getter, valueList);}}// 设置分页if (sqlParam.isPage()) {example.setPagination(sqlParam.getPageIndex(), sqlParam.getPageSize());}// 设置排序String orderByClause sqlParam.getOrderByClause();if (StringUtils.isNotBlank(orderByClause)) {example.setOrderByClause(orderByClause);}return example;
} 不同DAO的Mock实现基本类似只要拷贝并修改泛型参数即可。 在上述的DAO层Mock实现MockApiInfoDaoImpl中继承了基类AbstractMockDaoImpl这是由于同一套Mybatis插件生成的DAO接口方法类似我们可以定义一个抽象类实现这些接口DAO层Mock实现继承该抽象类则不需要再去实现DAO层的接口了。其部分实现如下 public abstract class AbstractMockDaoImplDO, Param, Criteria, Criterionextends AbstractMockTunnelImplDO, Param, Criteria, Criterion {public long countByQuery(Param param) {MockExampleDO example this.convert(param);return MockTunnelUtil.countByExample(this, example);}public int deleteByQuery(Param param) {MockExampleDO example this.convert(param);return MockTunnelUtil.deleteByExample(this, example);}
} ▐ 小结 这里我们介绍了完整Mock数据库的一种思路这种Mock实现仍然存在一些缺陷 暂时无法支持事务无法实现数据库的特性比如必填校验等 以上两点都可以在未来支持。它的优点也是比较明显的 执行速度快不依赖数据库已有数据不会受数据库已有数据的影响不会造成脏数据 造数据 基于以上的两个基础设施Mock Spring容器、Mock数据库可以使得写测试变得更加容易对于测试中比较费时费力的造数据也可以更加快速的实现。 在日常的集成测试中造数据是一个比较麻烦的事情虽然我们使用测试的RollBack机制可以保证对现有数据无污染。但是在某些依赖已有数据的情况则比较麻烦。如果预先造了这样的数据可能被其他人无意修改。而且在一些查询场景已有数据可能对测试执行结果造成干扰。 有了这套完整的Mock工具我们可以使用线上数据进行测试更加快捷的回归 发现问题。 ▐ 造数据的几种方式 常见造数据的两种方式 通过属性设值。即各种New对象Set属性通过JSON解析文件 第一种方式的开发维护成本较高尤其是构建大对象时。 造数据的来源也有两种方式 通过DOData Object去造数据即把数据直接插入数据库通过领域对象造数据调用Repository去创建数据 根据数据来源也分为日常、线上。显然线上数据质量远高于日常更容易发现问题。 ▐ 方案 将数据库查询到的线上日常库数据转换为领域对象有比较大的转换成本如果转换为DO对象也有一定成本但成本较低。所以本方案采用了后一种方案。 但是把数据库查询出来的数据拷贝出来直接转换为DO所需要的JSON格式文件也有较高的成本所以这里直接使用字段拆分解析的方式读取其内容再反射设值到DO对象中。这里有个问题数据库查询出来的字段顺序可能和DO中字段定义顺序不一致所以需要有个元信息文件用于指定数据库查询出来数据的字段顺序。 以下是本测试框架的 TableLoadUtil#Load 方法用于将数据库查询出来的数据转换为DO数据。第一个参数对应的文件内容是数据库查询出来的各行数据第二个参数对应的文件内容是DO的字段顺序。 public class TableLoadUtil {/*** 根据元信息定义加载数据* param fileName 表数据文件路径* param metadata 表字段顺序元信息定义文件路径* param clazz DO类*/public static T ListT load(String fileName, String metadata, ClassT clazz);
} 这样就实现了通过数据库数据直接快速造数据的目的推荐使用线上数据但对敏感数据需要脱敏保证测试质量。 我们需要将测试涉及到的表的少量行数据不需要全量查询出来并添加到对应文件中。对于复杂场景这种造数据的方式显然更加高效。而且可以做到每个测试的数据都是重新初始化的互相隔离不影响。这些数据还可以在不同测试间共享。不需要启动完整的Spring容器只需要启动Mock的Spring容器保证测试启动无论工程多么庞大在10秒以内大部分测试启动在3秒以内。 比较通用的做法是在测试基类里做数据的初始化和清理具体的测试类继承该类以下是一个线上应用的测试基类 public class DataPrepareBaseOnTable {/*** 准备数据*/BeforeClasspublic static void prepare() {cleanUp();initData(BackendServiceConfigDO.class, MockBackendServiceConfigMapperImpl.getInstance()::insert);}/*** 清理数据*/AfterClasspublic static void cleanUp() {MockBackendServiceConfigMapperImpl.getInstance().getCache().clear();}/*** 初始化数据*/public static DO void initData(ClassDO clazz, ConsumerDO insertMethod) {String objName clazz.getSimpleName().substring(0, clazz.getSimpleName().length() -2);ListDO doList TableLoadUtil.load(table/ objName / objName .txt,table/Metadata/ objName .txt,clazz);NbfListUtils.forEach(doList, insertMethod);}
} 这里可以设置为整个测试类初始化 清理一次数据也可以设置为单个测试初始化一次推荐。 造数据的流程大致如下 ▐ 小结 上述小节介绍了一种通过直接将数据库查询到的数据转为测试准备数据的方案该方案的优点如下 构造数据足够简单快捷避免了测试数据被外部意外修改数据变动过程可以通过git记录查到各个测试之间测试数据隔离测试执行速度快绝大部分测试启动在10秒以内测试数据质量较高可完全使用线上数据测试数据相对干净、纯粹避免测试环境很多脏数据导致测试不稳定 以上的优点主要是相对于集成测试 RollBack的传统测试方式 该方案的成本主要在于创建字段顺序文件。但对于每张表是一次性的后续增加字段只需追加新增字段即可 可能的不足是 如果数据库新增字段可能需要更新对应表文件 如果测试不涉及新增字段大部分是向前兼容的 当数据量较大时管理表文件可能有一定成本 推荐使用文件行排序避免插入重复数据且要使得数据尽量少仅包含测试需要的行数据 Mock中间件 mock中间件相对较为简单这里仅把我们的方案做简单介绍。 ▐ Mock RPC框架 只需要创建Mock对象记录测试case情况下日常或线上该接口的出参即可推荐用JSON文件保存让Mock方法根据入参加载对应的出参JSON文件作为结果返回。 ▐ Mock Redis 也通过HashMap进行Mock即可实现复杂度取决于需要覆盖其功能的完整性。 ▐ 小结 本文介绍了一种秒级启动的集成测试框架使用该框架可以方便的修改和完善测试用例使得测试用例成为测试过程的产物。测试通过之后也同时沉淀了覆盖多种测试场景的测试用例。可以方便的使用线上数据作为数据来源保证测试的质量。甚至在遇到线上问题时可以将这些数据作为数据来源用测试用例执行来反复重现 Debug这些问题同时沉淀线上问题的测试用例保证后续代码改造或重构不会重新触发该故障。 团队介绍 物流技术基础技术团队主要技术产品NBFNew-Retail Business Framework 提供了服务DevOpsLowCode编排和云原生基础设施能力旨在成为新零售PaaS平台化和SaaS产品化的技术底座。 ¤ 拓展阅读 ¤ 3DXR技术 | 终端技术 | 音视频技术 服务端技术 | 技术质量 | 数据算法