tdsl

0.2.7 • Public • Published

TDSL (Test Domain Specific Language), 测试领域专用语言,是一套高效简洁描述测试代码的规范脚本语法。

  • 一、为什么要使用TDSL

  单元测试是前端开发中重要的一部分工作,但常常由于时间紧张,除了要写测试用例代码,还要维护旧的用例,每个人用例写法多少也有不一致,同伴的用例代码也往往会含有特殊的想法和逻辑,而且代码量大修改起来也难定位,前前后后也消耗了不少时间。不得不说,用例虽不难,但持续性开发维护会让我们投入不少精力。

  由于测试用例大部分都是单元模块IO的验证代码,核心代码比较固定和机械化,但是往往修改了代码,还要去改用例,非常闹心。实际上我们可以根据源码进行ast分析,解析出各个模块的输入输出结构,直接生成用例代码,我们只需要指令式管理IO样本数据即可。所以,这里定义了一套简洁的描述语法规则TDSL。TDSL的基础理念是:重复类似的测试用例的代码只需要工具写一次,其他的交给TDSL。

  而为了做到跨技术栈使用,可通过tdsl.config.js配置来适配自己的项目。使用简洁直接的语法书写用例,然后自动生成对应的完整jest测试用例代码。

  优势

1,不用了解框架即可手写用例,也不用维护用例代码。

2,面向指令,所有用例和模块IO一目了然。边界IO也更加直观。

3,统一用例代码,使用核心常用语法,保持一致性。

4,约束源码实现,约束一个函数只做一件IO事情,手动编写的用例过于灵活,后面其他人维护成本较高。

5,大大降低TDD成本。

6,统一IO数据样本文件管理,样本数据更易维护。

7,提高效率的同时对源码的组织结构形成反向约束。

8,覆盖率低?不存在的。

  劣势:

1,TDSL语法成本。(但是不用再去折腾测试用例api怎么写了,TDSL语法远比用例语法简单,记住一种语法规范即可)

  • 二、快速开始
npm i tdsl --save-dev

  配置script任务,运行 npm run tdsl 即可,就能自动读取文件分析出用例了,默认会在根目录生成__test__/目录,里面包含所有的用例代码。   解析的用例dsl文件需要配置,详细看 tdsl.config.js的配置。

scripts: {
    ...
    "tdsl": "node ./node_modules/tdsl/scripts/tdsl"
}

  • 三、TDSL基本语法

1、声明引用

  一般情况下,一个.test (默认扩展名为.test,可以进行配置) 文件关联一个源码文件,同时选择一种全局注入方式。注入方式在tdsl.config中配置。编写是支持常用的注释方式。

test './module.js';
use 'moduleName';

// add(1, 3) => (3)
# add(1, 3) => (3)

2、断言比较。相等 =>、深度相等 ==>、不相等 !=>、深度不相等 !==>

**调用函数方法,返回预期的校验值,如果没有returnValue,则没有断言判断输出,例如:

test './module.js';
use 'moduleName';

B(1, 3) => (3)
B(1, 3) !=> (4)
B(1, 3) ==> (3)
B(1, 3) !==> (4)
B(1, 4) => ()

  module.js代码为

export function B (a: number, b: number): number {
    return a * b;
}

  自动编译后,B(1, 4) => () 没有返回值,不会编译输出:

test("B module", () => {
    expect(B(1, 3)).toBe(3);
    expect(B(1, 3)).not.toBe(4);
    expect(B(1, 3)).toEqual(3);
    expect(B(1, 3)).not.toEqual(4);
});

3,语法换行和注释

  如果需要换行,把块注释换行前面的 * 去掉即可,另外遵循不要在过程内部换行,例如

# 换行
pullToRefresh.call('mockThis:of:../data/data.js')
    -> 1(wx.showToast)
    -> 1(setTimeout:mockof:../../../common/utils) => ()

// add(1, 2) => (3)
/* add(1, 2) => (3) */

4,常量路径 :of:

("data.dataKey1:of:path", ...params) => ('data.dataKey:of: path')

  如果我们要处理的样本数据太大,需要从外部一个数据文件中导出使用,就可以使用:of:。使用:of:分割,:of: 前为读取mock数据的属性key,如果没有key则表示无输入,后面为相对路径,此时参数输入必须为字符串,相对路径最终会根据生成用例文件的路径对比,计算出一个新的相对路径,而且相同重复的路径最终会自动合并到一个import语句。

test './module.js';
use 'moduleName';

A("a:of:./data.js", 3) => (3)
A("b:of:./data.js", "d:of:./data1.js") => (1)
A("a:of:./data.js", 1) => ("a.b:of:./data1.js")

  ./module.js文件内容为

export function B (a: number, b: number): number {
    return a * b;
}

  其中data.js和data1.js文件内容为:(支持export和module.exports导出)

// data.js
module.exports = {
    a: 1,
    b: 1
};

// data1.js
export default {
    c: 1,
    d: 1
}

  自动编译后,常量路径会被自动计算并填充到变量:

import { a, b } from '../data.js';
import { c, d } from '../data1.js';
test("B module", () => {
    expect(B(a, 3)).toBe(3);
    export(B(b, d)).toBe(1);
    export(B(a, 1)).toBe(a.b);
});

5, 析构参数路径 [Number]...[key]:of:

("[number]...data.dataKey1:of:path", ...params) => ('data.dataKey:of: path')

  常量路径能够帮助我们引入文件数据,简化脚本长度,但是如果有时需要引入多个常量路径,脚本仍不够简洁,这时就可以使用析构常量路径。注意此时常量路径输出必须是数组类型才生效,其中数字表示使用前面多少个参数,方便不同个数参数复用,如果不设置,则自动为函数的最大参数个数计算。

test './module.js';
use 'moduleName';

A("2...params:of:./data.js", 3) => (6)
A("...params1:of:./data.js") => (6)

  ./module.js文件内容为

export function B (a: number, b: number,  c: number): number {
    return a + b + c;
}

  其中data.js文件内容为:(支持export和module.exports导出)

// data.js
module.exports = {
    params: [1, 2],
    params1: [1, 2, 3]
};

  自动编译后,常量路径会被自动计算并填充到变量:

import { params, params1 } from '../data.js';
test("B module", () => {
    expect(B(params[0], params[1], 3)).toBe(6);
    expect(B(params1[0], params1[1], params1[2])).toBe(6);
});

6, 属性判断

fn(...Params).property => (lengthValue)

  属性读取判断,所以尽可能使用这条规则实现更多的原始判断,例如:

test './module.js';
use 'moduleName';

B(1, 3).length => (1)
B(1, 3)['length'] => (1)
B(1, 3)['length']['xxx'] !=> (1)

  ./module.js文件内容为

export function B (a: number, b: number): number {
    return a * b;
}

  自动编译后输出:

test("B module", () => {
    expect(B(1, 3).length).toBe(1);
    expect(B(1, 3)['length']).toBe(1);
    expect(B(1, 3)['length']['xxx']).not.toBe(1);
});

7,异步判断

fn(...params) -> (module.property) ==> (returnValue)

  有时我们需要异步执行一个函数,然后再判断某个变量是否符合预期。例如:异步执行fn,然后判断module的property属性值是否和returnValue相符。注意:这里因为右侧只能写一个输出数据,所以中间过程仅支持一项,如果有多个,请分多条书写。

test './module.js';
use 'moduleName';

// 拉取接口数据,然后把接口数据dispatch到store上,然后判断store的数据是否符合预期
getOverviewChart({}) -> (getState().apiData) ==> ({chart: {a:1}})

  ./module.js文件内容为

export const getOverviewChart = (params = {}) => {
  // ...request操作,然后dispatch数据到store的apiData
  ...
}

  编译后输出:

test("getOverviewChart module", done => {
  getOverviewChart({}).then(() => {
    expect(getState().apiData).toEqual({
      chart: {
        a: 1
      }
    });
    done();
  }, () => {});
})

  如果是async异步函数,则会解析编译为:

test("async getOverviewChart module", async done => {
    await getOverviewChart({});
    expect(getState().apiData).toEqual({
      chart: {
        a: 1
      }
    });
})

8, 二阶异步判断

moduleName(fn(...params)) -> (module.property) -> (returnValue)

 有时模块函数可能是高阶函数,需要嵌套一次调用测试,例如: dispatch(A())。目前最多支持二阶函数。

test './module.js';
use 'moduleName';

// 拉取接口数据,然后把接口数据dispatch到store上,然后判断store的数据是否符合预期

dispatch(getOverviewChart({})) -> (getState().apiData) ==> ({chart: {a:1}})

  ./module.js文件内容为

export const getOverviewChart = (params = {}) => (dispatch, getState) => {
  // ...request操作,然后dispatch数据到store的apiData
  ...
}

  编译后输出

test("getOverviewChart module", done => {
  dispatch(getOverviewChart({})).then(() => {
    expect(getState().apiData).toEqual({
      chart: {
        a: 1
      }
    });
    done();
  }, () => {});
})

9, 函数调用 :of:、:mockof:、:from:

9.1、fn(...params) -> number1('module1:of:path') -> ... => (returnValue)

  常常我们会去测试一个事件触发时是否调用了一些数据业务逻辑,这种情况可以使用调用次数的判断。例如,fn调用时调用module1次数为number1,调用module2次数为number1,module1来自path路径的文件,module1和module2均会被自动mock,然后返回断言值为returnValue。中间的调用判断支持多项,并支持自动合并。注意这里 number1('module1:of:path') -> (module) 不能和异步判断混用。如果需要多次判断,请分多条规则书写。

test './module.js';
use 'moduleName';

//执行initData时,分别执行了1次onChangeChartDate和1次getAdvertiserListInfo,其中两个函数都来自../action模块
initData({})
    -> 1(onChangeChartDate:of:../action)
    -> 1(getAdvertiserListInfo:of:../action) => ()

  ./module.js文件内容为

export const initData = function (options = {}) {
    onChangeChartDate(time);
    getAdvertiserListInfo({page: 1});
}

  这里的模块路径均会被自动重新计算,编译后输出:

import { onChangeChartDate, getAdvertiserListInfo } from '../action';
test("initData module", () => {
  jest.clearAllMocks();
  initData();
  expect(onChangeChartDate).toBeCalledTimes(1);
  expect(getAdvertiserListInfo).toBeCalledTimes(1);
});

9.2、fn(...params) -> number1('module1:mockof:path') -> ... => (returnValue),如果需要对引入的方法进行mock,则可使用:mockof:

test './module.js';
use 'moduleName';

// 执行initData时,分别执行了1次onChangeChartDate和1次getAdvertiserListInfo,其中两个函数都来自../action模块

initData({}) -> 1(onChangeChartDate:mockof:../action)
    -> 1(getAdvertiserListInfo:mockof:../action) => ()

  ./module.js文件内容为

export const initData = function (options = {}) {
    onChangeChartDate(time);
    getAdvertiserListInfo({page: 1});
}

  这里的模块路径均会被自动重新计算和自动mock,编译后输出:

import { onChangeChartDate, getAdvertiserListInfo } from '../action';
jest.mock('../action', () => {
  return {
    onChangeChartDate: jest.fn(),
    getAdvertiserListInfo: jest.fn(),
  };
});
test("initData module", () => {
  jest.clearAllMocks();
  initData();
  expect(onChangeChartDate).toBeCalledTimes(1);
  expect(getAdvertiserListInfo).toBeCalledTimes(1);
});

9.3、fn(...params) -> number1('module1:mockof:path:from:path1') -> ... => (returnValue),此外如果需要对引入的方法进行自定义mock,则可使用:from:,其中path1的路径为自定义mock函数的路径地址,注意这里path和path1导出module1的名称必须相同

test './module.js';
use 'moduleName';

// 执行initData时,执行了1次onChangeChartDate,其中onChangeChartDate的自定义mock函数为../mock.js中的onChangeChartDate;getAdvertiserListInfo和7.2相同
initData({}) -> 1(onChangeChartDate:mockof:../action:from:../mock.js) -> 1(getAdvertiserListInfo:mockof:../action) => ()

  ./module.js文件内容为

export const initData = function (options = {}) {
    onChangeChartDate(time);
    getAdvertiserListInfo({page: 1});
}

  这里的模块路径均会被自动重新计算和自动mock,编译后输出:

import { onChangeChartDate as _onChangeChartDate, } from '../mock.js';
import { onChangeChartDate, getAdvertiserListInfo, } from '../action';
jest.mock('../action', () => {
  return {
    onChangeChartDate: _onChangeChartDate,
    getAdvertiserListInfo: jest.fn(),
  };
});
test("initData module", () => {
  jest.clearAllMocks();
  initData();
  expect(onChangeChartDate).toBeCalledTimes(1);
  expect(getAdvertiserListInfo).toBeCalledTimes(1);
});

10, this绑定call、apply

fn.call(this, ...params) -> number1('module1:of:path') -> ... => (returnValue)

fn.apply(this, [...params]) -> number1('module1:of:path') -> ... => (returnValue)

  有时验证一个模块时,由于模块可能依赖了this的一些方法和属性,才能运行,此时我们就需要mock一个this,然后用call或apply进行绑定。推荐使用call,apply后面数组元素如果引用了路径不会被解析。前面的方法模块均支持和call或apply绑定。

test './module.js';
use 'moduleName';

initData.call('mockThis:of:./data.js', {}) -> 1(getMessageList:of:./action) => (true)

  ./module.js文件内容为

export const initData = function (options = {}) {
    getMessageList({page: 1});
    this.setData({
      a: 1,
    });
    return true;
}
// 其中./data.js中包含

export const mockThis = {
  setData: () => {},
  xxx...
}

  编译后输出:

import { mockThis } from "../data.js";
import { getMessageList } from '../action';
jest.mock('../action', () => {
  return {
    getMessageList: jest.fn()
  };
});
test("initData module", () => {
  jest.clearAllMocks();
  initData.call(mockThis, {});
  expect(getMessageList).toBeCalledTimes(1);
  expect(initData.call(mockThis, {})).toBeCalledTimes(1);
});

四、tdsl.config配置

  tdsl是基于jest的跨框架单元测试描述脚本语法,tdsl.config.js用于配置项目,项目中tdsl会自动查找项目中的tdsl.config.js配置文件。

  一个tdsl.config.js配置例子如下:


module.exports = {
    ruleReg: /\.test/,   // 匹配哪些文件进行tdsl解析
    rootDirs: ['./test', './src'], // 匹配哪些目录下文件进行解析
    module: [{
        name: 'moduleName1',  // 模块的名称,DSL中使用use "moduleName1" 来关联
        async: 'async',        // 模块函数是否使用async方式测试
        inject: {               // 注入的内容,会出现在编译后代码的前后面
            // 会出现在测试代码文件头部
            beforeAllCode: `
                import { useDispatch, initStore, getState } from '../../../initStore';
                initStore({});
                const dispatch = useDispatch();
            `,
            // 会出现在测试代码文件尾部
            afterAllCode: '',
        }
    }, {
        name: 'moduleName2',
        test: 'src/biz-components/.*/index.js',
        inject: {
            beforeAllCode: `
            jest.clearAllMocks();
            jest.mock('../../../initStore',() => {
                return {
                    useDispatch: jest.fn(),
                }
            });
            const simulate = require('miniprogram-simulate');
            `,
            afterAllCode: ``,
        },
        ui: 'miniprogram',  // 标识为ui类用例,并且是小程序类ui
    }]
};

五、测试用例类型

  tdsl目前定义了四种类型的函数模块用例:

  • (1) 同步数据处理转换类。用户输入数据,处理后输出判断是否符合预期。例如同步计算两个数字相加,
add(1, 2) => (3)
  • (2) 异步数据操作并判断操作后数据。例如异步请求接口,并把接口返回的数据dispatch到store上,然后判断store上数据是否符合预期(可能需要结合接口mock一起使用)。
requestAndSetStore.callby('dispatch', {paramId: 1003}) -> (getState().xx) ==> ({list:[]})
  • (3) 事件触发函数测试。一般用于测试触发函数执行时,是否调用了对应了业务逻辑函数,以及业务逻辑函数的次数。
 initData({}) -> 1('onChangeChartDate:of:../action') -> 1('getAdvertiserListInfo:of:../action') => (true)

  • (4) UI类测试。调用render函数后查找节点,判断其个数、类名、属性等是否符合预期 (待完善)

对于React技术栈UI测试为例,npm i jest jest-babel @babel/preset-es2015 @babel/preset-env @babel/preset-react,package.json可如下配置

  "jest": {
    "testRegex": "test/test.jsx?$",
    "moduleDirectories": [
      "node_modules"
    ],
    "transform": {
      "^.+\\.js": "<rootDir>/node_modules/babel-jest"
    },
    "collectCoverage": true
  }

.babelrc配置(babel7的配置)

{
  "presets": ["@babel/preset-env", "@babel/react"]
}

TODO: 1, 编译语法出错提示 2,编辑器语法支持(暂时可使用coffeescript,HLSL等语法支持高亮和快捷注释) 3,一些复杂用法去掉或简化

Readme

Keywords

none

Package Sidebar

Install

npm i tdsl

Weekly Downloads

1

Version

0.2.7

License

ISC

Unpacked Size

63.9 kB

Total Files

15

Last publish

Collaborators

  • ouvenzhang