Listing 11.9 Adding the addObserver method function addObserver { } Observable.prototype.addObserver = addObserver; With the method in place, Listing 11.10 shows that the test now fails
Trang 1tddjs.util is undefined
()@http://localhost:4224/ /observable_test.js:5
11.2.1.2 Making the Test Pass
Fear not! Failure is actually a good thing: It tells us where to focus our efforts
The first serious problem is that tddjs.util doesn’t exist Listing 11.5 adds
the object using the tddjs.namespace method Save the listing in src/
observable.js
Listing 11.5 Creating the util namespace
tddjs.namespace("util");
Running the tests again yields a new error, as seen in Listing 11.6
Listing 11.6 Tests still failing
chris@laptop:~/projects/observable$ jstestdriver tests all
E
Total 1 tests (Passed: 0; Fails: 0; Errors: 1) (1.00 ms)
Firefox 3.6.3 Linux: Run 1 tests \
(Passed: 0; Fails: 0; Errors 1) (1.00 ms)
Observable.addObserver.test \
should store function error (1.00 ms): \
tddjs.util.Observable is not a constructor
()@http://localhost:4224/ /observable_test.js:5
Listing 11.7 fixes this new issue by adding an empty Observable constructor
Listing 11.7 Adding the constructor
(function () {
function Observable() {
}
tddjs.util.Observable = Observable;
}());
To work around the issues with named function expressions discussed in
Chapter 5, Functions, the constructor is defined using a function declaration
in-side an immediately called closure Running the test once again brings us directly
to the next problem, seen in Listing 11.8
Download from www.eBookTM.com
Trang 2Listing 11.8 Missing addObserver method
chris@laptop:~/projects/observable$ jstestdriver tests all
E
Total 1 tests (Passed: 0; Fails: 0; Errors: 1) (0.00 ms)
Firefox 3.6.3 Linux: Run 1 tests \
(Passed: 0; Fails: 0; Errors 1) (0.00 ms)
Observable.addObserver.test \
should store function error (0.00 ms): \
observable.addObserver is not a function
()@http://localhost:4224/ /observable_test.js:8
Listing 11.9 adds the missing method
Listing 11.9 Adding the addObserver method
function addObserver() {
}
Observable.prototype.addObserver = addObserver;
With the method in place, Listing 11.10 shows that the test now fails in place
of a missing observers array
Listing 11.10 The observers array does not exist
chris@laptop:~/projects/observable$ jstestdriver tests all
E
Total 1 tests (Passed: 0; Fails: 0; Errors: 1) (1.00 ms)
Firefox 3.6.3 Linux: Run 1 tests \
(Passed: 0; Fails: 0; Errors 1) (1.00 ms)
Observable.addObserver.test \
should store function error (1.00 ms): \
observable.observers is undefined
()@http://localhost:4224/ /observable_test.js:10
As odd as it may seem, Listing 11.11 now defines the observers array inside
the addObserver method Remember, when a test is failing, we’re instructed to
do the simplest thing that could possibly work, no matter how dirty it feels We will
get the chance to review our work once the test is passing
Trang 3Listing 11.11 Hard-coding the array
function addObserver(observer) {
this.observers = [observer];
}
Success! As Listing 11.12 shows, the test now passes
Listing 11.12 Test passing
chris@laptop:~/projects/observable$ jstestdriver tests all
Total 1 tests \
(Passed: 1; Fails: 0; Errors: 0) (0.00 ms)
Firefox 3.6.3 Linux: Run 1 tests \
(Passed: 1; Fails: 0; Errors 0) (0.00 ms)
11.2.2 Refactoring
While developing the current solution, we have taken the quickest possible route
to a passing test Now that the bar is green, we can review the solution and perform
any refactoring we deem necessary The only rule in this last step is to keep the bar
green This means we will have to refactor in tiny steps as well, making sure we
don’t accidentally break anything
The current implementation has two issues we should deal with The test makes
detailed assumptions about the implementation of Observable and the
addOb-serverimplementation is hard-coded to our test
We will address the hard-coding first To expose the hard-coded solution,
Listing 11.13 augments the test to make it add two observers instead of one
Listing 11.13 Exposing the hard-coded solution
"test should store function": function () {
var observable = new tddjs.util.Observable();
var observers = [function () {}, function () {}];
observable.addObserver(observers[0]);
observable.addObserver(observers[1]);
assertEquals(observers, observable.observers);
}
Download from www.eBookTM.com
Trang 4As expected, the test now fails The test expects that functions added as
ob-servers should stack up like any element added to an array To achieve this, we
will move the array instantiation into the constructor and simply delegate
addOb-serverto the array method push as Listing 11.14 shows
Listing 11.14 Adding arrays the proper way
function Observable() {
this.observers = [];
}
function addObserver(observer) {
this.observers.push(observer);
}
With this implementation in place, the test passes again, proving that we have
taken care of the hard-coded solution However, accessing a public property and
making wild assumptions about the implementation of Observable is still an
issue An observable object should be observable by any number of objects, but it
is of no interest to outsiders how or where the observable stores them Ideally, we
would like to be able to check with the observable if a certain observer is registered
without groping around its insides We make a note of the smell and move on Later,
we will come back to improve this test
11.3 Checking for Observers
We will add another method to Observable, hasObserver, and use it to remove
some of the clutter we added when implementing addObserver
11.3.1 The Test
A new method starts with a new test Listing 11.15 describes the desired behavior
for the hasObserver method
Listing 11.15 Expecting hasObserver to return true for existing observers
TestCase("ObservableHasObserverTest", {
"test should return true when has observer": function () {
var observable = new tddjs.util.Observable();
var observer = function () {};
observable.addObserver(observer);
Trang 5assertTrue(observable.hasObserver(observer));
}
});
We expect this test to fail in the face of a missing hasObserver, which it
does
11.3.1.1 Making the Test Pass
Listing 11.16 shows the simplest solution that could possibly pass the current test
Listing 11.16 Hard-coding hasObserver’s response
function hasObserver(observer) {
return true;
}
Observable.prototype.hasObserver = hasObserver;
Even though we know this won’t solve our problems in the long run, it keeps
the tests green Trying to review and refactor leaves us empty-handed as there are no
obvious points where we can improve The tests are our requirements, and currently
they only require hasObserver to return true Listing 11.17 introduces another
test that expects hasObserver to return false for a non-existent observer, which
can help force the real solution
Listing 11.17 Expecting hasObserver to return false for non-existent observers
"test should return false when no observers": function () {
var observable = new tddjs.util.Observable();
assertFalse(observable.hasObserver(function () {}));
}
This test fails miserably, given that hasObserver always returns true, forcing
us to produce the real implementation Checking if an observer is registered is a
simple matter of checking that the this.observers array contains the object
originally passed to addObserver as Listing 11.18 does
Listing 11.18 Actually checking for observer
function hasObserver(observer) {
return this.observers.indexOf(observer) >= 0;
}
Download from www.eBookTM.com
Trang 6The Array.prototype.indexOf method returns a number less than 0 if
the element is not present in the array, so checking that it returns a number equal
to or greater than 0 will tell us if the observer exists
11.3.1.2 Solving Browser Incompatibilities
Running the test produces somewhat surprising results as seen in the relevant excerpt
in Listing 11.19
Listing 11.19 Funky results in Internet Explorer 6
chris@laptop:~/projects/observable$ jstestdriver tests all
.EE
Total 3 tests (Passed: 1; Fails: 0; Errors: 2) (11.00 ms)
Microsoft Internet Explorer 6.0 Windows: Run 3 tests \
(Passed: 1; Fails: 0; Errors 2) (11.00 ms)
Observable.hasObserver.test \
should return true when has observer error (11.00 ms): \
Object doesn't support this property or method
Observable.hasObserver.test \
should return false when no observers error (0.00 ms): \
Object doesn't support this property or method
Internet Explorer versions 6 and 7 failed the test with their most generic of error
messages: “Object doesn’t support this property or method.” This can indicate any
number of issues
• We are calling a method on an object that is null
• We are calling a method that does not exist
• We are accessing a property that doesn’t exist
Luckily, TDD-ing in tiny steps, we know that the error has to relate to the
re-cently added call to indexOf on our observers array As it turns out, IE 6 and 7 does
not support the JavaScript 1.6 method Array.prototype.indexOf (which we
cannot really blame it for, it was only recently standardized with ECMAScript 5,
December 2009) In other words, we are dealing with our first browser compatibility
issue At this point, we have three options:
• Circumvent the use of Array.prototype.indexOf in hasObserver,
effectively duplicating native functionality in supporting browsers
• Implement Array.prototype.indexOf for non-supporting browsers
Alternatively implement a helper function that provides the same functionality
Trang 7• Use a third-party library that provides either the missing method, or a similar
method
Which one of these approaches is best suited to solve a given problem will
depend on the situation; they all have their pros and cons In the interest of keeping
Observableself-contained, we will simply implement hasObserver in terms
of a loop in place of the indexOf call, effectively working around the problem
Incidentally, that also seems to be the “simplest thing that could possibly work” at
this point Should we run into a similar situation later on, we would be advised to
reconsider our decision Listing 11.20 shows the updated hasObserver method
Listing 11.20 Manually looping the array
function hasObserver(observer) {
for (var i = 0, l = this.observers.length; i < l; i++) {
if (this.observers[i] == observer) {
return true;
}
}
return false;
}
11.3.2 Refactoring
With the bar back to green, it’s time to review our progress We now have three
tests, but two of them seem strangely similar The first test we wrote to verify the
correctness of addObserver basically tests for the same things as the test we
wrote to verify hasObserver There are two key differences between the two
tests: The first test has previously been declared smelly, as it directly accesses the
observersarray inside the observable object The first test adds two observers,
ensuring they’re both added Listing 11.21 joins the tests into one that verifies that
all observers added to the observable are actually added
Listing 11.21 Removing duplicated tests
"test should store functions": function () {
var observable = new tddjs.util.Observable();
var observers = [function () {}, function () {}];
observable.addObserver(observers[0]);
observable.addObserver(observers[1]);
Download from www.eBookTM.com
Trang 8assertTrue(observable.hasObserver(observers[0]));
assertTrue(observable.hasObserver(observers[1]));
}
11.4 Notifying Observers
Adding observers and checking for their existence is nice, but without the ability
to notify them of interesting changes, Observable isn’t very useful
In this section we will add yet another method to our library Sticking to the Java
parallel, we will call the new method notifyObservers Because this method
is slightly more complex than the previous methods, we will implement it step by
step, testing a single aspect of the method at a time
11.4.1 Ensuring That Observers Are Called
The most important task notifyObservers performs is calling all the observers
To do this, we need some way to verify that an observer has been called after the
fact To verify that a function has been called, we can set a property on the function
when it is called To verify the test we can check if the property is set The test in
Listing 11.22 uses this concept in the first test for notifyObservers
Listing 11.22 Expecting notifyObservers to call all observers
TestCase("ObservableNotifyObserversTest", {
"test should call all observers": function () {
var observable = new tddjs.util.Observable();
var observer1 = function () { observer1.called = true; };
var observer2 = function () { observer2.called = true; };
observable.addObserver(observer1);
observable.addObserver(observer2);
observable.notifyObservers();
assertTrue(observer1.called);
assertTrue(observer2.called);
}
});
To pass the test we need to loop the observers array and call each function
Listing 11.23 fills in the blanks
Trang 9Listing 11.23 Calling observers
function notifyObservers() {
for (var i = 0, l = this.observers.length; i < l; i++) {
this.observers[i]();
}
}
Observable.prototype.notifyObservers = notifyObservers;
11.4.2 Passing Arguments
Currently the observers are being called, but they are not being fed any data They
know something happened, but not necessarily what Although Java’s
implemen-tation defines the update method of observers to receive one or no arguments,
JavaScript allows a more flexible solution We will make notifyObservers take
any number of arguments, simply passing them along to each observer Listing 11.24
shows the requirement as a test
Listing 11.24 Expecting arguments to notifyObservers to be passed
to observers
"test should pass through arguments": function () {
var observable = new tddjs.util.Observable();
var actual;
observable.addObserver(function () {
actual = arguments;
});
observable.notifyObservers("String", 1, 32);
assertEquals(["String", 1, 32], actual);
}
The test compares passed and received arguments by assigning the received
arguments to a variable that is local to the test Running the test confirms that it
fails, which is not surprising as we are currently not touching the arguments inside
notifyObservers
To pass the test we can use apply when calling the observer, as seen in
Listing 11.25
Download from www.eBookTM.com
Trang 10Listing 11.25 Using apply to pass arguments through notifyObservers
function notifyObservers() {
for (var i = 0, l = this.observers.length; i < l; i++) {
this.observers[i].apply(this, arguments);
}
}
With this simple fix tests go back to green Note that we sent in this as the
first argument to apply, meaning that observers will be called with the observable
as this
11.5 Error Handling
At this point Observable is functional and we have tests that verify its behavior
However, the tests only verify that the observables behave correctly in response to
expected input What happens if someone tries to register an object as an observer
in place of a function? What happens if one of the observers blows up? Those are
questions we need our tests to answer Ensuring correct behavior in expected
situa-tions is important—that is what our objects will be doing most of the time At least
so we could hope However, correct behavior even when the client is misbehaving
is just as important to guarantee a stable and predictable system
11.5.1 Adding Bogus Observers
The current implementation blindly accepts any kind of argument to
addOb-server This contrasts to the Java API we started out comparing to, which allows
objects implementing the Observer interface to register as observers Although
our implementation can use any function as an observer, it cannot handle any value.
The test in Listing 11.26 expects the observable to throw an exception when
at-tempting to add an observer that is not callable
Listing 11.26 Expecting non-callable arguments to cause an exception
"test should throw for uncallable observer": function () {
var observable = new tddjs.util.Observable();
assertException(function () {
observable.addObserver({});
}, "TypeError");
}