• JavaScript 测试基础,TDD、BDD、Benchmark


    原则:谁开发,谁测试。

    注意: 原则上应该先写测试,再进行编码;如果需求时间紧,可以先进行功能实现,但务必后续维护时候将测试代码补充完善。

    BDD(优先)+TDD(完全代码覆盖)

    测试框架

    常见的组合:

    • ES5: mocha + istanbul
    • ES6: ava + nyc

    TDD

    Test Driven Development,(单元)测试驱动开发

    特点:

    1. 直接引用对应源码,执行方法进行测试;
    2. 测试用例须设计完整,把所有分支都 Cover 到。

    示例:

    describe('Lib Common', function () {
      'use strict';
      it('isEmpty', function () {
        // isObject
        isEmpty({}).should.be.equal(true);
        isEmpty([]).should.be.equal(true);
        isEmpty({ a: 1 }).should.be.equal(false);
        isEmpty([1, 2]).should.be.equal(false);
        // isString
        isEmpty('').should.be.equal(true);
        isEmpty('sth').should.be.equal(false);
        // isNumber
        isEmpty(0).should.be.equal(true);
        isEmpty(0.1).should.be.equal(false);
        // null and undefined
        isEmpty(null).should.be.equal(true);
        isEmpty(undefined).should.be.equal(true);
        // boolean
        isEmpty(false).should.be.equal(true);
        isEmpty(true).should.be.equal(false);
        // 最后一行false
        isEmpty(isEmpty).should.be.equal(false);
      });
      it('md5/sha1', function () {
        md5('sth').should.equal('7c8db9682ee40fd2f3e5d9e71034b717');
        sha1('sth').should.equal('dec981e3bbb165d021029c42291faf06f59827c1');
      });
      it('authcode', function () {
        authcode(authcode('test'), 'DECODE').should.be.equal('test');
        authcode(authcode('test', 'ENCODE', 'key'), 'DECODE', 'key').should.be.equal('test');
        authcode('c008AsZqmGL8VuEVpZKVlbPwXzSsCZ+YX5K5CAGpMMqn', 'DECODE').should.be.equal('');
      });
    });
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    BDD

    Behavior Driven Development,行为驱动开发

    特点:

    1. 运行系统,模拟用户请求进行访问;
    2. 行为分析要完整,要将可能所有结果覆盖。

    示例:

    /* 测试路由 */
    app.get('/test/model/mysql/init/ok', function (req, res) {
      'use strict';
      return db
        .opensips('v1/subscriber')
        .then(function () {
          res.send(200, 'ok');
        })
        .catch(function (err) {
          logger('routes/test/model/mysql/ok', err);
          res.send(403, 'fail');
        });
    });
    
    app.get('/test/model/mysql/init/fail', function (req, res) {
      'use strict';
      return db
        .opensips('test/notExisted')
        .then(function () {
          res.send(200, 'OK');
        })
        .catch(function () {
          res.send(200, 'fail');
        });
    });
    
    /* 测试脚本 */
    describe('Demo', function () {
      'use strict';
      it('404 not found', function (next) {
        request(app)
          .get('/sth/not/exist')
          .set('Accept', 'text/plain')
          .expect(200)
          .end(function (err, res) {
            if (err) {
              throw err;
            }
            should(res.body.status).be.equal(0);
            next();
          });
      });
      it('403 not allowed', function (next) {
        request(app)
          .get('/v2/basic/mqtt')
          .set('Accept', 'text/plain')
          .expect(200)
          .end(function (err, res) {
            if (err) {
              throw err;
            }
            should(res.body.status).be.equal(0);
            next();
          });
      });
      it('Init opensips/subscriber Should be OK', function (next) {
        request(app)
          .get('/test/model/mysql/init/ok')
          .set('Accept', 'text/plain')
          .expect(200)
          .expect('ok')
          .end(function (err) {
            if (err) {
              //console.log(res.body);
              throw err;
            }
            next();
          });
      });
      it('Init test/subscriber Should be FAILED', function (next) {
        request(app)
          .get('/test/model/mysql/init/fail')
          .set('Accept', 'text/plain')
          .expect(200)
          .expect('fail')
          .end(function (err) {
            if (err) {
              //console.log(res.body);
              throw err;
            }
            next();
          });
      });
    });
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84

    ES6 下的 BDD 测试示例对比:

    import { test, server, assert } from './_import';
    let location;
    test.before(async () => {
      const response = await server.inject({
        method: 'POST',
        url: '/login',
        payload: {
          username: 'willin',
          password: 'PASSWORD'
        }
      });
      location = response.headers.location;
    });
    
    test('GET / 302', async () => {
      const response = await server.inject({
        method: 'GET',
        url: '/'
      });
      assert.equal(response.statusCode, 302);
    });
    
    test('GET /login 200', async () => {
      const response = await server.inject({
        method: 'GET',
        url: '/login'
      });
      assert.equal(response.statusCode, 200);
    });
    
    test('POST /login 302', async () => {
      const response = await server.inject({
        method: 'POST',
        url: '/login',
        payload: {
          username: 'willin',
          password: 'PASSWORD'
        }
      });
      assert.equal(response.statusCode, 302);
    });
    
    test('POST /login 401', async () => {
      const response = await server.inject({
        method: 'POST',
        url: '/login',
        payload: {
          username: 'willin',
          password: 'Ww10842073305zZa28v3PO5Ok0L63IdA'
        }
      });
      assert.equal(response.statusCode, 401);
    });
    
    test('POST /login Invalid Params 403', async () => {
      const response = await server.inject({
        method: 'POST',
        url: '/login',
        payload: {
          username: 'willin'
        }
      });
      assert.equal(response.statusCode, 403);
    });
    
    test('GET /doc 200', async () => {
      const response = await server.inject({
        method: 'GET',
        url: location
      });
      assert.equal(response.statusCode, 200);
    });
    
    test('GET /doc 302', async () => {
      const response = await server.inject({
        method: 'GET',
        url: '/doc?'
      });
      assert.equal(response.statusCode, 302);
    });
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80

    Benchmark

    性能对比测试框架 Matcha: https://github.com/logicalparadox/matcha

    使用场景

    技术选型,如图形验证码,在 NPM 包选取使用canvas还是ccap时可以用。

    或,一个问题,有多种解决方案,选择采用哪一种方案的时候。

    注意: 所有需要做选择的场景,最好都先做一下对比。

    结果报告示例

    ATL (After v1.0.1)
      if > (true) .................................... 4,752,967 op/s
      if = (true) .................................... 4,653,896 op/s
      if < (false) ................................... 4,612,560 op/s
    
    Left Shift (ATL v1.0.0)
      << > (true) .................................... 2,562,098 op/s
      << = (true) .................................... 2,473,787 op/s
      << < (false) ................................... 2,458,286 op/s
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    示例代码

    suite('ATL', function () {
      bench('if > (true)', function () {
        atl('1.6.7', '1.4.4');
      });
      bench('if = (true)', function () {
        atl('1.4.4', '1.4.4');
      });
      bench('if < (false)', function () {
        atl('1.1.6', '1.4.4');
      });
    });
    
    suite('Left Shift', function () {
      bench('<< > (true)', function () {
        atls('1.6.7', '1.4.4');
      });
      bench('<< = (true)', function () {
        atls('1.4.4', '1.4.4');
      });
      bench('<< < (false)', function () {
        atls('1.1.6', '1.4.4');
      });
    });
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    源码位于: https://github.com/WulianCC/node-atl/blob/master/benchmark/parse.js

    当我写一段测试的时候,我在想些什么

    按照上面推荐方式完成代码后,需要进行代码的测试。

    首先需要明确业务的流程,理清测试的思路。

    • 成功
    • 错误
      • 错误 1:用户未加入组织
      • 错误 2:传入参数组织不存在
      • 错误 3:用户无组织权限

    主要有两种设计思路:

    设计思路

    思路一
    1. 完成测试用例,覆盖成功的所有情况
    2. 完成测试用例,覆盖错误 1 的所有情况
    3. 完成测试用例,覆盖错误 2 的所有情况
    4. 完成测试用例,覆盖错误 3 的所有情况

    这是传统的单元测试衍生而来的 BDD 测试方式。

    这里测试用例的个数应该为8次:

    • 成功:
      • 1.当前组织的用户有传入组织 oid
      • 2.当前组织的用户未传入组织 oid
      • 3-5.上级组织,上上级组织,根级组织的管理员用户传入组织 oid
    • 6.失败 1:用户未加入组织
    • 7.失败 2:传入参数组织不存在
    • 8.失败 3:用户无组织权限

    其中,测试 3-5 可以优化为一次测试(即根据所有管理员 uid 的数组比较是否包含当前用户 uid),最终优化后的结果应当为6次。

    但由于该思路中不明确用户,所以用户行为无法准确表达,在创建测试数据的时候较为困难,不仔细思考分析,无法优化需要创建多少条测试数据。

    思路二

    而实际上 BDD 测试为用户行为测试,可以以几类用户的情形分别进行测试。

    1. 模拟一个用户的数据,覆盖成功和可能错误(有可能无法涵盖到所有错误)的所有情况
    2. 根据未覆盖的部分,再模拟另一个用户的数据,覆盖成功和可能错误(有可能无法涵盖到所有错误)的所有情况

    以此循环,直至覆盖所有。

    • 用户 1(非组织管理员,查询自己的组织)
      • 1.成功(未传入组织 oid)(组织 1)
      • 2.成功(传入组织 oid)
      • 3.失败 2:传入参数组织不存在
      • 4.失败 3:用户无组织权限(组织 2)
    • 用户 2(上级某组织管理员)(组织 3)
      • 5.成功
    • 用户 3(未加入组织用户)
      • 6.失败 1:用户未加入组织

    非常简洁明了的关系,需要 3 个测试用户,3 个组织(上下级关系进行数据复用,一个无权限的组织),即可涵盖所有范围。

    最终优化版设计:

    • 用户 1(某组织管理员,有下级组织)
      • 1.成功(未传入组织 oid,查询自己的组织)
      • 2.成功(传入当前的组织 oid(组织 1))
      • 3.成功(传入下级的组织 oid(组织 2))
      • 4.失败 2:传入参数组织不存在
      • 5.失败 3:用户无组织权限
    • 用户 2(未加入组织用户)
      • 6.失败 1:用户未加入组织(组织 3)

    两个用户,三个组织。完成所有覆盖。

    当我以测试驱动开发的时候,我在想些什么

    可以从上述测试思路二中进行反推。

    实际上思路可能是在写代码或者写测试的过程中不断的改进和完善的。

    • 如果已经写好了测试正在写代码,可以及时回过头来调整测试;
    • 如果功能写好了又再重新测试,可以在测试优化后再去看逻辑代码是否还有优化的空间。
  • 相关阅读:
    Spring源码系列-第7章-AOP的执行流程原理和监听器原理
    C# Redis NReJSON 实现对RedisJson的使用,听说比ES快500倍
    计算机网络-传输层(TCP可靠传输(校验,序号,确认,重传),TCP流量控制,TCP拥塞控制(慢开始,拥塞避免)(快重传,快恢复))
    DDS的一点理解
    shader 开发实战
    [C++随想录] 优先级队列
    使用C语言+USRP B210从零开始实现无线通信(5) 纠错与输出
    解决创建maven工程时,产生“找不到插件的错误”
    RocketMQ入门
    力扣刷题(简单篇):两数之和、两数相加、无重复字符的最长子串
  • 原文地址:https://blog.csdn.net/jslygwx/article/details/131871798