Listing 15.85 Expecting the message form to clear message "test should clear form after publish": function { var el = this.element.getElementsByTagName"input"[0]; el.value = "NP: A visi
Trang 1Listing 15.85 Expecting the message form to clear message
"test should clear form after publish": function () { var el = this.element.getElementsByTagName("input")[0];
el.value = "NP: A vision of misery";
this.controller.handleSubmit(this.event);
assertEquals("", el.value);
}
Ideally, we would not clear the form until we know for sure the message was sent Unfortunately, the cometClient does not support adding a success callback
at this point, so the best we can do is clearing it immediately after having sent it and hope for the best The proper fix would include adding a third options argument
to cometClient and wait for success Listing 15.86 shows the message form controller’s updated handleSubmit
Listing 15.86 Clearing the message after publishing it
function handleSubmit(event) { /* */
input.value = "";
}
It would also be nice if the message form gave focus to the input field immedi-ately upon initializing it I will leave doing so as an exercise
15.6.2 Notes on Deployment
Copy over the message form and message list controllers to chapp’s public di-rectory and reload your browser The application should now be slightly smoother
to use
Simply copying files to deploy them is cumbersome and error prone Addi-tionally, serving the application with 15 individual script files is not optimal for performance If you installed Ruby and RubyGems to use the jstestdriver
and jsautotest tools in Chapter 3, Tools of the Trade, then you have a JavaScript
and CSS concatenator and minifier at your fingertips Listing 15.87 shows the three required commands to install Juicer, which will conveniently package your scripts for deployment
Trang 2434 TDD and DOM Manipulation: The Chat Client
Listing 15.87 Installing Juicer and YUI Compressor
$ gem install juicer
$ juicer install yui_compressor
Run from the root of the Node.js application, the command in Listing 15.88 will produce a single file, chat.min.js, containing the entire client-side application
Listing 15.88 Using Juicer to compress files
juicer merge -s -f -o public/js/chat.min.js \
public/js/function.js \ public/js/object.js \ public/js/tdd.js \ public/js/observable.js \ public/js/form_controller.js \ public/js/user_form_controller.js \ public/js/json2.js \
public/js/url_params.js \ public/js/ajax.js \
public/js/request.js \ public/js/poller.js \ public/js/comet_client.js \ public/js/message_list_controller.js \ public/js/message_form_controller.js \ public/js/chat_client.js
The final result is a 14kB JavaScript file containing a fully operational chat room
Served with gzip compression, the total download should be about 5kB
Juicer is also able to find dependencies declared inside script files, meaning that
we can jot down each file’s dependencies inside comments in them and then simply
run “juicer merge chat.js” to produce the complete file, including the dependencies
More information on Juicer is available from the book’s website.4
15.7 Summary
In this chapter we have been able to pull together a lot of the code developed
throughout this book to create a fully functional, entirely JavaScript based
browser-based chat application And we did it all using test-driven development, right from
the very start
4 http://tddjs.com
From the Library of WoweBook.Com
Please purchase PDF Split-Merge on www.verypdf.com to remove this watermark.
Trang 3The key aspect of this chapter has been unit testing DOM manipulation, and structuring the outermost application layer in a sensible way As we’ve discussed numerous times already, well factored software easily lends itself to unit testing, and the GUI—the DOM—is no exception to this rule
By employing the Model View Presenter/Passive View pattern, we were able
to identify reusable components in the view and implement the chat client in a modular way, resulting in very loosely coupled modules that were easy to test in isolation Developing these components using TDD was straightforward because each distinct unit had a well-defined responsibility Dividing a hard problem into several smaller problems is a lot more manageable than trying to solve it all in one go
An interesting aspect about a pattern such as Model View Presenter is that there are numerous ways to apply it to the problem domain of client-side JavaScript For instance, in many cases a portion of the DOM will represent the model because JavaScript widgets frequently manipulate the data already found on the page
The chat client was the final test-driven example, and we have reached the end
of Part III, Real-World Test-Driven Development in JavaScript In the final part of
the book we’ll draw some lessons from the past five chapters as we dive deeper into stubbing and mocking, and finally identify some guidelines for writing good unit tests
Trang 4This page intentionally left blank
From the Library of WoweBook.Com
Please purchase PDF Split-Merge on www.verypdf.com to remove this watermark.
Trang 5Part IV Testing Patterns
Trang 6This page intentionally left blank
From the Library of WoweBook.Com
Please purchase PDF Split-Merge on www.verypdf.com to remove this watermark.
Trang 716
Mocking and Stubbing
While using test-driven development to develop five sample projects, we’ve become intimately familiar with the stubFn function We have used it as a tool to both inspect interaction between objects, as well as isolating interfaces under test
But what exactly is a stub? We are about to find out as we dive a little deeper into
the topic of using test doubles, objects that look like the real thing but really are
bleak impersonations used to simplify tests
In this chapter we will look at the general theory of using test doubles, and get to know a few common types of test doubles a little better Because we have
already used stubs extensively in tests throughout Part III, Real-World Test-Driven Development in JavaScript, we will relate the discussion to previous examples We
will also look at a more capable stubbing and mocking library and see how such a thing can be used in place of stubFn and other homegrown helpers to simplify some of the tests we have written so far
16.1 An Overview of Test Doubles
A test double is an object that supports the same API, or at least the parts of it relevant to a given test, as the real thing, but does not necessarily behave the same way Test doubles are used to both isolate interfaces and make tests more convenient;
making tests faster, avoiding calls to inconvenient methods, or spying on method calls in place of assertions on direct or indirect output
Trang 8The terminology used in this chapter is mostly adapted from Gerard Meszaros book “xUnit Test Patterns,” [7] slightly adjusted to the world of JavaScript In
addition to the names and definitions of different types of test doubles, I will use
“system under test” to describe the code being tested
16.1.1 Stunt Doubles
Gerard Meszaros compares test doubles to Hollywood’s stunt doubles Some movie
scenes require dangerous stunts, physically demanding feats or other behavior that
the leading actor is either not willing or able to perform In such cases, a stunt
double is hired to do the job The stunt double need not be an accomplished actor,
he simply needs to be able to catch on fire or fall off a cliff without being mortally
wounded; and he needs to look somewhat like the leading actor, at least from a
distance
Test doubles are just like stunt doubles They take on the job when it’s incon-venient to use the leading star (production code); all we require from them is that
the audience (system under test) cannot tell it apart from the real deal
16.1.2 Fake Object
The stubs we’ve been using aggressively throughout the example projects in Part III,
Real-World Test-Driven Development in JavaScript, are one form of test doubles.
They appear to behave like real objects, but their actions are pre-programmed to
force a certain path through the system under test Additionally, they record data
about their interaction with other objects, available in the test’s verification stage
Another kind of test double is the fake object A fake object provides the same
functionality as the object it replaces and can be seen as an alternative
implementa-tion, only its implementation is considerably simpler For example, when working
with Node.js the file system can easily become inconvenient from a testing
perspec-tive Constantly accessing it can make tests slower, and keeping a lot of test data
on disk requires cleanup We can alleviate these problems by implementing an
in-memory file system that supports the same API as Node’s fs module and use this
in tests
Fakes differ from stubs in that stubs are usually created and injected into the system from individual tests on a per-need basis Fakes are more comprehensive
replacements, and are usually injected into the system as a whole before running
any tests Tests are usually completely unaware of the fakes because they behave
just like the objects they mirror, only significantly simplified In the Node.js file
system example we can imagine a complete implementation of the fs module as
From the Library of WoweBook.Com
Please purchase PDF Split-Merge on www.verypdf.com to remove this watermark.
Trang 9an in-memory file system The test setup can then make sure to place the fake implementation ahead of the built-in one on the load path Neither individual tests nor production code will be aware that require("fs") actually loads a simplified in-memory file system
16.1.3 Dummy Object
A dummy object, as its name suggests, is usually just an empty object or function
When testing functions that expect several parameters, we are often only concerned with one of them at a time If the function we’re testing throws errors for missing
or wrongly typed arguments, we can pass it a dummy to “shut it up” while we focus
on behavior not related to the argument in question
As an example, consider the test in Listing 16.1 from Chapter 15, TDD and DOM Manipulation: The Chat Client The test verifies that the message list controller sets
the element’s scrollTop equal to the value of its scrollHeight However, the method also appends a new DOM element to the view element, and throws an exception if it does not have an appendChild method For the purpose of this test we use a dummy to pass the test on appendChild to get to the behavior we want to test
Listing 16.1 Using a dummy function
"test should scroll element down": function () { var element = {
appendChild: stubFn(), scrollHeight: 1900 };
this.controller.setView(element);
this.controller.addMessage({ user:"me",message:"Hey" });
assertEquals(1900, element.scrollTop);
}
16.2 Test Verification
Unit tests have four stages; setup, often divided between a shared setUp method and test specific configuration of objects; exercise, in which we call the function(s)
to test; verification, in which we assert that the result of the exercise stage coincides with our expectations; and finally tear down, which never happens inside a test, but
rather in a dedicated and shared tearDown method
Trang 10Before we get into the nitty-gritty of stubs, mocks, and the difference between them, we will explore our options at the verification stage As we will see shortly,
veri-fication strategy is a central issue when making the choice between stubs and mocks
16.2.1 State Verification
Many of the tests in Part III, Real-World Test-Driven Development in JavaScript,
determine success by asserting that certain objects have a specific state after some
function was called As an example, consider Listing 16.2 from Chapter 15, TDD
and DOM Manipulation: The Chat Client, which expects the user form controller
to set the currentUser property of the model object It passes a dummy model
object to the controller, and then inspects the object’s currentUser object to
verify its behavior
Listing 16.2 Inspecting an object’s state to verify test
"test should set model.currentUser": function () {
var model = {};
var event = { preventDefault: stubFn() };
var input = this.element.getElementsByTagName("input")[0];
input.value = "cjno";
this.controller.setModel(model);
this.controller.setView(this.element);
this.controller.handleSubmit(event);
assertEquals("cjno", model.currentUser);
}
The fact that the last line inspects a property of an object passed to the system
under test to verify its success is called state verification State verification leads to
intuitive tests that clearly describe the outcome of using some part of the system In
this case, if the input field contains a username when the controller handles a submit
event, we expect it to transfer this username to the model object’s currentUser
property The test does not say anything about how this should happen, thus it is
completely detached from the implementation of handleSubmit
16.2.2 Behavior Verification
In many cases, testing the direct output of a test is not as simple as in Listing 16.2
For instance, keeping with the chat client example, the message form controller is
in charge of publishing messages from the client to the server through the model
object Because there is no server in the tests, we cannot simply ask it for the message
From the Library of WoweBook.Com
Please purchase PDF Split-Merge on www.verypdf.com to remove this watermark.
Trang 11we expected it to receive To test this, we used a stub, as seen in Listing 16.3 Rather than inspecting some object’s state to verify its results, this test stubs the model’s publishmethod and then proceeds by asserting that it was called
Listing 16.3 Inspecting a function’s behavior to verify test
"test should publish message": function () { var controller = Object.create(messageController);
var model = { notify: stubFn() };
controller.setModel(model);
controller.handleSubmit();
assert(model.notify.called);
assertEquals("message", model.notify.args[0]);
assertObject(model.notify.args[1]);
}
This test contrasts with the previous one that used state verification It does
not check whether the message was stored somewhere, instead it uses behavior verification; it verifies that the model’s publish method was called with the correct
arguments Having already tested the Comet client to be used in production, we know that the message will be handled correctly if publish is called this way
16.2.3 Implications of Verification Strategy
The chosen verification strategy directly influences how a test reads, which is obvious from looking at the two tests above Less clear is the fact that the verification strategy also influences production code, as well as its relationship to the tests
Behavior verification taps into the system’s implementation by expecting certain function calls to take place On the other hand, state verification is a mere obser-vation on the (direct or indirect) input/output relationship This means that using behavior verification extensively couples the test code tighter to the system, which
in turn limits our ability to change its implementation, e.g., through refactoring, without also having to change the tests
16.3 Stubs
Stubs are test doubles with pre-programmed behavior They may return a specific value, regardless of received arguments, or throw an exception Because stubs are used in place of real objects and functions, they are also used as a measure to avoid bumping into inconvenient interfaces