转载于:http://blog.csdn.net/littlechang/article/details/8120322
颠倒开发顺序
在传统的编程中,直到概念完全在代码中体现,问题被编程解决。理想状态,代码遵照一些完整的架构设计思考,尽管在很多情况下,可能不是这种情况,特别在JavaScript的世界。这种编程风格通过猜测需要什么代码解决问题来解决问题,这种策略很容易导致臃肿而紧耦合的方案。如果没有单元测试,这种方法生成的代码甚至可能会产生永远都不会执行的代码,如错误处理逻辑和“柔性”参数处理,或包含没有彻底测试或根本没有测试的边界检查情况。
测试驱动开发颠倒了开发周期。不再是关注于什么需要代码来解决问题,测试驱动开发通敲定目标开始。关于什么行动受支持并解释,单元测试形成规格和文档。但是,TDD的目标是测试,所以它不能保证它对例如边界情况处理的更好。但是,每一行代码都被示例代码的典型片断测试过,TDD可能会减少产生多余的代码,而对应的功能可能更强健。完全的测试驱动开发保证系统不会包含执行不到的代码。
过程
驱动测试开发过程是一个迭代的过程,每个迭代包括下面四个步骤:
·
写一个测试
·
运行测试,看到新测试失败
·
使该测试通过
·
重构以清除重复
在每个迭代,没有都是规格。一旦已经写了足够的产品代码(并且没有更多)使测试通过,我们就完工了,我们就应该重构代码以消除重复和/或改善设计,同时保持测试仍然通过。
TDD实践:观察者模式
观察者模式(也叫出版/订阅模式或简称pubsub
)是一个设计模式,它允许我们观察一个对象的状态,并当它发生改变时会被通知。该模式可以提供一个具有强扩展点的对象同时维持松耦合。
观察者模式有两个角色—被观察者和观察者。观察者是一个对象或函数,它在被观察者状态改变时会被通知。被观察者决定什么时间通知和提供什么数据给它的观察者。被观察者至少提供两个公共方法:通知他的观察者有新数据的pubsub,和订阅观察者事件的pubsub。
被观察者库
测试驱动开发允许我们在需要时以非常小的步子前进。在这第一个现实世界的例子中,我们以最微小的步子开始。随着我们对代码和过程信心的增强,当环境允许时(即实现很不重要的代码),我们将逐步的增加步子的大小。以小的频繁迭代写代码能帮助我们设计的API更好,帮助我们犯更小的错误。当出错时,我们能很快的修复它们,因为错误很容易在每次我们添加一些代码后执行测试时跟踪到。
搭建环境
本例使用JsTestDriver运行测试。官方网站上有环境搭建指南。
像下面这样初始化项目布局:
1.
chris@laptop:~/projects/observable $ tree
2.
.
3.
|-- jsTestDriver.conf
4.
|-- src
5.
| `-- observable.js
6.
`-- test
7.
`-- observable_test.js
最小的JsTestDriver配置文件:
1.
server: http://localhost:4224
2.
load:
3.
- lib/*.js
4.
- test/*.js
添加观察者
我们首先开始有实现一个计划作为观察者的对象的想法。要这样做会让我们首先写一个测试,并看着它失败,通过最直接的方式让它通过,然后重构让它更合理。
第一个测试
第一个测试将添加一个叫addObserver
的方法的观察者。为了验证这个工作,我们假定被观察者把观察者保存到一个数组中,然后验证观察者数组中只有一项。测试在test/observable_test.js
中,其内容如下面所示:
- TestCase("ObservableAddObserverTest", {
- "test should store function":function () {
- var observable =new tddjs.Observable();
- var observer =function () {};
- observable.addObserver(observer);
- assertEquals(observer, observable.observers[0]);
- }
- });
TestCase("ObservableAddObserverTest", {
"test should store function": function () {
var observable = new tddjs.Observable();
var observer = function () {};
observable.addObserver(observer);
assertEquals(observer, observable.observers[0]);
}
});
执行测试并看着它失败:
乍一看,第一个测试的执行结果是毁灭性的:
- Total 1 tests (Passed: 0; Fails: 0; Errors: 1) (0.00 ms)
- Firefox 3.6.12 Linux: Run 1 tests (Passed: 0; Fails: 0; Errors 1) (0.00 ms)
- ObservableAddObserverTest.test should store function error (0.00 ms): \
- tddjs is not defined
- /test/observable_test.js:3
- Tests failed.
Total 1 tests (Passed: 0; Fails: 0; Errors: 1) (0.00 ms)
Firefox 3.6.12 Linux: Run 1 tests (Passed: 0; Fails: 0; Errors 1) (0.00 ms)
ObservableAddObserverTest.test should store function error (0.00 ms): \
tddjs is not defined
/test/observable_test.js:3
Tests failed.
使测试通过
不要害怕!失败其实是个好事:它告诉我们努力的目标。第一个严重的问题是由于tddjs不存在。我们在src/observable.js
中添加命名空间对象:
- 1. var tddjs = {};
1. var tddjs = {};
重新运行测试产生一个新错误:
- 1. E
- 2. Total 1 tests (Passed: 0; Fails: 0; Errors: 1) (0.00 ms)
- 3. Firefox 3.6.12 Linux: Run 1 tests (Passed: 0; Fails: 0; Errors 1) (0.00 ms)
- 4. ObservableAddObserverTest.test should storefunction error (0.00 ms): \
- 5. tddjs.Observable is not a constructor
- 6. /test/observable_test.js:3
- 7. Tests failed.
1. E
2. Total 1 tests (Passed: 0; Fails: 0; Errors: 1) (0.00 ms)
3. Firefox 3.6.12 Linux: Run 1 tests (Passed: 0; Fails: 0; Errors 1) (0.00 ms)
4. ObservableAddObserverTest.test should store function error (0.00 ms): \
5. tddjs.Observable is not a constructor
6. /test/observable_test.js:3
7. Tests failed.
我们通过添加一个空的Observable构造器来修正这个新问题:
- 1. var tddjs = {};
- 2. (function () {
- 3. function Observable() {}
- 4. tddjs.Observable = Observable;
- 5. }());
1. var tddjs = {};
2. (function () {
3. function Observable() {}
4. tddjs.Observable = Observable;
5. }());
再一次执行测试立即又带来了下一个问题:
- 1. E
- 2. Total 1 tests (Passed: 0; Fails: 0; Errors: 1) (0.00 ms)
- 3. Firefox 3.6.12 Linux: Run 1 tests (Passed: 0; Fails: 0; Errors 1) (0.00 ms)
- 4. ObservableAddObserverTest.test should store function error (0.00 ms): \
- 5. observable.addObserver is not a function
- 6. /test/observable_test.js:6
- 7. Tests failed.
1. E
2. Total 1 tests (Passed: 0; Fails: 0; Errors: 1) (0.00 ms)
3. Firefox 3.6.12 Linux: Run 1 tests (Passed: 0; Fails: 0; Errors 1) (0.00 ms)
4. ObservableAddObserverTest.test should store function error (0.00 ms): \
5. observable.addObserver is not a function
6. /test/observable_test.js:6
7. Tests failed.
添加缺失的方法:
- 1. function addObserver() {}
- 2. Observable.prototype.addObserver = addObserver;
1. function addObserver() {}
2. Observable.prototype.addObserver = addObserver;
随着方法的添加,现在测试提示缺少观察者数组。
- 1. E
- 2. Total 1 tests (Passed: 0; Fails: 0; Errors: 1) (0.00 ms)
- 3. Firefox 3.6.12 Linux: Run 1 tests (Passed: 0; Fails: 0; Errors 1) (0.00 ms)
- 4. ObservableAddObserverTest.test should store function error (0.00 ms): \
- 5. observable.observers is undefined
- 6. /test/observable_test.js:8
- 7. Tests failed.
1. E
2. Total 1 tests (Passed: 0; Fails: 0; Errors: 1) (0.00 ms)
3. Firefox 3.6.12 Linux: Run 1 tests (Passed: 0; Fails: 0; Errors 1) (0.00 ms)
4. ObservableAddObserverTest.test should store function error (0.00 ms): \
5. observable.observers is undefined
6. /test/observable_test.js:8
7. Tests failed.
看起来有点奇怪,我现在要在pubsub方法中定义观察者数组。当一个测试时,TDD教导我们做能工作的最简单的事情,别管它有多糟糕。一旦测试通过,我们有机会审视我们的工作。
- 1. function addObserver(observer) {
- 2. this.observers = [observer];
- 3. }
- 4. Success! The test now passes:
- 5. .
- 6. Total 1 tests (Passed: 1; Fails: 0; Errors: 0) (1.00 ms)
- 7. Firefox 3.6.12 Linux: Run 1 tests (Passed: 1; Fails: 0; Errors 0) (1.00 ms)
1. function addObserver(observer) {
2. this.observers = [observer];
3. }
4. Success! The test now passes:
5. .
6. Total 1 tests (Passed: 1; Fails: 0; Errors: 0) (1.00 ms)
7. Firefox 3.6.12 Linux: Run 1 tests (Passed: 1; Fails: 0; Errors 0) (1.00 ms)
重构
在开发出现在的解决方案的时候,我们采取了最快的路径来通过测试。现在条形是绿色的,我们可以审视解决方案并执行任何我们认为需要的重构。在这最后一步,惟一的规则就是保持条形是绿色的。这意味着我们不得不以微小的步子重构,确保不会意外破坏任何事情。
目前的实现有两个问题要处理。测试对被观察者的实现做了详细的假设(Thetest makes detailed assumptions about the implementation of Observable)和addObserver
实现是对测试的硬编码。
我们首先处理硬编码。为了暴露硬编码方案,我们增加测试使它添加两个观察者而不是一个。
- 1. "test should store function":function () {
- 2. var observable =new tddjs.Observable();
- 3. var observers = [function () {},function () {}];
- 4. observable.addObserver(observers[0]);
- 5. observable.addObserver(observers[1]);
- 6. assertEquals(observers, observable.observers);
- 7. }
1. "test should store function": function () {
2. var observable = new tddjs.Observable();
3. var observers = [function () {}, function () {}];
4. observable.addObserver(observers[0]);
5. observable.addObserver(observers[1]);
6. assertEquals(observers, observable.observers);
7. }
正如预期,测试失败了。测试期望函数能添加观察者并堆积,像任何元素添加到一个pubsub
一样。为了实现这个功能,我们把数组实例到构造函数中,并简单的使用
数组方法push代理
addObserver
:
- 1. function Observable() {
- 2. this.observers = [];
- 3. }
- 4. function addObserver(observer) {
- 5. this.observers.push(observer);
- 6. }
1. function Observable() {
2. this.observers = [];
3. }
4. function addObserver(observer) {
5. this.observers.push(observer);
6. }
有了这个实现,测试又一次通过了,验证我们已经处理了硬编码方案。然而,存取一个公共属性的问题和野蛮的假设被观察者的实现也是一个问题。一个可观察的pubsub
应该是对任意数量的对象可观察的,但外部对象对如何存储它们或把它们存到哪里不感兴趣。理想情况,如果某一观察者已注册,我们希望能不需要搜索被观察者的内部就能用它检查。我们做了一个注解并继续前行。随后,我们回来改进这个测试。
检查观察者
我们给被观察者添加另一个方法,hasObserver
,
并用它除去部分当实现addObserver
时引起的混乱。
测试
一个新方法开始于一个新测试,下一个测试期望hasObserver
方法的行为:
- 1. TestCase("ObservableHasObserverTest", {
- 2. "test should return true when has observer":function () {
- 3. var observable =new tddjs.Observable();
- 4. var observer =function () {};
- 5. observable.addObserver(observer);
- 6. assertTrue(observable.hasObserver(observer));
- 7. }
- 8. });
1. TestCase("ObservableHasObserverTest", {
2. "test should return true when has observer": function () {
3. var observable = new tddjs.Observable();
4. var observer = function () {};
5. observable.addObserver(observer);
6. assertTrue(observable.hasObserver(observer));
7. }
8. });
我们期望这个测试失败于缺少hasObserver
,
实际上确实如此。
使测试通过
又一次,我们采用能使测试通过的最简单的方案。
- 1. function hasObserver(observer) {
- 2. returntrue;
- 3. }
- 4. Observable.prototype.hasObserver = hasObserver;
1. function hasObserver(observer) {
2. return true;
3. }
4. Observable.prototype.hasObserver = hasObserver;
虽然从长远看我们知道这不能解决我们的问题,但它保持测试是绿色的。尝试审视和重构让我们无从下手,因为没有明显可以改善的点。测试就是我们的需求,当前它们只需要hasObserver
返回true。为了修正它,我们加入另一个对不存在的观察者期望hasObserver
返回false的测试,它可以帮助促使真实的方案。
- 1. "test should return false when no observers":function () {
- 2. var observable =new tddjs.Observable();
- 3. assertFalse(observable.hasObserver(function () {}));
- 4. }
1. "test should return false when no observers": function () {
2. var observable = new tddjs.Observable();
3. assertFalse(observable.hasObserver(function () {}));
4. }
这个测试可悲的失败了,因为hasObserver
总是返回true,强迫我们来产生真实的方案。如果一个观察者已经注册,验证就是个简单的检查this.observers数组包含起初传过来的对象事情。
- 1. function hasObserver(observer) {
- 2. returnthis.observers.indexOf(observer) >= 0;
- 3. }
1. function hasObserver(observer) {
2. return this.observers.indexOf(observer) >= 0;
3. }
如果元素不在数组中,Array.prototype.indexOf
方法返回一个小于0的数值,所以验证它返回的数值大于等于0就会告诉我们观察者是否存在。
解决浏览器不兼容
在多于一个浏览器中运行测试会产生令人吃惊的结果:
- 1. chris@laptop:~/projects/observable$ jstestdriver --tests all
- 2. ...E
- 3. Total 4 tests (Passed: 3; Fails: 0; Errors: 1) (11.00 ms)
- 4. Firefox 3.6.12 Linux: Run 2 tests (Passed: 2; Fails: 0; Errors 0) (2.00 ms)
- 5. Microsoft Internet Explorer 6.0 Windows: Run 2 tests \
- 6. (Passed: 1; Fails: 0; Errors 1) (0.00 ms)
- 7. ObservableHasObserverTest.test should return true when has observer error \
- 8. (0.00 ms): Object doesn't support this property or method
- 9. Tests failed.
1. chris@laptop:~/projects/observable$ jstestdriver --tests all
2. ...E
3. Total 4 tests (Passed: 3; Fails: 0; Errors: 1) (11.00 ms)
4. Firefox 3.6.12 Linux: Run 2 tests (Passed: 2; Fails: 0; Errors 0) (2.00 ms)
5. Microsoft Internet Explorer 6.0 Windows: Run 2 tests \
6. (Passed: 1; Fails: 0; Errors 1) (0.00 ms)
7. ObservableHasObserverTest.test should return true when has observer error \
8. (0.00 ms): Object doesn't support this property or method
9. Tests failed.
IE版本6和7测试失败,用最通用的错误信息提示:“对象不支持此属性或方法”。这可以指任意数量的问题:
·
我们调用了一个空对象的方法
·
我们调用了一个不存在的方法
·
我们操作了一个不存在的属性
幸运的是,TDD步子微小,我们知道错误和最近添加的对观察者数组的indexOf
调用有关。事实证明,IE6和7不支持JavaScript1.6的Array.prototype.indexOf
方法
(对此,我们真不能责怪它,它只是最近在2009年12月的ECMAScript
5被标准化)。关于这一点,我们有三个选择:
·
避免在hasObserver中使用Array.prototype.indexOf,重复使用浏览器支持的原生功能。
·
为不支持的浏览器实现数组Array.prototype.indexOf。或者选择实现一个提供相同功能的帮助函数
·
使用提供缺失方法或相似方法的第三方库
哪一个方法最适合解决给定问题要视情况而定:它们都有自己的优点缺点。如果更看中保持被观察者的自包含,我们会使用循环代替indexOf
简单实现hasObserver
,有效的解决问题。同时,这好像也是让它能正常工作的最简单的事情。如果以后遇到同样的相似的情况,我们会被建议重新考虑我们的决定。更新的hasObserver
像这样:
- 1. function hasObserver(observer) {
- 2. for (var i = 0, l =this.observers.length; i < l; i++) {
- 3. if (this.observers[i] == observer) {
- 4. returntrue;
- 5. }
- 6. }
- 7. returnfalse;
- 8. }
1. function hasObserver(observer) {
2. for (var i = 0, l = this.observers.length; i < l; i++) {
3. if (this.observers[i] == observer) {
4. return true;
5. }
6. }
7. return false;
8. }
重构
随着条形回到绿色,是时候该审视我们进展了。我们现在有三个测试,但其中的两个出奇的相似。我们写的第一个验证addObserver
的正确性测试基本上和为验证重构写的测试做了相同的事。在这两个测试中有两个关键的不同:第一个测试预先被声明为发臭的,因为它直接存取被观察者对象内部观察者数组。第二个测试添加两个观察者,确保它们都被添加了。我们现在把两个测试连接两个测试为一个验证所有添加到被观察者的观察者是真实的添加了。
- view plaincopy to clipboardprint?
- 1. "test should store functions":function () {
- 2. var observable =new tddjs.Observable();
- 3. var observers = [function () {},function () {}];
- 4. observable.addObserver(observers[0]);
- 5. observable.addObserver(observers[1]);
- 6. assertTrue(observable.hasObserver(observers[0]));
- 7. assertTrue(observable.hasObserver(observers[1]));
- 8. }
view plaincopy to clipboardprint?
1. "test should store functions": function () {
2. var observable = new tddjs.Observable();
3. var observers = [function () {}, function () {}];
4. observable.addObserver(observers[0]);
5. observable.addObserver(observers[1]);
6. assertTrue(observable.hasObserver(observers[0]));
7. assertTrue(observable.hasObserver(observers[1]));
8. }
通知观察者
添加观察者并验证他们存在已经正常,但没有能力通知他们感兴趣的变化,被观察者就没有什么用。是实现通知方法的时候了。
保证观察者被调用
通知执行最重要的任务是调用观察者。为达到这个目的,我们需要一些方法验证在通知之后观察者的确被调用了。为了验证一个函数被调用,我们可以在这个函数被调用时设置一个属性。为验证测试,我们可以检查属性是否被设置。下面的测试在第一个notify测试中使用这个思想。
- 1. TestCase("ObservableNotifyTest", {
- 2. "test should call all observers":function () {
- 3. var observable =new tddjs.Observable();
- 4. var observer1 =function () { observer1.called =
true; }; - 5. var observer2 =function () { observer2.called =
true; }; - 6. observable.addObserver(observer1);
- 7. observable.addObserver(observer2);
- 8. observable.notify();
- 9. assertTrue(observer1.called);
- 10. assertTrue(observer2.called);
- 11. }
- 12. });
1. TestCase("ObservableNotifyTest", {
2. "test should call all observers": function () {
3. var observable = new tddjs.Observable();
4. var observer1 = function () { observer1.called = true; };
5. var observer2 = function () { observer2.called = true; };
6. observable.addObserver(observer1);
7. observable.addObserver(observer2);
8. observable.notify();
9. assertTrue(observer1.called);
10. assertTrue(observer2.called);
11. }
12. });
为了使这个测试通过我们需要循环观察者数组并调用每个函数。
- 1. function notify() {
- 2. for (var i = 0, l =this.observers.length; i < l; i++) {
- 3. this.observers[i]();
- 4. }
- 5. }
- 6. Observable.prototype.notify = notify;
1. function notify() {
2. for (var i = 0, l = this.observers.length; i < l; i++) {
3. this.observers[i]();
4. }
5. }
6. Observable.prototype.notify = notify;
传参数
现在观察者已经被调用了,但它们没有被送入任何数据。它们知道什么事情发生了-但不需要知道是什么。我们使通知带任意数量的参数,简单的把他们传递给每个观察者:
- 1. "test should pass through arguments":function () {
- 2. var observable =new tddjs.Observable();
- 3. var actual;
- 4. observable.addObserver(function () {
- 5. actual = arguments;
- 6. });
- 7. observable.notify("String", 1, 32);
- 8. assertEquals(["String", 1, 32], actual);
- 9. }
1. "test should pass through arguments": function () {
2. var observable = new tddjs.Observable();
3. var actual;
4. observable.addObserver(function () {
5. actual = arguments;
6. });
7. observable.notify("String", 1, 32);
8. assertEquals(["String", 1, 32], actual);
9. }
测试通过把接收的参数赋值给一个本地变量来对比接收的参数和传递的参数。我们创建的观察者实际是一个非常简单的人工测试间谍。运行测试确认它是失败的,这并不令人吃惊,因为我们现在还没有在notify中接触参