单元测试的解答

接上篇 https://blog.csdn.net/ykdsg/article/details/88410310

来说说自己的思路,一家之言不一定对。

针对上一篇的总结的几个问题:

  1. 白盒测试需要针对代码的内部实现逻辑,成本较高。
  2. 数据初始化&准确性的问题:怎么保证每次跑用例的时候数据是可控的,不会被其他人篡改。
  3. 需要处理复杂的依赖关系,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 里面了,可以拿来直接使用。

总结:

至此基本解决了框架上的问题,剩下的就是准备数据和用例了,这块就是工作的大头。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值