npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

tdsl

v0.2.7

Published

unit test descriptiton script language

Readme

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,一些复杂用法去掉或简化