接上篇 https://blog.csdn.net/ykdsg/article/details/88410310
来说说自己的思路,一家之言不一定对。
针对上一篇的总结的几个问题:
- 白盒测试需要针对代码的内部实现逻辑,成本较高。
- 数据初始化&准确性的问题:怎么保证每次跑用例的时候数据是可控的,不会被其他人篡改。
- 需要处理复杂的依赖关系,spring 容器一起来就是一堆,一不小心还有可能有依赖缺失。
接下来会一一解答:
白盒测试成本高
借鉴BDD(行为驱动开发) 的思想,通过自然语言定义系统行为,以功能使用者的角度来定义系统行为。就是不再基于代码的内部实现而是基于功能的使用场景,1个场景可以使用多个用例去覆盖。举个例子:
Scenario Outline: 非活动大贸detail-<Mark>-<Itemid>
When 查询商品detail价格<Itemid>&<Spec>&<Num>&'<AfterLogisticType>'&'<ProductDate>'
Then detail价格是'<DetailPrice>'运费是'<Logisticfee>'总价是'<TotalPrice>'
Examples:
| Itemid | Spec | Num | AfterLogisticType | ProductDate | DetailPrice | Logisticfee | TotalPrice | Mark |
| 986 | 1 | 1 | 物流到店 | 2016-09 | 7.99 | 7.0 | 14.99 | 大贸有生产日期物流到店 |
| 986 | 1 | 1 | 供应商配送 | 2016-09 | 7.99 | 1 | 8.99 | 大贸有生产日期供应商配送 |
| 8545 | 1 | 1 | 物流到店 | 无 | 23 | 1 | 24 | 大贸无生产日期物流到店到付运费 |
| 8545 | 1 | 1 | 京东配送 | 无 | 23 | 1 | 24 | 大贸无生产日期京东配送 |
when 表示条件,then 表示验证的结果。Examples 下面是准备的用例数据,会动态替换到when和then里面。关键是要梳理好场景跟这些数据之间的关系,通过数据把场景给固化下来。关于BDD的框架还是挺多的,目前比较流行的是cucumber,上面的例子就是cucumber 的feature,关于BDD 和 cucumber 是很大的一块内容,这里不再展开。
数据初始化&准确性
一定要确保每次运行的数据都是准确可控的,不然测试用例就会变成排查是数据的问题&程序的问题。这样的测试报告就非常的鸡肋。所以需要保证数据是可控并且是隔离的。
目前java 里面用的比较多的是DBunit,通过xml和excel 能方便的进行数据初始化。接下来就是数据库的问题,首先共用一个库效果太差,每次跑用例都需要插入数据,主键肯定是不能重复的,共用一个库没办法做数据初始化的事情。能选的就是内存数据库了,选一个跟当前使用的数据库兼容较好的内存数据库,我们正式用的是mysql就选了H2,实测下来兼容性还好,就是一些比较特殊的mysql语句不支持,但是也极少碰到。最大的问题是H2的索引机制跟mysql的不一样,h2的索引名称必须全局唯一,这个在初始化表结构的时候特别坑,这里写了一个小工具来进行ddl的转化
public class TransferMysqlToH2 {
private static Pattern pattern = Pattern.compile("(?<=KEY )(.*?)(?= \\()");
private static ClassLoader classLoader = TransferMysqlToH2.class.getClassLoader();
private static String mysqlPath = "/yticp-test/src/main/resources/sql/mysql/";
private static String h2Path = "/yticp-test/src/main/resources/sql/h2/";
public static void main(String[] args) throws Exception {
//String test = "\\\"test";
//String out = test.replaceAll("\\\\\"", "\"");
//System.out.println(test);
//System.out.println(out);
convert("yt_icp_structure.sql");
}
private static void convert(String sqlFileName) throws IOException {
String sqlPath = new File(".").getCanonicalPath();
String mysqlFilePath = sqlPath + TransferMysqlToH2.mysqlPath + sqlFileName;
File file = new File(mysqlFilePath);
String content = Files.toString(file, Charsets.UTF_8);
content = "SET MODE MYSQL;\n\n" + content;
content = content.replaceAll("`", "");
//替换mysql json串中 \"itemChildType 为"itemChildType
content = content.replaceAll("\\\\\"", "\"");
content = content.replaceAll("COLLATE.*(?=D)", "");
content = content.replaceAll("COMMENT.*'(?=,)", "");
content = content.replaceAll("\\).*ENGINE.*(?=;)", ")");
content = content.replaceAll("DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP", " AS CURRENT_TIMESTAMP");
content = uniqueKey(content);
String h2FilePath = sqlPath + TransferMysqlToH2.h2Path + sqlFileName;
File h2File = new File(h2FilePath);
if (!h2File.exists()) {
h2File.createNewFile();
}
Files.write(content, h2File, Charsets.UTF_8);
}
/**
* h2的索引名必须全局唯一
*
* @param content sql建表脚本
* @return 替换索引名为全局唯一
*/
private static String uniqueKey(String content) {
int inc = 0;
Matcher matcher = pattern.matcher(content);
StringBuffer sb = new StringBuffer();
while (matcher.find()) {
matcher.appendReplacement(sb, matcher.group() + inc++);
}
matcher.appendTail(sb);
return sb.toString();
}
}
将数据源配置成h2:
<!-- H2数据源 -->
<jdbc:embedded-database id="icDataSourceTarget" type="H2">
<jdbc:script location="classpath:sql/h2/test_ddl.sql"/>
</jdbc:embedded-database>
现在表结构有了,剩下的是怎么进行数据初始化。结合上面的cucumber 可以封装一个glue来进行专门的数据初始化。
Scenario: feature级别数据初始化
Given reloadData:"sql/price/detail/noActivityGeneral.xlsx"
reloadData 的具体实现
/**
* 先清理,再插入数据
*
* @param excelPath
* @throws Throwable
*/
@Given("reloadData:{string}")
public void reloadData(String excelPath) throws Throwable {
if (StringUtils.isBlank(excelPath)) {
throw new RuntimeException("excelPath is blank");
}
Fixtures.reloadData(icDataSourceTarget, "classpath:" + excelPath);
}
Fixtures是自己封装的一个工具类,大量使用了DBunit的api,至此数据初始化的问题解决。
注意:数据初始化跟具体的用例是紧密关联的,所以数据的准备通常是匹配具体用例的。
复杂的依赖关系
很多时候我们需要运行一个简单bean中的方法,但是不得不把这个bean 需要依赖的bean都准备好,不然spring 容器就起不来。然后就会发现要起一堆的东西,很多像缓存,dubbo服务 你可能压根不会调用,但是被被强迫进行依赖。解决的思路是mock,我只维护我本次需要用到的bean,如果有其他依赖在spring 上下文中找不到就mock一个bean definition 。这里不得不说spring 极其厉害的扩展机制,让你能够非常方便hack他一整个生命周期。这里主要使用BeanDefinitionRegistryPostProcessor,简单一点的原理就是解析容器中的配置的bean,通过反射获取class 和field,看field 上有没有@Autowired或者@Resource 注解,有的话就在容器中看下是否存在,没有就mock一个。当然实际上没这么简单,因为spring的扩展性需要考虑其他一些复杂的情况,比如通过mybatis 生成的bean。这块封装在内部的ytasd-spring-mock 里面了,可以拿来直接使用。
总结:
至此基本解决了框架上的问题,剩下的就是准备数据和用例了,这块就是工作的大头。