supNate的新天地 ——学的越多,知道的越少
用DOH实现Javascript,Ajax应用的单元测试
来自:supNate@ supNate的新天地 日期:2009-6-24 23:47:22 全文阅读:loading... 分类:技术专区
【摘要】
当我花费了2个小时才发现把enalbed拼写错误之后,郁闷之后痛定思痛,决定在我的新项目中完全彻底坚定的落实好单元测试。保证代码质量的同时也能提高开发效率,而且还能够帮助更好的划分模块。
服务器端自然好办,VS2008自带的测试框架非常强大,轻松就能搞定。但javascript端,大家都知道的,还是有点麻烦的。大概比较多的人会用JSUnit,借助JUnit的大名,想必质量不会太差。但我没用过,也就没啥好评论。
这里我要介绍的是Dojo框架自带的javascript测试框架D.O.H,全称是Dojo Objective Harness。用过之后感觉功能很强大,可惜的是文档相当稀少,官方仅有的一篇:http://www.dojotoolkit.org/book/dojo-book-0-9/part-4-meta-dojo/d-o-h-unit-testing。抗议者甚多,认为文章中假设太多,看过之后仍然摸不着头脑。但在我勇于探索勇于研究的精神指引下(轻拍),还是基本掌握其要领,遂写此文章一篇与大家共享。

似乎废话多了点。。言归正传,尽量用最简洁的文章来让大家迅速上手
目录:
1. DOH特点
2. DOH下载
3. Hello DOH!
4. 用统一界面运行单元测试
5. 单独的JS单元测试文件
6. 单元测试对象(Test Fixture)
7. 分组测试
8. 异步测试
9. 总结

【全文】

1. DOH特点

  • 既能够测试基于Dojo框架的ajax应用,也能够测试基于非Dojo框架的ajax应用(老版本只支持Dojo框架的)。最新版本现在是0.9,而且是08年就发布的,可见其还是相当稳定和够用的。
  • 能够将多个单元测试用例(多个js,html)文件,在同一个界面上集中展示测试结果。
  • 既可以在浏览器中运行测试,也可以用命令行的方式来运行测试,命令行方式仅限于纯js代码的测试。因为本人暂不需要这个功能,且浏览器测试里也能包含js纯代码测试,故本人没做研究,本文不做介绍。

2. DOH下载

最直接的方式就是去下载一个Dojo框架,里面自带DOH。http://www.dojotoolkit.org/downloads。当前Dojo最新版本是1.31。这个页面下载的是压缩版,源码不可读且无注释。
如果要源码可以到:http://download.dojotoolkit.org/release-1.3.1/。有源码有文档。
解压后得到的目录是:


如果不用Dojo框架做开发,就只需要doh这个目录来做单元测试。

3. Hello doh!

国际惯例,也是经典,我们来简单的写一个测试快速上手DOH。根据DOH特点,分2种情况,一种是基于dojo的,另一种是不基于dojo的。两者的区别首先是引入DOH框架的方法不一样。
基于Dojo的可以这样写:
<html>
<head>
<script src="dojoroot/dojo/dojo.js"></script>
<script type="text/javascript">
dojo.require('doh.runner');
</script>
</head>
<body></body>
</html>

不基于Dojo则需要手动引入doh的runner文件:
<html>
<head>
<script src="doh/runner.js"></script>
</head>
<body></body>
</html>

看上去还是不用dojo方便点呵呵。实际上doh为了支持非dojo,写了很多冗余代码,背后做的工作还是很多的。
引入doh框架后就可以用它来做单元测试了,下面除非特别说明,我都会基于dojo框架来写,但相信大家即使没用过dojo,也能看的懂。

完整的Hello doh在这:
<html>
<head>
<script type="text/javascript" src="../dojo/dojo.js"></script>
<script type="text/javascript">
dojo.require('doh.runner');

//The function to apply unit test
function add(a, b){
    return a + b;
}
//Register test cases to doh framework
doh.register('hellodoh.add', [
    function add2positive(){
        var a = 1, b = 2;
        doh.is(3, add(a, b));
    }
   
    ,function add2negative(){
        var a = -1, b = -2;
        doh.is(-3, add(a, b));
    }
]);

//Important! Need to manually call doh.run().
doh.run();
</script>
</head>
<body></body>
</html>

和dojo, dijit, util目录同级创建一个supnate文件夹,把上面的代码保存为supnate/hellodoh.html。运行之,用Firefox的话可以在Firebug里看到如下测试结果:


可以看到我们注册了一个名字为hellodoh.add的测试,用于测试add函数,里面有两个测试用例,分别测试两个正数相加和两个负数相加。doh.is等价与doh.assertEqual,用过单元测试的应该都知道其含义,表示判断2个值相等。
doh一共有3个assert方法,分别是:
doh.assertEqual(a, b)   //equals to doh.is
doh.assertTrue(condition)    //equals to doh.t
doh.assertFalse(condition)  //equals to doh.f


怎么样,DOH用起来还是很简单的吧。我们把要测试的js文件包含进来,就可以用这样的方法来测试其中的代码了。
但这仅是最简单的一种应用,缺点是我们需要借助Firebug才能看到输出内容,测试结果不够明显,而且一次只能运行一个单元测试文件。下面我们来看更复杂一点的应用。

4. 用统一界面运行单元测试

在实际的项目中,肯定需要大量的单独的单元测试文件,这些单元测试或者是一个html文件(如Hello doh),或者是一个单独的js文件(后面介绍),那么我们自然不可能每次测试都一个个去运行这些文件,而是需要一种机制把所有的测试一次运行。这就是下面介绍的所谓统一界面。
为了告诉DOH我们有哪些测试用例(文件),我们需要一个单独的js文件来包含所有的测试用例文件。根据Dojo框架本身的DOH实践,通常是创建一个名为module.js的文件。这个文件利用doh提供的一些方法来包含所有的测试用例。最重要的一个方法就是:
doh.registerUrl(name, url);
其中name表示这个单元测试的名字,url表示了运行这个测试的文件,例如上面介绍的supnate/hellodoh.html文件。就可以写成:
doh.registerUrl('hellodoh.add', dojo.moduleUrl('supnate', 'hellodoh.html'));
这里的dojo.moduleUrl用来生成正确的相对路径,你也可以用绝对路径/dojo/supnate/hellodoh.html来代替。

为了体现集中测试的好处,我们把hellodoh.html复制一份,重命名为hellodoh2.html作为第二个单元测试文件,和hellodoh.html一起,添加到supnate/module.js中去:
dojo.provide('supnate.module');

try{
    doh.registerUrl('hellodoh.add', dojo.moduleUrl('supnate', 'hellodoh.html'));
    doh.registerUrl('hellodoh2.add', dojo.moduleUrl('supnate', 'hellodoh2.html'));
}catch(e){
    doh.debug(e);
}

再创建一个runTests.html的文件,这个文件仅仅是从语义上的需要,作用是设定参数并且重定向到实际的doh单元测试框架页面,runTests.html的内容如下:
<html>
    <head>
    <title>Supnate Unit Test Runner</title>
    <meta http-equiv="REFRESH" content="0;url=../util/doh/runner.html?testModule=supnate.module"/>
    </head>
    <body>
        Redirecting to D.O.H runner.
    </body>
</html>

这时我们就仅需要在浏览器中输入:http://localhost/dojo/supnate/runTests.html就能看到所有的测试结果:

可以看到,测试结果以很好的格式组织起来,无需借助Firebug就能很清楚的看到。为了结果明显,我们修改一下测试文件,把第二个hellodoh2.html中的add函数故意写错:
function add(a, b){
    return 3;
}


那么将得到这样的结果页:

可以很明显的看到错误的结果。

5. 单独的JS单元测试文件

上面的例子虽然没有用到Html元素,但是在测试例子中完全可以把Html节点相关的操作加入到测试中去,也就是说可以测试带界面的功能。虽然这种方法也可以很好的测试纯粹的JS代码,只要包含进去即可,但有时候我们希望尽量减少不必要的干扰,只需要把测试放到单独的js文件中去,这时就需要把js文件注册到doh中去。这个功能主要就是在module.js中完成,原理就是动态的载入用于测试的js文件,和动态载入其他js文件完全一样。如果基于Dojo框架,那就仅仅是简单的一句dojo.require(.....)。其他的Ajax框架虽然我用的不多,但相信一定有自己的动态载入机制。
类似的,我们把原来在hellodoh.html文件中的测试代码复制出来创建一个supnate/hellodoh.js文件,里面包含了测试代码:
dojo.provide('supnate.hellodoh');

//The function to apply unit test, should be imported using other ways in real case
function add(a, b){
    return a + b;
}
//Register test cases to doh framework
doh.register('hellodoh.pure_js_add', [
    function add2positive(){
        var a = 1, b = 2;
        doh.is(3, add(a, b));
    }
   
    ,function add2negative(){
        var a = -1, b = -2;
        doh.is(-3, add(a, b));
    }
]);

注意这里我们为测试的名字命名为hellodoh.pure_js_add。
底下我们借助module.js把这个js文件加入到doh的测试集合中去,将module.js文件的内容修改为:
dojo.provide('supnate.module');

try{
    doh.registerUrl('hellodoh.add', dojo.moduleUrl('supnate', 'hellodoh.html'));
    doh.registerUrl('hellodoh2.add', dojo.moduleUrl('supnate', 'hellodoh2.html'));
    dojo.require('supnate.hellodoh');
}catch(e){
    doh.debug(e);
}
在浏览器中输入:http://localhost/dojo/supnate/runTests.html可以看到测试结果:

可以看到,用纯JS写的单元测试在名字上也会更加清楚。

6. 单元测试对象(Test Fixture)

英文是Test Fixture,暂且翻译为单元测试对象。。前面介绍的一个单元测试用例就是一个函数,里面做了所有的测试操作,但用过C#或者Java单元测试的都知道,单元测试执行通常会有初始化,执行,释放资源这样的过程。对于简单的测试来说可以在一个函数全部完成,但对于复杂的测试来说,把这些过程分开会更好点,DOH也支持这种复杂格式的单元测试用例,它的格式是一个对象,例如:
// a test fixture
{
        name: "thingerTest",
        setUp: function(){
                this.thingerToTest = new Thinger();
                this.thingerToTest.doStuffToInit();
        },
        runTest: function(){
                doh.assertEqual("blah", this.thingerToTest.blahProp);
                doh.assertFalse(this.thingerToTest.falseProp);
                // ...
        },
        tearDown: function(){
        }
}

这个对象它包含name,setUp,runTest,tearDown这么几个对象属性/方法。构成一个完成的测试用例。在注册时,它和其他函数的注册方式完全一样,例如:
doh.register('tests', [
    function functionTest(){
        //do testing...
    }
    ,{
        name: 'testFixture'
        ,setUp: function(){}
        ,runTest: function(){}
        ,tearDown: function(){}
    }
]);

7. 分组测试

有时候多个单元测试用例可能会用到相同的初始化代码,这时我们可以用doh.registerGroup方法来注册这样的测试用例,语法如下:
doh.registerGroup(/*string*/name, /*array*/testCases, /*function*/setUp, /*function*/tearDown);

参数含义如下:
name: 测试的名字
testCases: 测试用例集合
setUp: 测试开始之前做的初始化,是一个函数
tearDown: 测试完成后执行,是一个函数
为便于理解,贴一段官方文档的示例代码:
dojo.provide("tests.fullGroupTest");
dojo.require("tests.runner");
doh.registerGroup("tests.fullGroupTest",
    [
        // single test, no test fixture
        function assertTrueTest(t){ t.t(true); },
        // string variant of the same:
        "doh.t(true);",
        // test that uses variable set up by group
        function assertTrueTest(t){
            t.t(tests.fullGroupTest._localVariable);
        },
        // ...
    ],
    function(){ // setUp
        tests.fullGroupTest._localVariable = true;
    },
    function(){ // tearDown
        tests.fullGroupTest._localVariable = false;
    }
);


8. 异步测试(XMlHttpRequest)

在Ajax开发中经常用到异步请求服务器数据,需要等待返回,为了对其进行单元测试,doh是通过doh.Deffered实现的,个人感觉很容易理解,就继续直接官方文档的一段示例代码:
doh.register("tests.moduleToBeTested",
    // the async  text fixture
    {
        name: "thingerTest",
        timeout: 2000, // 2 seconds, defaults to half a second
        setUp: function(){
            this.thingerToTest = new Thinger();
            this.thingerToTest.doStuffToInit();
        },
        runTest: function(){
            var testCondition = true;
            var d = new doh.Deferred();
            setTimeout(function(){
                    try{
                            if(testCondition){
                                    d.callback(true);
                            }else{
                                    d.errback(new Error("we got a failure"));
                            }
                    }catch(e){
                            d.errback(e);
                    }
            }, 100);
            return d;
        }
    }
);


9. 总结

坚信只有自己动手做一遍才能真正领会,就不贴Hello doh源代码了,大家有任何问题欢迎讨论。
【全文结束】
【评论1】时间:2009-6-29 20:59:37作者:bb
看不懂瓦
但是猛顶新项目[s:14]
【评论2】时间:2009-9-2 11:04:49作者:看不明白
我基础不好
【评论3】时间:2009-9-10 7:29:53作者:sdf
sdf
【评论4】时间:2009-9-10 7:30:05作者:sadfsad
asdfsadfasdf
【评论5】时间:2009-10-14 21:13:24作者:Star
确实不错!调用url来加载测试很好,不过就是要是调试多了不知道资源释放得了不?
发表您的评论:
署名:记住我
主页:
内容: