那些依赖于用户操作的代码,不能通过执行函数来测试。通常元素的事件使用异步函数,例如click,这些需要模拟。
你可以使用jQuery的 trigger()方法来触发事件,然后测试预期的行为。如果你不想浏览器事件被触发,你可以使用triggerHandler()来执行事件相关方法。这对于测试链接的click事件是有帮助的,因为trigger()可能会使浏览器改变地址栏信息,这恐怕不是测试过程中想要发生的。假设我们有一个简单的
KeyLogger需要测试:
function KeyLogger( target ) { if ( !(this instanceof KeyLogger) ) { return new KeyLogger( target ); } this.target = target; this.log = []; var self = this; this.target.off( "keydown" ).on( "keydown", function( event ) { self.log.push( event.keyCode ); }); }
我们可以手动的触发keypress事件,然后观察logger是否工作:
test( "keylogger api behavior", function() { var event, $doc = $( document ), keys = KeyLogger( $doc ); // trigger event event = $.Event( "keydown" ); event.keyCode = 9; $doc.trigger( event ); // verify expected behavior equal( keys.log.length, 1, "a key was logged" ); equal( keys.log[ 0 ], 9, "correct key was logged" ); });
如果你的事件处理不依赖任何特定的事件属性,你可以调用trigger(eventType)。但如果你的事件处理依赖于特殊的事件属性,你就需要使用$.Event创建一个事件对象,并设置必要的属性。对于复杂的行为触发相关事件是非常重要的,例如dragging,他由mousedown、mousemove和mouseup组成。即使是看起来很简单的事件也有可能是有很多事件组成的,例如click是由mousedown、mouseup和click组成的。你是否需要触发所有三个事件,依赖于你的测试代码,只触发click大多数情况是满足要求的。
如果那些仍然不够,你就需要一个框架来帮你模拟用户事件了:
当测试集中在一起的时候,原本应该通过的测试会失败,本该失败的测试会通过,因为之前测试的副作用,使测试结果失效。
test( "2 asserts", function() { var $fixture = $( "#qunit-fixture" ); $fixture.append( "<div>hello!</div>" ); equal( $( "div", $fixture ).length, 1, "div added successfully!" ); $fixture.append( "<span>hello!</span>" ); equal( $( "span", $fixture ).length, 1, "span added successfully!" ); });
第一个append()添加了一个div,第二个equal()并没有把它考虑进去。
使用test()方法保持测试的原子性,使每一个断言清洁而不存在副作用。你应该只依赖 #qunit-fixture元素内部的
fixture标签,修改和依赖其他东西将会存在副作用。
test( "Appends a div", function() { var $fixture = $( "#qunit-fixture" ); $fixture.append( "<div>hello!</div>" ); equal( $( "div", $fixture ).length, 1, "div added successfully!" ); }); test( "Appends a span", function() { var $fixture = $( "#qunit-fixture" ); $fixture.append("<span>hello!</span>" ); equal( $( "span", $fixture ).length, 1, "span added successfully!" ); });
QUnit会在每次测试之后重置 #qunit-fixture中的元素,移出已经存在的事件。如果你只是使用了fixture内部的元素,每次测试之后你不需要执行手工操作来保持它的原子性。
#qunit-fixture和过滤("
高效发展"会讲到)之外,QUnit还提供了noglobals标志,看看下面的测试:
test( "global pollution", function() { window.pollute = true; ok( pollute, "nasty pollution" ); });
一般情况下,测试会得到成功的结果。但是选中noglobals后ok()得到失败的结果,这是因为QUnit认为他污染了window对象。在任何时候都没有必要使用那个标志,但是在整合第三方类库的时候,他对于判断是否引起全局命名污染是有帮助的。而且他也能帮助去发现由于副作用引起的bug。
你已经分割了你所有的测试,来让他们保持原子性并不产生副作用,但是你想保证他们的逻辑可组织,它本身能以组的方式运行。
你可以使用module()去把测试分组:
module( "group a" ); test( "a basic test example", function() { ok( true, "this test is fine" ); }); test( "a basic test example 2", function() { ok( true, "this test is fine" ); }); module( "group b" ); test( "a basic test example 3", function() { ok( true, "this test is fine" ); }); test( "a basic test example 4", function() { ok( true, "this test is fine" ); });
module()后面的测试会被分在一个组里面,测试结果中每个测试的名称会被加上module名称。你还可以通过module名字选择特定测试来运行。
module()除了实现分组功能外,还可以用来提取通用代码,他接受一个可选的第二个参数,定义module每次运行测试开始和结束的行为。
module( "module", { setup: function() { ok( true, "one extra assert per test" ); }, teardown: function() { ok( true, "and one extra assert after each test" ); } }); test( "test with setup and teardown", function() { expect( 2 ); });
你可以一起定义setup和teardown属性,或者只定义其中一个。再次调用modile()方法的时候,会重置掉之前方法定义的setup/teardown方法。
当你的测试需要运行很长时间的时候(例如好几秒),你可以不想浪费时间等待结果。
QUnit有一系列的方法可以解决这个问题。最有趣的一个是点击头部的 "Hide passed tests"选项,这样QUnit会只显示失败的测试,他不会对测试时间有影响,只是起到聚焦失败测试的作用。
另外一个有趣的特性是,QUnit会把失败的测试的名字保存在sessionStorage中(你的浏览器必须要支持),默认情况下这个特性是开启的,只是我们没有注意到他的存在。下次你再运行测试的时候,之前失败的测试会被先执行,但是他对输出结果的顺序没有影响,影响的只是执行顺序。结合使用 "Hide passed tests" 你可以立即得到失败的测试。
自动排序会默认发生,你以为着你的测试需要保持原子性,如果不能保证这点将会产生随机的异常。修复问题是个正确的解决方案,或者存在困难,你可以设置QUnit.config.reorder = false。
除了自动排序之外,还有一些手工可选项。可以点击测试后面的"Rerun"链接,运行单个测试。他会在url中添加"testNumber=N"字符串参数,N代表你点击的测试编号。你可以重刷页面,只运行那个测试,或者使用浏览器的返回按钮区运行所有测试。
运行module中的所有测试,几乎以相同的方式工作。除非你选择页眉右上角的module,他会在url中添加"module=N"字符串,N代表module的编号,例如:"?module=testEnvironment%20with%20object"。