使用Mocha测试node应用

[TOC]

前言

在使用node开发iconfont平台时,由于没有产品与设计的主导,我遇到了协同开发的一大难题——合并代码。开发过程中每次合并代码时基本上都有冲突,在手动解决冲突的过程中,随着代码量的增大,解决过程我真是如履薄冰,生怕改错了逻辑,导致一些原本的功能出错等后果。而每次合并完提交前,都要将所有的功能手动测试一遍,费时费力。

基于以上的原因,编写测试来保证应用的健壮性,减低协同开发的成本是非常有必要的。而且,node社区已经有成千上万的开源模块,当开发者使用第三方模块时,没有提供测试的第三方模块值得信赖嘛?对于开发者而言,应该对自己产出的代码负责。

单元测试

单元测试主要包含断言,测试框架,测试用例,测试覆盖率,mock,持续集成等几个方面,在用Mocha对node应用进行测试时,我以下面几个方面为例进行介绍:

Mocha—Javascript测试框架

Mocha is a feature-rich JavaScript test framework running on Node.js and the browser, making asynchronous testing simple and fun. Mocha tests run serially, allowing for flexible and accurate reporting, while mapping uncaught exceptions to the correct test cases. Hosted on GitHub. —— MochaJS

可以在其官网介绍中看出,Mocha是具有强大测试功能的测试框架:

  • 断言库支持
  • 钩子函数
  • 异步代码测试和超时控制支持
  • 测试报告
  • ...
断言

断言(assertion)是一种放在程序设计中的一阶逻辑(如一个结果为真或为假的逻辑判断式)

Mocha支持你用任何一种断言库,无论是should.js、chai、expect.js、better-assert等等。也可以用node原始的assert。这里我们就用expect.js

expect(window.r).to.be(undefined);
expect({ a: 'b' }).to.eql({ a: 'b' })
expect(5).to.be.a('number');
expect([]).to.be.an('array');
expect(window).not.to.be.an(Image);
测试风格

Mocha支持BDD(行为驱动开发)TDD(测试驱动开发)两种测试风格,BDD对于TDD来说在关注点更关注整体行为是否符合预期,在表达方式上更接近于自然语言的习惯。Mocha的默认模式是BDD,在此我们只关注BDD模式。

钩子函数

BDD风格的钩子函数有:before, after, beforeEach, afterEach 典型BDD风格测试:

var assert = require("assert");
describe('Array', function(){

    before(function() {
        // ...
    })

    describe('#indexOf()', function(){
        it('should return -1 when the value is not present', function(){
            assert.equal(-1, [1,2,3].indexOf(5));
            assert.equal(-1, [1,2,3].indexOf(0));
        })
    })

    after(funtion() {
        // ...
    })

});

Demo

在此我以测试iconfont平台首页的展示功能为例: 注意编写测试代码时最重要的两件事就是:

  • Mock数据
  • 解决诸如异步、超时控制等问题

在下面的代码中,我是以测试路由接口的形式,通过测试返回的html字符串与构造的mock数据相比的方法来测试的。在实际应用中,有远比这展示功能复杂的功能,比如搜索功能,可以通过rewire来获取routes/search.js中私有方法search,来测试,比较回调函数中参数对象。

代码如下:

var http = require('http'),
    fs = require('fs');
var expect = require('expect.js'),
    ejs = require('ejs'),
    rewire = require('rewire'),
    EventEmitter = require('eventemitter2').EventEmitter2,
    emitter = new EventEmitter();
var Icon = require('../models/icon.js'),
    Business = require('../models/Business.js'),
    svgParser = require('../utils/svg_parser.js');

var options = {
    host: 'localhost',
    port: '4001',
    path: '/',
    method: 'GET'
};
var objs = [
    {name: 'PC 平台', pm: 'moxhe'},
    {name: 'H5公众账号', pm: 'moxhe'},
    {name: '上课web化', pm: 'moxhe'}
];
var icons = [
    [{name: 'right', path: '/right.svg'}],
    [{name: 'search', path: '/search.svg'}],
    [{name: 'list', path: '/list.svg'}]
];
var renderParams = {
    all: {},
    user: undefined,
    bMaps: {}
};

describe('router/main.js', function(){

    before(function (done) {
        // tell mocha disable timeouts
        // default is 2000
        this.timeout(0);

        Icon.remove(function () {
            Business.remove(function () {
                objs.forEach(function (obj) {
                    (new Business(obj)).save(function (err, doc) {
                        bids.push(doc.bid);
                        renderParams.bMaps[doc.bid] = doc.name;
                        emitter.emit('bid ready');
                    });
                });
            });
        });
        var bids = [];
        var count = 0;
        emitter.on('bid ready', function () {
            count++;
            if (count !== objs.length) return;
            icons.forEach(function (arr, index) {
                var bid = bids[index];
                arr.forEach(function (icon) {
                    icon.business = bid;
                    var icon = new Icon(icon);
                    icon.save(function (err, doc) {
                        if (!renderParams.all[doc.business]) {
                            renderParams.all[doc.business] = [];
                        }
                        // special inject
                        doc.content = svgParser.generateHtmlIconContent(doc.iconId);
                        renderParams.all[doc.business].push(doc);
                        emitter.emit('insert success');
                    });
                });
            });
        });
        var count2 = 0;
        emitter.on('insert success', function () {
            count2++;
            if (count2 >= 3) {
                console.log('db ready!');
                done();
            }
        });
    });

    describe('router ["/", "/index"]', function() {
        it('should display all icons in business order', function (done) {
            this.timeout(5000);
            // var getAllIcons = rewire('../routes/main.js').__get__('getAllIcons');
            // getAllIcons(function (err, icons, ret) {
            //     expect(icons).to.eql(renderParams.all);
            //     done();
            // });
            var content = fs.readFileSync('../views/index.html').toString();
            renderParams.filename = __dirname + '/../views/index.ejs';
            var html = ejs.render(content, renderParams);//.replace(/\s/g, '');

            http.get(options, function (res) {
                var str = '';
                res.on('data', function (trunk) {
                    str += trunk;
                });
                res.on('end', function () {
                    expect(str).to.eql(html);
                    // expect(str.replace(/\s/g, '')).to.eql(html);
                    done();
                });
            });
        });
    });

    after(function() {
        // ...
    });

});

让我们来看看运行的结果:

小结

编写测试用例也是一门重要的学问,所谓测试驱动开发,本应该先写测试后开发,从而保证应用的健壮性,当然这个应用也必须足够分量。我觉得这还是蛮科学的,但是身边普遍部署测试的时候都是不得不部署的时候才开始的。相信当实践经验足够丰富时,对各种业务逻辑足够熟悉时就能科学地开发吧!

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏程序员互动联盟

程序语言变形记

随着科技的发展我们生活中接触到的应用程序越来越多,它给我们的生活带来了很大的便利。移动端安卓,苹果大肆横行;pc上QQ,浏览器大行天下。我们在享受这些软件给我们...

3105
来自专栏机器学习和数学

[编程经验] 我是如何半自动抓取素材公社图片的

网络爬虫是一件比较繁琐的事情,特别考验人的耐心。但又是非常令人着迷的一件事,因为当你从网络上爬到了自己的想要的数据,满满的成就感油然而生。但是我对爬虫掌握的并不...

3265
来自专栏LET

谈谈JavaScript代码优化

1826
来自专栏FreeBuf

一种常规Android脱壳技术的拓展(附工具)

最近在做移动安全测试的项目时,遇到了最新的某数字壳,好久都没脱壳了,记得上次脱壳还是zjdroid通杀的时候。秉着安全研究的精神,趁此机会,又把最新的加固与脱壳...

2688
来自专栏杨建荣的学习笔记

需要了解的pssh(r11笔记第28天)

昨天的一篇文章,关于ssh命令的几个使用小技巧(r11笔记第27天),也收到了不少朋友的反馈,其中有个朋友提议说还是用pssh吧,我想想也是。 对...

3196
来自专栏python3

python语音识别

语音识别技术,也被称为自动语音识别,目标是以电脑自动将人类的语音内容转换为相应的文字。应用包括语音拨号、语音导航、室内设备控制、语音文档检索、简单的听写数据录入...

2474
来自专栏北京马哥教育

让弹幕文明一点的Python屏蔽功能小实验

突然想到一个视频里面弹幕被和谐的一满屏的*号觉得很有趣,然后就想用python来试试写写看,结果还真玩出了点效果,思路是首先你得有一个脏话存放的仓库好到时候检测...

2715
来自专栏IT大咖说

解读移动端的跨平台开发:TypeScript + Angular

摘要 Google技术经理陈亮将为大家介绍TypeScript和Angular是什么以及如何利用TypeScript和Angular进行移动端的跨平台介绍。 ?...

3548
来自专栏Crossin的编程教室

如何直观地理解程序的运行过程?

了解代码的执行过程是编程的基本要求。一个熟练的编程老手只需要用肉眼看着代码,就能对其运行的过程有所了解。然而对于刚接触编程不久的新手来说,这种事情就没那么显而易...

2494
来自专栏Vamei实验室

Linux文本流

我之前已经用文本编辑器修改过文本。现在,我们要深入理解所谓的“文本”。 文本流 在计算机中,所谓的数据就是0或1的二进制序列,但严格来说,Unix以字节(byt...

1909

扫码关注云+社区