在单元测试中实现逻辑以缩短语法并使测试代码更易于重用

时间:2015-02-18 14:33:51

标签: javascript unit-testing

我经常听到开发人员说测试代码应该“丑陋”,尽可能简单。

原因在于,测试中的任何逻辑都需要自我测试,并且会产生鸡蛋和蛋悖论。

我发现自己通过使用一些简单的逻辑使我的测试更具可读性,结构化和测试代码更加可重复使用。

问题是那些有效的单元测试吗?

我使用karma作为测试运行器,这个特定项目使用带有connect-asset-manager的节点服务器,以及前端的bower包。

如何使代码更加模块化(AMD?browserify?),而无需从头开始实现所有内容或引入框架。

目前,全局状态在许多文件中都有变化,而且很多闭包,我不喜欢它。 如何使闭包内的代码仍可测试?也许this package

示例代码:

utils.js

function getRealTemplatValues (inputs, templateFn, outerId, innerClass) {
    var i, res;
    for (i = 0; i < inputs.length; i++) {
        res = templateFn(inputs[i]);
        $('#' + outerId).append(res);
    }
    return $('.' + innerClass).map(function(){
        return $(this).html();
    }).get();
};


function assertEqual (done, expect, inputs, outputs, fn, decorator) {
    function iter (input, output, cb) {
        var i;
        for (i = 0; i < inputs.length; i++) {
            cb(input[i], output[i]);
        }
    };

    function cb (input, output) {
        output = !!decorator ? decorator(output) : output;
        expect(fn(input, decorator)).toBe(output);
        done && done();
    }

    iter(inputs, outputs, cb, decorator);
};

helper.scenario.js

describe('helpers', function () {
    var inputs = ["#string#string#string", "string#string", "string#string#string#", "#", "###"],
        outputs = ["#string #string #string", "string #string", "string #string #string#", "#", "###"],
        decorString = 'magic!',
        outerId = "container",
        innerClass = "inner",
        decorator = function(input) {return input + decorString};


   describe('breakHashtags - unit', function(done) {
       it('should break hashtags with prefixed with spaces - non decorated', function (done) {
           return assertEqual(done, expect, inputs, outputs, breakHashtags);
       });

       it('should break hashtags with prefixed with spaces - decorated', function () {
           return assertEqual(done, expect, inputs, outputs, breakHashtags, decorator);
       });
   });

    describe('handle bars integration', function () {
        var outerId = "container",
            outerClass = "inner",
            fixture = '<div id="' + outerId + '"></div>',
            template = Handlebars.compile('<div class="' + outerClass + '">{{breakHashtags hashtags}}</div>'),
            decoratedTemplate = Handlebars.compile('<div id="inner">{{breakHashtags hashtags decoratorHelper}}</div>');
        beforeEach( function () {
            addFixture(fixture);
        });
        afterEach( function () {
            clearMyFixtures();
            Handlebars.helpers['decoratorHelper'] && delete Handlebars.helpers['decoratorHelper'];
        });

        it('should have the breakHashtags function registered as a helper', function () {
            expect(Handlebars.helpers['breakHashtags']).toEqual(breakHashtags);
        });

        it('should replace hashtags with hashtags prefixed with spaces', function(){
            var realValues = getRealTemplatValues(inputs, template, outerId, outerClass);
            assertEqual(done, expect, inputs, realValues, breakHashtags);
        });

        it('should replace hashtags with hashtags prefixed with ' +
            'spaces and invoke the decorator on theo put', function(){
            Handlebars.registerHelper('decoratorHelper', decorator);
            var realValues = getRealTemplatValues(inputs, decoratedTemplate, outerId, outerClass);
            assertEqual(done, expect, inputs, realValues, breakHashtags, decorator);
        });
    });

});

helpers.js:

function breakHashtags (text, decorator) {
    var pattern = /\w(#).+/i,
        p1, p2, idx = text.search(pattern), prefix = '';
    while (idx > 0) {
        if (idx === 1) {
            text = text.substring(idx);
            prefix = text.substring(0, 1);
        }
        else{
            p1 = text.substring(0, idx + 1);
            p2 = text.substring(idx + 1);
            text = p1 + ' ' + p2;
            console.log(p1, p2, text)
        }

        idx = text.search(pattern);
    }
    return !!decorator ? decorator(prefix + text) : prefix + text;

}

Handlebars.registerHelper('breakHashtags', breakHashtags);

1 个答案:

答案 0 :(得分:2)

我的意见: 我觉得你太过分了。例如:

it('should replace hashtags with hashtags prefixed with spaces', function(){
            var realValues = getRealTemplatValues(inputs, template, outerId, outerClass);
            assertEqual(done, expect, inputs, realValues, breakHashtags);
});

它可能对您更具可读性,但仅限于您。编程是团队游戏。当其他人第一次看到该代码时,他不知道发生了什么。 该测试中有描述,但它与您实际测试的内容无关。不知道你的代码究竟做了什么,但测试看起来应该是这样的:

it('should replace hashtags with hashtags prefixed with spaces', function(){
            var result = testedFunction("#hashtag1#hashtag2#hashtag3");
            assertThat(result).isEqualTo("#hashTag1 #hashTag2 #hashTag3");
});

现在每个测试都是一个完整的测试。没有人必须检查不同的文件来理解它。如果有人改变你的助手功能怎么办?你会注意到吗?

  

如何使代码更加模块化(AMD?browserify?),而不必   从头开始实现一切或引入框架。

框架有什么问题?毕竟你刚开始写自己的。最好使用第三方框架,因为团队中的每个人都知道它究竟是做什么的。当你写自己的助手时,每个人都必须从头开始学习。 此外,最好编写帮助程序,使您的测试在一般方面更强大。随意为参数化测试或通用断言创建简单的框架。它非常清楚它是如何工作的,它做什么,何时以及如何改变它。所以你可以在所有测试中使用它们

但是当你创建像assertEqual (done, expect, inputs, outputs, fn, decorator)这样的函数时,没有人知道它的作用。为什么地狱assertEquals有这么多参数?它应该至多2-3(实际的,预期的,错误消息)。我可以在需要时更改断言等级吗?或者我应该保留它,因为它对某人很重要,只需复制粘贴它?这样的测试是维护的噩梦

  

我经常听到开发人员说测试代码应该“丑陋”,并且   尽可能简单。

是的,他们应该是明白的,不,不应该是丑陋的。这是你的代码所以重构它。但重构的方式是使它们成为可读的文档。每一项测试都应该是它自己的文档的一小部分。适合所有人,不只是为了你。无需在其他帮助文件或函数中查找任何内容