1. Trang chủ
  2. » Công Nghệ Thông Tin

Test Driven JavaScript Development- P13 ppsx

20 234 0
Tài liệu đã được kiểm tra trùng lặp

Đang tải... (xem toàn văn)

Tài liệu hạn chế xem trước, để xem đầy đủ mời bạn chọn Tải xuống

THÔNG TIN TÀI LIỆU

Thông tin cơ bản

Tiêu đề Test Driven JavaScript Development
Trường học University of Example
Chuyên ngành Computer Science
Thể loại Bài luận
Năm xuất bản 2023
Thành phố Example City
Định dạng
Số trang 20
Dung lượng 186,97 KB

Các công cụ chuyển đổi và chỉnh sửa cho tài liệu này

Nội dung

Listing 11.31 Various ways to share observable behavior var Observable = tddjs.util.Observable; // Extending the object with an observable object tddjs.extendnewsletter, new Observable;

Trang 1

By throwing an exception already when adding the observers we don’t need

to worry about invalid data later when we notify observers Had we been

pro-gramming by contract, we could say that a precondition for the addObserver

method is that the input must be callable The postcondition is that the observer

is added to the observable and is guaranteed to be called once the observable calls notifyObservers

The test fails, so we shift our focus to getting the bar green again as quickly

as possible Unfortunately, there is no way to fake the implementation this time—

throwing an exception on any call to addObserver will fail all the other tests

Luckily, the implementation is fairly trivial, as seen in Listing 11.27

Listing 11.27 Throwing an exception when adding non-callable observers

function addObserver(observer) {

if (typeof observer != "function") { throw new TypeError("observer is not function");

} this.observers.push(observer);

} addObservernow checks that the observer is in fact a function before adding

it to the list Running the tests yields that sweet feeling of success: All green

11.5.2 Misbehaving Observers

The observable now guarantees that any observer added through addObserver

is callable Still, notifyObservers may still fail horribly if an observer throws

an exception Listing 11.28 shows a test that expects all the observers to be called even if one of them throws an exception

Listing 11.28 Expecting notifyObservers to survive misbehaving observers

"test should notify all even when some fail": function () { var observable = new tddjs.util.Observable();

var observer1 = function () { throw new Error("Oops"); };

var observer2 = function () { observer2.called = true; };

observable.addObserver(observer1);

observable.addObserver(observer2);

observable.notifyObservers();

assertTrue(observer2.called);

}

Trang 2

Running the test reveals that the current implementation blows up along with the first observer, causing the second observer not to be called In effect,

noti-fyObserversis breaking its guarantee that it will always call all observers once

they have been successfully added To rectify the situation, the method needs to be

prepared for the worst, as seen in Listing 11.29

Listing 11.29 Catching exceptions for misbehaving observers

function notifyObservers() {

for (var i = 0, l = this.observers.length; i < l; i++) { try {

this.observers[i].apply(this, arguments);

} catch (e) {}

} }

The exception is silently discarded It is the observers responsibility to ensure that any errors are handled properly, the observable is simply fending off badly

behaving observers

11.5.3 Documenting Call Order

We have improved the robustness of the Observable module by giving it proper

error handling The module is now able to give guarantees of operation as long as it

gets good input and it is able to recover should an observer fail to meet its

require-ments However, the last test we added makes an assumption on undocumented

features of the observable: It assumes that observers are called in the order they

were added Currently, this solution works because we used an array to implement

the observers list Should we decide to change this, however, our tests may break

So we need to decide: Do we refactor the test to not assume call order, or do we

simply add a test that expects call order, thereby documenting call order as a

fea-ture? Call order seems like a sensible feature, so Listing 11.30 adds the test to make

sure Observable keeps this behavior

Listing 11.30 Documenting call order as a feature

"test should call observers in the order they were added":

function () {

var observable = new tddjs.util.Observable();

var calls = [];

var observer1 = function () { calls.push(observer1); };

var observer2 = function () { calls.push(observer2); };

observable.addObserver(observer1);

Trang 3

observable.addObserver(observer2);

observable.notifyObservers();

assertEquals(observer1, calls[0]);

assertEquals(observer2, calls[1]);

} Because the implementation already uses an array for the observers, this test succeeds immediately

11.6 Observing Arbitrary Objects

In static languages with classical inheritance, arbitrary objects are made observable

by subclassing the Observable class The motivation for classical inheritance

in these cases comes from a desire to define the mechanics of the pattern in one place and reuse the logic across vast amounts of unrelated objects As discussed

in Chapter 7, Objects and Prototypal Inheritance, we have several options for code

reuse among JavaScript objects, so we need not confine ourselves to an emulation

of the classical inheritance model

Although the Java analogy helped us develop the basic interface, we will now break free from it by refactoring the observable interface to embrace JavaScript’s object model Assuming we have a Newsletter constructor that creates newsletterobjects, there are a number of ways we can make newsletters observ-able, as seen in Listing 11.31

Listing 11.31 Various ways to share observable behavior

var Observable = tddjs.util.Observable;

// Extending the object with an observable object tddjs.extend(newsletter, new Observable());

// Extending all newsletters with an observable object tddjs.extend(Newsletter.prototype, new Observable());

// Using a helper function tddjs.util.makeObservable(newsletter);

// Calling the constructor as a function Observable(newsletter);

// Using a "static" method:

Trang 4

Observable.make(newsletter);

// Telling the object to "fix itself" (requires code on

// the prototype of either Newsletter or Object)

newsletter.makeObservable();

// Classical inheritance-like

Newspaper.inherit(Observable);

In the interest of breaking free of the classical emulation that constructors provide, consider the examples in Listing 11.32, which assume that tddjs

util.observableis an object rather than a constructor

Listing 11.32 Sharing behavior with an observable object

// Creating a single observable object

var observable = Object.create(tddjs.util.observable);

// Extending a single object

tddjs.extend(newspaper, tddjs.util.observable);

// A constructor that creates observable objects

function Newspaper() {

/* */

}

Newspaper.prototype = Object.create(tddjs.util.observable);

// Extending an existing prototype

tddjs.extend(Newspaper.prototype, tddjs.util.observable);

Simply implementing the observable as a single object offers a great deal of flexibility To get there we need to refactor the existing solution to get rid of the

constructor

11.6.1 Making the Constructor Obsolete

To get rid of the constructor we should first refactor Observable such that

the constructor doesn’t do any work Luckily, the constructor only initializes the

observersarray, which shouldn’t be too hard to remove All the methods on

Observable.prototypeaccess the array, so we need to make sure they can all

handle the case in which it hasn’t been initialized To test for this we simply need to

write one test per method that calls the method in question before doing anything

else

Trang 5

As seen in Listing 11.33, we already have tests that call addObserver and hasObserverbefore doing anything else

Listing 11.33 Tests targeting addObserver and hasObserver

TestCase("ObservableAddObserverTest", {

"test should store functions": function () { var observable = new tddjs.util.Observable();

var observers = [function () {}, function () {}];

observable.addObserver(observers[0]);

observable.addObserver(observers[1]);

assertTrue(observable.hasObserver(observers[0]));

assertTrue(observable.hasObserver(observers[1]));

}, /* */

});

TestCase("ObservableHasObserverTest", {

"test should return false when no observers": function () { var observable = new tddjs.util.Observable();

assertFalse(observable.hasObserver(function () {}));

} });

The notifyObservers method however, is only tested after addObserver has been called Listing 11.34 adds a test that expects it to be possible to call this method before adding any observers

Listing 11.34 Expecting notifyObservers to not fail if called before

addObserver

"test should not fail if no observers": function () { var observable = new tddjs.util.Observable();

assertNoException(function () { observable.notifyObservers();

});

} With this test in place, we can empty the constructor as seen in Listing 11.35

Trang 6

Listing 11.35 Emptying the constructor

function Observable() {

}

Running the tests shows that all but one is now failing, all with the same message:

“this.observers is not defined.” We will deal with one method at a time Listing 11.36

shows the updated addObserver method

Listing 11.36 Defining the array if it does not exist in addObserver

function addObserver(observer) {

if (!this.observers) { this.observers = [];

} /* */

}

Running the tests again reveals that the updated addObserver method fixes all but the two tests that do not call it before calling other methods, such as

hasObserverand notifyObservers Next up, Listing 11.37 makes sure to

return false directly from hasObserver if the array does not exist

Listing 11.37 Aborting hasObserver when there are no observers

function hasObserver(observer) {

if (!this.observers) { return false;

} /* */

}

We can apply the exact same fix to notifyObservers, as seen in Listing 11.38

Listing 11.38 Aborting notifyObservers when there are no observers

function notifyObservers(observer) {

if (!this.observers) { return;

} /* */

}

Trang 7

11.6.2 Replacing the Constructor with an Object

Now that the constructor doesn’t do anything, it can be safely removed We will then add all the methods directly to the tddjs.util.observable object, which can then be used with, e.g., Object.create or tddjs.extend to create observable objects Note that the name is no longer capitalized as it is no longer a constructor

Listing 11.39 shows the updated implementation

Listing 11.39 The observable object

(function () { function addObserver(observer) { /* */

} function hasObserver(observer) { /* */

} function notifyObservers() { /* */

} tddjs.namespace("util").observable = { addObserver: addObserver,

hasObserver: hasObserver, notifyObservers: notifyObservers };

}());

Surely, removing the constructor will cause all the tests so far to break Fixing them is easy, however; all we need to do is to replace the new statement with a call

to Object.create, as seen in Listing 11.40

Listing 11.40 Using the observable object in tests

TestCase("ObservableAddObserverTest", { setUp: function () {

this.observable = Object.create(tddjs.util.observable);

}, /* */

});

TestCase("ObservableHasObserverTest", { setUp: function () {

Trang 8

this.observable = Object.create(tddjs.util.observable);

}, /* */

});

TestCase("ObservableNotifyObserversTest", {

setUp: function () { this.observable = Object.create(tddjs.util.observable);

}, /* */

});

To avoid duplicating the Object.create call, each test case gained a setUp method that sets up the observable for testing The test methods have to be updated

accordingly, replacing observable with this.observable

For the tests to run smoothly on any browser, the Object.create

imple-mentation from Chapter 7, Objects and Prototypal Inheritance, needs to be saved in

lib/object.js

11.6.3 Renaming Methods

While we are in the game of changing things we will take a moment to reduce the

ver-bosity of the interface by renaming the addObserver and notifyObservers

methods We can shorten them down without sacrificing any clarity Renaming

the methods is a simple case of search-replace so we won’t dwell on it too long

Listing 11.41 shows the updated interface, I’ll trust you to update the test case

accordingly

Listing 11.41 The refurbished observable interface

(function () {

function observe(observer) { /* */

} /* */

function notify() { /* */

} tddjs.namespace("util").observable = {

Trang 9

observe: observe, hasObserver: hasObserver, notify: notify

};

}());

11.7 Observing Arbitrary Events

The current observable implementation is a little limited in that it only keeps

a single list of observers This means that in order to observe more than one event, observers have to determine what event occurred based on heuristics on the data they receive We will refactor the observable to group observers by event names Event names are arbitrary strings that the observable may use at its own discretion

11.7.1 Supporting Events in observe

To support events, the observe method now needs to accept a string argument

in addition to the function argument The new observe will take the event as its first argument As we already have several tests calling the observe method, we can start by updating the test case Add a string as first argument to any call to observeas seen in Listing 11.42

Listing 11.42 Updating calls to observe

TestCase("ObservableAddObserverTest", { /* */

"test should store functions": function () { /* */

this.observable.observe("event", observers[0]);

this.observable.observe("event", observers[1]);

/* */

}, /* * });

TestCase("ObservableNotifyObserversTest", { /* */

"test should call all observers": function () {

Trang 10

/* */

this.observable.observe("event", observer1);

this.observable.observe("event", observer2);

/* */

},

"test should pass through arguments": function () { /* */

this.observable.observe("event", function () { actual = arguments;

});

/* */

},

"test should notify all even when some fail": function () { /* */

this.observable.observe("event", observer1);

this.observable.observe("event", observer2);

/* */

},

"test should call observers in the order they were added":

function () { /* */

this.observable.observe("event", observer1);

this.observable.observe("event", observer2);

/* */

}, /* */

});

Unsurprisingly, this causes all the tests to fail as observe throws an exception, because the argument it thinks is the observer is not a function To get tests back to

green we simply add a formal parameter to observe, as seen in Listing 11.43

Listing 11.43 Adding a formal event parameter to observe

function observe(event, observer) {

/* */

}

We will repeat this exercise with both hasObserver and notify as well, to make room for tests that describe actual functionality I will leave updating these

Trang 11

other two functions (and their tests) as an exercise When you are done you will note that one of the tests keep failing We will deal with that last test together

11.7.2 Supporting Events in notify

While updating notify to accept an event whose observers to notify, one of the existing tests stays in the red The test in question is the one that compares arguments sent to notify against those received by the observer The problem is that because notifysimply passes along the arguments it receives, the observer is now receiving the event name in addition to the arguments it was supposed to receive

To pass the test, Listing 11.44 uses Array.prototype.slice to pass along all but the first argument

Listing 11.44 Passing all but the first argument to observers

function notify(event) { /* */

var args = Array.prototype.slice.call(arguments, 1);

for (var i = 0, l = this.observers.length; i < l; i++) { try {

this.observers[i].apply(this, args);

} catch (e) {}

} } This passes the test and now observable has the interface to support events, even if it doesn’t actually support them yet

The test in Listing 11.45 specifies how the events are supposed to work It registers two observers to two different events It then calls notify for only one

of the events and expects only the related observer to be called

Listing 11.45 Expecting only relevant observers to be called

"test should notify relevant observers only": function () { var calls = [];

this.observable.observe("event", function () { calls.push("event");

});

this.observable.observe("other", function () {

Ngày đăng: 03/07/2014, 05:20