# tdsl

> unit test descriptiton script language

Latest version **0.2.7** (published 2020-06-24) · ISC license · 0 weekly downloads

## Install

```sh
npm install tdsl
pnpm add tdsl
yarn add tdsl
bun add tdsl
```

Provides the command `tdsl`.

## Health

**Score 15/100 (F)** — status: abandoned.

Positive: no vulnerabilities.

Warnings: low downloads; no types; no esm support; pre 1.0.

Negative: abandoned; low maintenance score.

## Facts

| | |
|---|---|
| Version | 0.2.7 |
| Published | 2020-06-24 |
| First published | 2020-03-26 |
| Weekly downloads | 0 |
| License | ISC |
| TypeScript types | none |
| Module format | CommonJS |
| Dependencies | 7 |
| Unpacked size | 62.4 KB |
| Known vulnerabilities | 0 |
| Install scripts | no |
| Maintainers | ouvenzhang |

## Links

- npm: https://www.npmjs.com/package/tdsl
- npm.io page: https://npm.io/package/tdsl

## Dependencies (7)

- [lodash](https://npm.io/package/lodash.md) ^4.17.15
- [fs-extra](https://npm.io/package/fs-extra.md) ^9.0.0
- [through2](https://npm.io/package/through2.md) ^3.0.1
- [typescript](https://npm.io/package/typescript.md) ^3.8.3
- [@babel/parser](https://npm.io/package/@babel/parser.md) ^7.9.4
- [@babel/generator](https://npm.io/package/@babel/generator.md) ^7.9.4
- [@typescript-eslint/typescript-estree](https://npm.io/package/@typescript-eslint/typescript-estree.md) ^2.27.0

## Recent versions

- 0.2.7 (latest) — 2020-06-24
- 0.2.6 — 2020-06-23
- 0.2.5 — 2020-06-23
- 0.2.4 — 2020-06-23
- 0.2.3 — 2020-06-23
- 0.2.1 — 2020-06-23
- 0.0.22 — 2020-05-19
- 0.1.6 — 2020-04-03
- 0.1.5 — 2020-04-03
- 0.1.4 — 2020-04-02
- 0.1.3 — 2020-04-01
- 0.1.2 — 2020-04-01
- 0.1.1 — 2020-04-01
- 0.1.0 — 2020-04-01
- 0.0.6 — 2020-03-27
- … 5 more at https://npm.io/package/tdsl/versions

## README

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

- **一、为什么要使用TDSL**

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

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

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

&emsp;&emsp;**优势**：

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

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

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

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

5，大大降低TDD成本。

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

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

8，覆盖率低？不存在的。

&emsp;&emsp;**劣势**: 

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


- **二、快速开始**


```
npm i tdsl --save-dev
```

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

```

- **三、TDSL基本语法**

#### 1、声明引用

&emsp;&emsp;一般情况下，一个.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) => ()

```
&emsp;&emsp;module.js代码为
```
export function B (a: number, b: number): number {
    return a * b;
}
```

&emsp;&emsp;自动编译后，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，语法换行和注释

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

```
# 换行
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')**

&emsp;&emsp;如果我们要处理的样本数据太大，需要从外部一个数据文件中导出使用，就可以使用: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")
```
&emsp;&emsp;./module.js文件内容为

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

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

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

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

&emsp;&emsp;自动编译后，常量路径会被自动计算并填充到变量：

```
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')**

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


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

A("2...params:of:./data.js", 3) => (6)
A("...params1:of:./data.js") => (6)
```
&emsp;&emsp;./module.js文件内容为

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

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

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

&emsp;&emsp;自动编译后，常量路径会被自动计算并填充到变量：

```
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)**

&emsp;&emsp;属性读取判断，所以尽可能使用这条规则实现更多的原始判断，例如：

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

B(1, 3).length => (1)
B(1, 3)['length'] => (1)
B(1, 3)['length']['xxx'] !=> (1)
```
&emsp;&emsp;./module.js文件内容为

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

&emsp;&emsp;自动编译后输出：

```
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)**

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

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

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

```
&emsp;&emsp;./module.js文件内容为

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

```

&emsp;&emsp;编译后输出：

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

&emsp;&emsp;如果是async异步函数，则会解析编译为：

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

#### 8, 二阶异步判断

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

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

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

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

dispatch(getOverviewChart({})) -> (getState().apiData) ==> ({chart: {a:1}})
```
&emsp;&emsp;./module.js文件内容为

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

&emsp;&emsp;编译后输出

```
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)**

&emsp;&emsp;常常我们会去测试一个事件触发时是否调用了一些数据业务逻辑，这种情况可以使用调用次数的判断。例如，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) => ()
```
&emsp;&emsp;./module.js文件内容为

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

```
&emsp;&emsp;这里的模块路径均会被自动重新计算，编译后输出：

```
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) => ()
```
&emsp;&emsp;./module.js文件内容为

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

```

&emsp;&emsp;这里的模块路径均会被自动重新计算和自动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) => ()
```
&emsp;&emsp;./module.js文件内容为

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

```

&emsp;&emsp;这里的模块路径均会被自动重新计算和自动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)**

&emsp;&emsp;有时验证一个模块时，由于模块可能依赖了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)
```
&emsp;&emsp;./module.js文件内容为

```
export const initData = function (options = {}) {
    getMessageList({page: 1});
    this.setData({
      a: 1,
    });
    return true;
}
```

```
// 其中./data.js中包含

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

&emsp;&emsp;编译后输出：

```
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配置

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

&emsp;&emsp;一个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
    }]
};
````

#### 五、测试用例类型

&emsp;&emsp;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，一些复杂用法去掉或简化

---
_Source: https://npm.io/package/tdsl · Machine-readable twin of the npm.io package page. Health data is recomputed on every publish._
