动机

TestNG 是一个非常优秀的框架,功能丰富、简单易用,但是它的 DataProvider 用起来确实有点蛋疼:不支持外部数据源(得自己写读取的代码),散落在各处的 DataProvider 方法,无限重复的 return new Object[] {} 语句,以及万一手滑返回值和参数不对应就抛出的异常实在是令人发狂,于是天真的我开始幻想这些能不能自动完成呢?答案是当然能!好了,闲言少叙,这里要实现的就是标题说提到的——TestNG 通用化数据驱动,具体如下:

原理

其实核心原理很简单,就是根据反射机制获取当前测试方法,然后遍历参数列表,根据不同的类型返回对应的值即可

结构设计

要支持不同格式的外部数据源首先要定义一个统一的数据格式,此处命名为 IRow,然后再定义一个接口 IReader,负责从外部读入数据并转换为 IRow

public interface IRow {

    int getIndex();

    String[] getColumnNames();

    int getColumnNumbers();

    boolean getBoolean(String key);

    boolean getBoolean(int index);

    // 省略部分方法

    String getString(String key);

    String getString(int index);

}

public interface IDataReader {

    void changeGroup(String group);

    IRow readRow();

    void close();

}

为了保证不同测试方法的数据相互独立,需要引入一个数据分组的概念,类似于 Excel 中的 sheet 或者数据库中 table,因此再定义一个接口 IGroupResolver 用于根据当前测试方法解析出其对应的数据分组名

public interface IGroupResolver {

    String resolve(Method method);

}

定义支持的参数类型,遍历参数列表生成 Object[][];此处可以简单的使用 if-else 语句或者 switch 语句,但是为了提高扩展性,我使用了组合模式(我不会告诉你这个是我从 SpringMVC 框架的 HandlerMethodArgumentResolverComposite 类抄的 😆

public class ArgumentResolverComposite implements IArgumentResolver {
    private List<IArgumentResolver> mResolvers = new LinkedList<>();
    private Map<Parameter, IArgumentResolver> mResolverCache = new HashMap<>();

    @Override
    public boolean support(Parameter param) {
        return getResolver(param) != null;
    }

    @Override
    public Object resolve(Parameter param, IRow row) {
        IArgumentResolver vResolver = getResolver(param);
        if (vResolver == null) {
            throw new IllegalArgumentException("Unknown parameter type [" + param.getType().getName() + "]");
        }
        return vResolver.resolve(param, row);
    }

    public ArgumentResolverComposite addResolver(IArgumentResolver resolver) {
        if (resolver != null)
            mResolvers.add(resolver);
        return this;
    }

    private IArgumentResolver getResolver(Parameter param) {
        IArgumentResolver vResolver = mResolverCache.get(param);
        if (vResolver == null) {
            for (IArgumentResolver r : mResolvers) {
                if (r.support(param)) {
                    vResolver = r;
                    mResolverCache.put(param, r);
                    break;
                }
            }
        }
        return vResolver;
    }

}

为了支持并发需要在 IDataReader 上动手脚,考虑到各个线程间彼此独立,使用 ThreadLocal 最合适不过了,为了减少重复代码,搞一个 AbstractReader 定义 ThreadLocal 的相关存取操作

public abstract class AbstractReader<T> implements IDataReader {
    private ThreadLocal<T> mData = new ThreadLocal<>();

    @Override
    public void changeGroup(String group) {
        T vLocal = getLocalData();
        if (vLocal == null) {
            vLocal = newLocalData(group);
            mData.set(vLocal);
        } else {
            resetLocalData(vLocal, group);
        }
    }

    @Override
    public boolean supportQuery() {
        return false;
    }

    @Override
    public void setCriteria(String criteria) {
    }

    protected T getLocalData() {
        return mData.get();
    }

    protected void removeLocalData() {
        mData.remove();
    }

    protected abstract void resetLocalData(T data, String group);

    protected abstract T newLocalData(String group);

}

大体结构就是这样,其余就是写实现类了,就再赘述,想详细了解的直接看代码吧

用法

设置 DataProviderX 配置信息

配置信息中可以设置 IDataReader、IGroupResolver、IRowFilter、Criteria

示例代码:

// 使用默认配置:IDataReader使用ExcelReader,IGroupResolver使用DefaultGroupResolver
DPXConfig config = DPXConfig.buildDefault("data.xls"); 
DataProviderX.setConfig(config);

// 也可以自己创建
IDataReader reader = new DBReader("jdbc:sqlite:data.db", "org.sqlite.JDBC");
IGroupResolver groupResovler = new DefaultGroupResolver();
DPXConfig config = new DPXConfig(reader, groupResovler);
DataProviderX.setConfig(config);

设置测试方法注解

示例代码:

@Test(dataProvider = DataProviderX.NAME, dataProviderClass = DataProviderX.class)

DataProviderX 提供了 4 个 DataProvider 方法,均可通过常量引用,分别是

常量 说明
DataProviderX.NAME 返回 Object[][] 的 DataProvider
DataProviderX.NAME_PARALLEL 返回 Object[][] 的 DataProvider,启用并发
DataProviderX.NAME_LAZY 返回 Iterator的 DataProvider
DataProviderX.NAME_LAZY_PARALLEL 返回 Iterator的 DataProvider,启用并发

设置数据分组名

在使用 DefaultGroupResolver 时,可以在测试方法上添加 DataGroup 注解来声明该方法的分组名,否则将使用 类名_方法名 作为分组名;若使用其他的 IGroupResolver,请按照其规则设置数据分组名

支持的参数类型

DataProviderX 默认支持以下类型的参数:

可以通过添加 IArgumentResolver 的实现类来让 DataProviderX 支持更多类型的参数,步骤如下:

  1. 创建一个类,实现 IArgumentResolver 接口,定义支持的参数格式和解析过程
  2. 调用 DataProviderX.RESOLVERS.addResolver() 方法添加刚才的类

示例代码:

/**
 * 解析String[]类型的参数
 */
public class ArrayResolver implements IArgumentResolver {

    @Override
    public boolean support(Parameter param) {
        return param.getType() == String[].class;
    }

    @Override
    public Object resolve(Parameter param, IRow row) {
        int len = row.getColumnNumbers();
        String[] arr = new String[len];
        for (int i = 0; i < len; ++i) {
            arr[i] = row.getString(i);
        }
        return arr;
    }

}

// 在使用DataProviderX前调用以下代码即可添加对String[]类型参数的支持
DataProviderX.RESOLVERS.addResolver(new ArrayResolver());

数据过滤

过滤数据有两种方式:

  1. 在 DPXConfig 中设置全局过滤器
  2. 在测试方法上添加 Filter 注解声明过滤器

注:测试方法上声明的过滤属性将会覆盖全局过滤属性

代码地址

github

欢迎大家批评指教 😄


↙↙↙阅读原文可查看相关链接,并与作者交流