UNDERSTANDING THE PATTERN THROUGH UNIT TESTS

Một phần của tài liệu reliable javascript how to code safely in the world s most dangerous language spencer richards 2015 07 13 Lập trình Java (Trang 188 - 200)

This section introduces you to the Callback Pattern via a series of unit tests.

The unit tests will illustrate how to create callback functions, as well as how to write functions that accept callbacks. You may also get some ideas for how to develop code that uses callbacks in sound, test-driven fashion. Both this section and the next will expose common problems and mistakes that occur when implementing the Callback Pattern and ways to avoid and fix them.

Writing and Testing Code That Uses Callback Functions

In Chapter 2, you were asked to create a website for an upcoming JavaScript conference. Continuing with that example, your next task is to allow

conference volunteers to check in attendees. The user interface will support selecting one or more attendees from a list, marking them as checked in, and registering the action in an external system. Implementing the check-in

behavior behind the user interface is the checkInService, which is the module you will create.

Objects created by the Conference.attendee function are responsible for

maintaining information about an attendee, including whether or not she has checked in. It will be up to the checkInService to manipulate those objects when attendees check in. Conference.attendee was written by your

conscientious and capable colleague, Charlotte, and she has created a full unit test suite for it, so you may assume it functions reliably.

var Conference = Conference || {};

Conference.attendee = function(firstName, lastName){

var checkedIn = false,

first = firstName || 'None', last = lastName || 'None';

return {

getFullName: function(){

return first + ' ' + last;

},

isCheckedIn: function(){

return checkedIn;

},

checkIn: function(){

checkedIn = true;

} };

};

Based on the description of the task, you’ll need to execute the checkIn function of one or more attendee objects.

It seems likely that you’ll have to manipulate collections of attendees in other ways in the future, so it makes sense to create an attendeeCollection object that encapsulates a collection of attendee objects.

In order to check in each attendee, the attendeeCollection object will need to allow for an action to be performed on each of the attendees in the collection.

You’ll allow the action to be performed to be specified via a callback function.

The attendeeCollection, with the requisite contains, add, and remove functions, is defined in Listing 5-1. The iterate function has been stubbed out and is where your efforts will be focused.

LISTING 5-1: The initial implementation of the Conference.attendeeCollection module

var Conference = Conference || {};

Conference.attendeeCollection = function(){

var attendees = [];

return {

contains: function(attendee){

return attendees.indexOf(attendee) > -1;

},

add: function(attendee){

if (!this.contains(attendee)){

attendees.push(attendee);

} },

remove: function(attendee){

var index = attendees.indexOf(attendee);

if (index > -1){

attendees.splice(index, 1);

} },

iterate: function(callback){

// execute callback for each attendee in attendees }

};

};

Before you dive into implementing the iterate functionality, you need to write unit tests to verify its behavior. The unit tests for the iterate function are shown in Listing 5-2.

LISTING 5-2: Unit tests for the attendeeCollection.iterate function (code filename:

Callbacks\attendeeCollection_tests.js)

describe('Conference.attendeeCollection',function(){

describe('contains(attendee)', function(){

// contains tests });

describe('add(attendee)', function(){

// add tests });

describe('remove(attendee)', function(){

// remove tests });

describe('iterate(callback)', function(){

var collection, callbackSpy;

// Helper functions

function addAttendeesToCollection(attendeeArray){

attendeeArray.forEach(function(attendee){

collection.add(attendee);

});

}

function

verifyCallbackWasExecutedForEachAttendee(attendeeArray){

// ensure that the spy was called once for each element

expect(callbackSpy.calls.count()).toBe(attendeeArray.length);

// ensure that the first argument provided to the spy // for each call is the corresponding attendee

var allCalls = callbackSpy.calls.all();

for(var i = 0; i < allCalls.length; i++){

expect(allCalls[i].args[0]).toBe(attendeeArray[i]);

} }

beforeEach(function(){

collection = Conference.attendeeCollection();

callbackSpy = jasmine.createSpy();

});

it('does not execute the callback when the collection is empty', function(){

collection.iterate(callbackSpy);

expect(callbackSpy).not.toHaveBeenCalled();

});

it('executes the callback once for a single element collection', function(){

var attendees = [

Conference.attendee('Pete', 'Mitchell') ];

addAttendeesToCollection(attendees);

collection.iterate(callbackSpy);

verifyCallbackWasExecutedForEachAttendee(attendees);

});

it('executes the callback once for each element in a collection', function(){

var attendees = [

Conference.attendee('Tom', 'Kazansky'),

Conference.attendee('Charlotte', 'Blackwood'), Conference.attendee('Mike', 'Metcalf')

];

addAttendeesToCollection(attendees);

collection.iterate(callbackSpy);

verifyCallbackWasExecutedForEachAttendee(attendees);

});

});

});

There’s quite a bit of code to digest in Listing 5-2, but it isn’t particularly complicated once you understand the primary goals of testing code that uses the Callback Pattern. The tests specific to callback functionality need to

ensure that:

The callback was executed the correct number of times.

The callback was executed with the correct arguments each time.

The first step in achieving the testing goals is to create some sort of callback function that can keep a record of each time it is executed, including the arguments that were provided when it was executed. Jasmine spies,

introduced in Chapter 2, provide this functionality (and more).

The tests for the iterate function in Listing 5-2 each use the callbackSpy

Jasmine spy instance, which is initialized in the beforeEach block prior to the execution of each test via callbackSpy = jasmine.createSpy();.

NOTE A spy created with createSpy is a bare spy. Unlike spies created with a call to spyOn(someObject, 'someFunction'), bare spies don’t require a pre-existing object and function to spy on.

Bare spies have no functionality beyond “spy stuff” such as tracking invocations; you can’t set them up to call through to the implementation of the spied on function (because there is none), nor can you configure them to call some other function when invoked.

Even with these limitations, a bare spy is perfect to ensure that a callback function is executed appropriately.

Each of the tests sets up the collection object, also created in the beforeEach block, to contain the number of attendee objects appropriate for the test.

The tests then execute collection.iterate(callbackSpy); which, once

iterate is implemented, should cause callbackSpy to be executed once per attendee in the collection.

The helper function verifyCallbackWasExecutedForEachAttendee performs the heavy lifting in each test; it’s responsible for ensuring that the goals for testing code using the Callback Pattern have been satisfied.

The callbackSpy automatically collects information about each time it is executed into an object that is added to its calls property. Comparing the number of times the spy was called—by counting the number of elements in the calls property—with the number of elements that were added to the collection with the statement

expect(callbackSpy.calls.count()).toBe(attendeeArray.length);

ensures callbackSpy was called a number of times equal to the number of

attendee objects in the collection.

The rest of the verifyCallbackWasExecutedForEachAttendee is dedicated to verifying that each invocation of callbackSpy was provided with the correct argument, specifically the appropriate attendee in the collection.

var allCalls = callbackSpy.calls.all();

for(var i = 0; i < allCalls.length; i++){

expect(allCalls[i].args[0]).toBe(attendeeArray[i]);

}

All of the calls recorded by callbackSpy are gathered and iterated through.

Each of the call objects has an array property, args, which contains all of the arguments provided to that call. Comparing the first argument of each

call.args with the corresponding attendee satisfies the second testing goal:

that the callback is invoked with the correct arguments.

Now that you have tests in place that exercise the iterate function, it’s time to implement it. You will recall from Chapter 4 that the forEach function is available as a property of JavaScript arrays, and it accepts a callback function that is executed once per element in the array, providing the element as the first argument to the callback.

The forEach function seems to be the perfect tool to implement the iterate function of the attendeeCollection. Listing 5-3 provides the fully

implemented attendeeCollection.

LISTING 5-3: The full implementation of the attendeeCollection module (code filename:

Callbacks\attendeeCollection.js)

var Conference = Conference || {};

Conference.attendeeCollection = function(){

var attendees = [];

return{

contains: function(attendee){

return attendees.indexOf(attendee) > -1;

},

add: function(attendee){

if(!this.contains(attendee)){

attendees.push(attendee);

} },

remove: function(attendee){

var index = attendees.indexOf(attendee);

if(index > -1){

attendees.splice(index, 1);

} },

getCount: function(){

return attendees.length;

},

iterate: function(callback){

attendees.forEach(callback);

} };

};

The implementation in Listing 5-3 causes all of the unit tests in Listing 5-2 to pass, as Figure 5.1 makes evident.

Figure 5.1

You now have a functional attendeeCollection, but you haven’t completed the task that you were presented with at the beginning of this section. You still need to implement the code that checks in the attendees, and records the check-ins in an external system.

Writing and Testing Callback Functions

Now that you have attendeeCollection, implementing the additional

required functionality is as simple as crafting a callback function that checks in an individual attendee. You could do this by defining an anonymous

function that checks in an attendee and providing that anonymous function directly to the attendeeCollection.iterate function:

var attendees = Conference.attendeeCollection();

// Add the attendees that were selected in the UI attendees.iterate(function(attendee){

attendee.checkIn();

// register check-in with external service });

Even if you only have limited exposure to JavaScript, you’ve probably seen code that looks like the previous example many times. The capability to

define a function and pass it immediately to another function as a callback is powerful feature of JavaScript, but its use can be a deviation from the path to reliability.

First, anonymous callback functions aren’t unit-testable; there’s no way to separate the callback from the function it’s provided to. In the case of the example, the act of checking in an attendee has been coupled to the

attendeeCollection. Therefore, to properly test that the attendees in the collection are checked in, you’d have to repeat the tests that you already created for the collection itself, except you’d test that the attendees were checked in and the check-ins recorded rather than the callback was executed.

If testing even just a single anonymous function callback results in a WET test suite, imagine if multiple tasks were completed using anonymous functions: There would be a flood of repetition.

Second, and far less significant than the testing difficulty just mentioned, is that anonymous functions can make debugging more difficult. Because the anonymous function—by definition—doesn’t have a name, there’s nothing for a debugger to display in the call stack. Figure 5.2 is a screenshot of the

Chrome Developer Tools when paused at a breakpoint set inside the anonymous function in the example.

Figure 5.2

When waiting at the breakpoint, outlined on the left, Chrome displays

(anonymous function) in the call stack, outlined on the right. When you don’t

have a function name to refer to, you don’t have an inkling of the context in which the function is being executed. This complicates the debugging task.

This is made worse if you’re debugging code reached through a series of anonymous callbacks. Each (anonymous function) entry in the call stack would represent a mystery you have to investigate in order to get a full picture of the context in which the code in question is being executed.

Thankfully, functions defined and provided directly as callbacks can be named. While this doesn’t make them any more testable, it does make the debugging experience less challenging. This example is identical to the previous one except that it provides a name, doCheckIn, for the callback function:

var attendees = Conference.attendeeCollection();

// Add the attendees that were selected in the UI attendees.iterate(function doCheckIn(attendee){

attendee.checkIn();

// record check-in with external service });

Now the Chrome Developer Tools have something to list in the call stack, outlined in Figure 5.3, providing context to you (or some other developer) to make debugging a bit easier.

Figure 5.3

If anonymous (and named) functions defined and provided directly as callbacks lead to difficult-to-test code, what is a better—testable—way to implement the callback that checks in an attendee?

We suggest that checking in an attendee is a significant responsibility, one

that should be encapsulated into its own module, called checkInService. Doing so provides a testable unit and also promotes code reuse by decoupling the act of checking in an attendee from the attendeeCollection.

NOTE Many people fear that test-driven development will lead to improvised, haphazard code. On the contrary, careful attention to the details of testing will improve the structure of your programs.

Additionally, as the section “Using the Module Pattern” illustrated in Chapter 3, dependencies may be injected into checkInService. It’s reasonable to

consider registration of a check-in with an external system as a separate responsibility, so it is appropriate to inject an object that has the registration responsibility into checkInService. That object is an instance of

checkInRecorder, a module you will develop later. (See Chapter 6.)

Listing 5-4 provides a test suite that exercises the basic functionality of the

checkInService .checkIn function.

LISTING 5-4: Tests for the

checkInService.checkIn(attendee) function (code filename:

Callbacks\checkInService_tests.js)

describe('Conference.checkInService', function(){

var checkInService, checkInRecorder, attendee;

beforeEach(function(){

checkInRecorder = Conference.checkInRecorder();

spyOn(checkInRecorder, 'recordCheckIn');

// Inject the checkInRecorder, with the spy configured on // its recordCheckIn function

checkInService = Conference.checkInService(checkInRecorder);

attendee = Conference.attendee('Sam', 'Wells');

});

describe('checkInService.checkIn(attendee)', function(){

it('marks the attendee checked in', function(){

checkInService.checkIn(attendee);

expect(attendee.isCheckedIn()).toBe(true);

});

it('records the check-in', function(){

checkInService.checkIn(attendee);

expect(checkInRecorder.recordCheckIn).toHaveBeenCalledWith(attendee);

});

});

});

Extracting the check-in and check-in recording functionality into separate, decoupled modules yields unit tests for checkInService.checkIn that are succinct and simple.

The implementation of checkInService is equally simple and is shown in Listing 5-5.

LISTING 5-5: Implementation of the

checkinService.checkIn(attendee) function (code filename:

Callbacks\checkInService.js)

var Conference = Conference || {};

Conference.checkInService = function(checkInRecorder){

// retain a reference to the injected checkInRecorder var recorder = checkInRecorder;

return {

checkIn: function(attendee){

attendee.checkIn();

recorder.recordCheckIn(attendee);

} };

};

The entire test suite now passes, as shown in Figure 5.4.

Figure 5.4

Finally, the following snippet shows how to tie together the three

independent, tested, and reliable modules to complete the task of checking in a conference attendee using the Callback Pattern.

var checkInService =

Conference.checkInService(Conference.checkInRecorder()), attendees = Conference.attendeeCollection();

// Add attendees selected in the UI to the attendee collection attendees.iterate(checkInService.checkIn);

Một phần của tài liệu reliable javascript how to code safely in the world s most dangerous language spencer richards 2015 07 13 Lập trình Java (Trang 188 - 200)

Tải bản đầy đủ (PDF)

(696 trang)