WHAT’S IN THIS CHAPTER?
There used to be a TV show called This Is Your Life. First aired in 1948, it enjoyed several revivals and specials through the 1980s. A fairly ordinary person would be surprised to find himself on the show, where his life would be reviewed. The subject’s long-lost childhood friends might make an appearance; his elementary school teacher, now in her nineties, would say how she still remembers what a good boy he was; and so on. Needless to say, the show would have quite an effect on the surprised subject, and the audience, too, would be deeply moved.
As you review the portion of your life spent in the preceding 23 chapters, you will see one friend who was always there for you: test-driven development. Over and over again, it has played an important role in making your software more reliable. In this chapter, let’s look back on that friendship. Although you may not be moved to tears as the subjects of This Is Your Life often were, we hope you will be inspired to keep test-driven development by your side.
Like an old friend, the unit tests of test-driven development are with you from the very beginning. They help you think through a problem ahead of time by clarifying how your program should behave, rather than merely verifying that it runs as you have written it.
Unit tests have got your back. If you want to refactor the code, the complete code coverage that is the inevitable result of test-driven development will alert you if the result is not quite right.
Counterintuitively, test-driven development’s insistence that functionality be added in small increments does not lead to ad hoc code. We’ve all worked on applications that have grown worse and worse over time because all the developers on the team are afraid to touch what’s there. Refactoring is out of the question because it would be tantamount to a rewrite. With test-driven development, your unit tests inevitably cover nearly 100 percent of your code, enabling the refactoring that keeps it clean and DRY. This is true from the very beginning of the development process. “Test, code, refactor, repeat” is not only the mantra of test-driven development but the key to making your code elegant.
In Chapter 2, you saw how one of the most elegant pieces of code we have ever encountered, the Aop.js
library, could be built up in a test-driven approach.
Not only that, but you saw that it was possible to use test-driven development to understand and code that little gem.
When test-driven development has helped you reach your goal of a working program, it continues to be your friend. The output of the unit tests (Jasmine’s, in this book) reads like a functional specification, helping future developers understand and appreciate what you have done...and keeping them from messing it up.
As you review the role test-driven development has played in this book, a few general principles will stand out. First among them is to write code that is unit-testable.
It’s simple: To make a program that is unit-testable, write your program in units. By this, we mean modules with small interfaces (see Chapter 3) and a single responsibility (see Chapter 1).
When one unit must use the services of another, dependency injection is your SOLID (see Chapter 1) buddy. If you supply one unit’s services to another through the latter’s constructor function, you can supply a mock in unit tests, thereby limiting the actual code under test to one unit, not two.
The following is the basic cycle of test-driven development:
The most important step, and the one we have found developers have the most trouble with, is the first one: writing the test before writing the code. Maintaining this discipline is incredibly difficult but is absolutely the key to reliability. Never let your code get ahead of your tests.
A test that is written after the code will initially pass rather than fail. How can you know that it passes because the code is correct, and not because the test is faulty?
Often, your tests will contain more lines of code than their subjects. This is a very broad hint that it’s just as important to keep your tests DRY as to keep your code in good shape. Jasmine’s nested structure, with the possibility of using beforeEach
at each level, makes this easy (see Chapter 2).
The first tests you write should be for error conditions. The reason is more psychological than technical. If you delay these tests until you’ve done all the “real” tests, you’re more likely to be tired of the subject under test, and more likely to cut corners so you can move on to other things. If, on the other hand, you save the most interesting and important tests for last, you have something to look forward to and will maintain enthusiasm until the end.
Beyond that, writing the error- and boundary-checking tests first will help you think through the parameters of the problem and may bring some additional positive tests to mind.
When testing the happy paths, start with the simplest. This will cause the subject under test to grow in the smallest possible increments. Which do you think is likely to be better-tested: a small increment written to fulfill a single test, or a larger increment that also fulfills one test? The answer is clear.
Starting with the simplest tests will tend to make your tests simple, too.
It is surprisingly easy to write tests that fall just short of testing what they’re supposed to test. For example, when testing an error condition, don’t be satisfied with the following:
expect(something).toThrow();
If the subject being tested does throw an error, how will you know it’s the right one? Maybe it threw the error before it even got to the part you’re trying to test. Instead, do this:
expect(something).toThrow(aSpecificError);
The specific error will often be a message that the subject exposes to the outside world for just this unit-testing purpose.
On the positive-testing side, don’t be content with this:
expect(something).toHaveBeenCalled();
How will you know it was called correctly? Instead, insist on the following:
expect(something).toHaveBeenCalledWith(the,proper,arguments);
For an example, see Listing 14-7 in Chapter 14.
Each test should verify just one thing. To be realistic, however, the one thing might be an array of conditions. You saw an example in Listing 11-3 in Chapter 11, where there was a test that new WidgetSandbox(toolsArray, widgetFcn)
threw an exception if widgetFcn
was not a function. Rather than writing a separate test for each kind of argument that is not a function, an array of non-functions was run through some test logic with a forEach
.
Thinking of your Jasmine output as a functional specification, you want it to be as detailed as possible. However, your policy should also be reasonable enough that developers don’t just give up.
Avoid test data that may do unintended favors for the subject under test. That means avoiding special numbers like 0 and 1 (unless those specific numbers pertain to the point of the test) and using different numbers for each piece of data. When testing a sort
, be sure to use data that sorts differently as strings than as numbers. (See the discussion following Listing 14-8 in Chapter 14.) If the real-life data are in a one-to-many relationship, be sure the test data are, too, and be sure to test the one-to-many behavior.
Most programmers are more interested in writing code than writing data. Don’t let a lack of interest make you lazy!
The Jasmine functions describe
and it
each take a string as a first argument. If you word the strings in each nested set of describe
s and it
s in such a way that their concatenation produces a sentence, the output of the unit tests will read like a functional specification. This discipline will also clarify in your own mind what you are trying to test.
We have seen many cases where a test passes when run by itself but fails when run as part of the whole suite. Invariably, this is because something is initialized once when it should be initialized in a beforeEach
.
Also, be sure to use afterEach
to clean up after your tests where necessary. For example, if each test creates an element on the DOM, use afterEach
to remove the element so subsequent tests will not confuse it with the elements they create.
The preceding principles apply wherever you use test-driven development, but a large section of this book was devoted to test-driven development of specific software-engineering patterns.
Aspect-oriented programming has made many appearances on these pages, and we hope you will find just as many uses for this powerful pattern in your code. When you write aspects, keep the following things in mind.
A library like Aop.js
is guaranteed to pass the proper arguments to the decorated function and capture the return value. However, if you use aspect-oriented techniques without the library, those are things you should test.
If it is important to spy on a function that is decorated with an aspect, be sure to capture a reference to the undecorated function before the aspect is applied. Then, spy on the undecorated version, as we did in Chapter 8 when testing the returnValueCache
memoization aspect.
In Chapter 3, you saw many ways to construct objects in JavaScript. Some of them warrant special care in testing.
Object literals are so easy to make in JavaScript that they tend to creep into a code base untested. The reliable way to make them is to use a factory method that is subject to all the usual rigors of test-driven development.
If an object is designed to be constructed with new
, be sure to include a test for what happens if it is not. An improper use of the constructor should throw an exception and the test should verify that this exact exception was thrown.
When you make an object with monkey-patching, the donor should manage the patching. It should verify that the recipient meets whatever requirements may exist, and unit tests should verify that the donor does this properly.
Callbacks are everywhere in JavaScript code and demand special unit-testing, as Chapter 5 explained. When testing the code that is doing the callback, the usual procedure in Jasmine is to spy on the callback function and ensure that it is called with the right arguments (see Listing 5-2 in Chapter 5).
When testing the callback functions themselves, take special care that the this
within the function is what you expect. See the section “Minding this” in Chapter 5. Avoid writing inline callbacks that can’t be unit-tested. The worst case is the “callback arrow” (see Listing 5-6, also in Chapter 5).
As Chapter 6 showed, Promise
s can be tricky, both to write and to test.
If you’re not careful, the Promise
you’re testing may be unresolved when your test finishes. If the code inside the Promise
is faulty, you won’t know about it. Listing 6-4 in Chapter 6 showed how to use Jasmine’s done()
mechanism to solve this problem.
Be aware that if one of the callbacks in Promise.then(resolveCallback, rejectCallback)
is called and does not return anything, what you actually get is a Promise
resolved as undefined
. Often, it’s sufficient in a test to construct a Promise
in an already-settled state (either fulfilled or rejected), but sometimes you need the Promise
to settle at a particular moment during a test. For these occasions, use a Promise
-wrapping library such as AngularJS
’s $q
.
Chained promises can surprise you. For example, if the code follows the rejection branch of a Promise
but if that branch returns a fulfilled Promise
, the result of the chain will be a fulfilled Promise
(which you might not expect, considering that a rejection had happened). Be sure to test your execution flow in all cases.
As Chapter 7 made clear, a unit test of a partial function application should only consist of ensuring that the underlying (full) function gets called with the correct arguments. There is no need to write tests for the underlying function in this context in addition to the tests that have surely been in place since before that function was written!
Similarly, your tests of the Memoization Pattern (see Chapter 8) need not concern themselves with the memoized function per se. They just have to ensure that multiple calls through the memoizer with the same arguments result in just one call, with the correct arguments, to the underlying function, and that the underlying function’s return value is passed through the memoizer. For this purpose, you can replace the underlying function with a spy.
Memoization can be turned into an aspect, at which point all the usual principles of testing an aspect apply. See the example in Chapter 8.
As you saw in Chapter 9, a Singleton brings a special consideration to the game in that every time you create an instance of it, usually with a function like getInstance()
, you are in fact getting the same instance. The test can be as simple as this:
expect(firstInstance).toBe(secondInstance);
Unlike C# or Java, you don’t need to worry about multiple threads accessing a Singleton because JavaScript is single-threaded.
In the Factory Pattern (see Chapter 10), the factory’s single responsibility is to construct and return another object. The single responsibility of its tests, therefore, is to verify that the correct arguments are passed to that object’s constructor, and the object thus constructed is returned. A Jasmine spy, with its ability to track arguments and return a value as commanded, should therefore be a stand-in for the real underlying object.
If you use the Sandbox Pattern (see Chapter 11), you will probably have help from a third-party product. However, if you’re writing your own sandbox component (and it’s not difficult), here are some things to consider in your tests.
The function to create a sandbox for a widget might look something like this:
function WidgetSandbox(toolsArray, widgetFcn)
Its unit tests can verify that widgetFcn
is indeed a function, that the function is executed, and that the sandbox is the first argument in the call (if that’s how your sandbox is designed).
Test the provision of tools to the sandbox by verifying that each tool’s constructor is called with the sandbox as an argument (again assuming that’s your design), and that each tool can be fetched by name.
To test an individual tool, verify that it adds itself to the sandbox. The sandbox does not have to be a real one for this test; it can be an empty object literal (see Listing 11-9 in Chapter 11). Of course, this is in addition to whatever tests are appropriate for the tool’s basic functionality.
Special tests for the widget that is sandboxed include one to verify that it throws the correct Error
s if the tools it requires are not available, and does not throw if they are. Again, a simple object literal that stands in for the real sandbox will keep your tests isolated to their real subject (see Listing 11-11 in Chapter 11).
It’s tempting to let the object being decorated, with whatever faults it may contain, become an unwitting subject of a decorator’s tests. To avoid this potential exposure, consider writing a simplified fake for the decorated object. Another advantage of a fake is that it’s usually easier to make it produce an error than to make the real object do so.
Speaking of which, your first test should be that errors in the fake are propagated through the decorator. With that done, you can verify that successful return values make their way up the call stack as planned. We recommend testing the actual functionality of the decorator as a last step. Chapter 12 has all the details.
The Strategy Pattern in Chapter 13 employed a factory to produce the right kind of object (the strategy) based on the needs of the moment. The implementation of the Strategy Pattern itself, as distinct from the factory, needed to be tested for error-handling only, for calling the factory properly to obtain the desired strategy, for calling the required function on the strategy, and for returning the strategy’s result. As you might guess by now, it’s best to mock everything but the subject under test (that is, mock the factory and the object it produces). The mock consists of fakes, spies, or most likely both.
You will recall from Chapter 14 that the Proxy Pattern consists of one object that serves as an expert in the use of an underlying object. What you want to test is how the proxy handles that object. This is easiest when the proxy receives the object by dependency injection, rather than constructing the object on its own. That lets you inject a spy instead, which can report to your tests how it was called and what it returned.
The proxy should concern itself with as little of its subject’s semantics as possible. Often this is very little indeed, which can result in very few tests. See the discussion following Listing 14-10 in Chapter 14.
A chainable method is usually nothing more than a method that returns this
. That’s something to test in addition to whatever other tests the method requires.
Sometimes, a method becomes chainable because it returns an object that has the same type or the same shape as this
. The then
method of the Promise
object is an example. It does not return the Promise
on which it was called, but a new Promise
. This, too, should be the subject of a test.
In Chapters 16 and 17, you saw how JavaScript, a language without interfaces, can benefit from the philosophy behind them. If you write code along those lines, something like the ContractRegistry
can help. It verifies that an object has the expected methods, and that those methods are called with the expected arguments. You can easily make the ContractRegistry
active during testing but vanish completely during production.
As explained at length in Chapter 18, the call
and apply
functions each take a parameter that becomes this
inside the invoked function. If that argument is not supplied, how do you want the code to behave? Remember that if the argument is absent in strict
mode, this
will be undefined
and in non-strict
mode it will be the window
object. An occasion for a test, surely!
When one object borrows a method from another, the chief danger is that the borrowed method might require something from its new host object. The safest course is to install code in the borrowed method to make it check for requirements. Much was said about this in Chapter 19, including the advisability of using an extra level of indirection and maybe an aspect.
As with other patterns that bring objects together in novel arrangements, consider using something like the ContractRegistry
.
Finally, be aware that it is surprisingly possible for the borrowed method, called from its new home, to affect even the private variables of its original home.
In Chapter 20, you saw how a general-purpose extend
function can facilitate the Mixin Pattern. When developing such a function, consider whether you want to copy only the donor’s “own” properties or its inherited properties as well. Then, write the appropriate tests.
Also consider what you want to do if the target object already has a property that is about to be imported from the mixin. Do you overwrite what’s there, leave it alone, or throw an Error
?
Test the mixin on an object that has the minimum requirements to receive it.
A functional mixin is one that contains a method to add itself to a host. This method is invoked with call
or apply
, with a context argument (the this
inside the method) that is the object being extended. The example from Chapter 20 was a mixin that added an id
property to any object thus:
Conference.mixins.addId.call(newAttendee);
If your mixin employs this pattern, the suggestions for call
and apply
pertain.
The Mediator and Observer Patterns of Chapter 21 both rely on “interfaces” between actors. You’ll want to ensure that the requirements of the interfaces are met. The ContractRegistry
can help.
In the Observer Pattern, you can use a fake, simplified version of the subject. Likewise, when testing a Mediator, you can use fake stand-ins for the real colleagues. As usual, the idea is to keep the focus on the subject under test, isolating your tests from the behavior of other objects.
The first rule of testing DOM access (see Chapter 22) is to avoid it. Put as much of your code as possible in functions that have nothing to do with the DOM. Then, your tests have to concern themselves only with simple questions like, “If I click this button, does this [already tested] function get called?”
Your Jasmine tests can add the DOM element in a beforeEach
and remove it in an afterEach
. Consider using jasmine-jquery
as an aid to interact with the DOM element from there.
Avoid unit tests that are concerned with appearance only; they can be brittle.
In Chapter 23, you saw several variations of the Magic Key Technique, which ensured that a component can only be called from other components as your architecture allows. In each variation, unit tests ensured that the key had the desired effect.
This chapter presented the principles of test-driven development you’ve encountered in this book, including the general mechanics of test-driven development and specific suggestions for many common patterns in software engineering.
If you came to JavaScript from another language, you might find it somewhat quirky—in a good way. The next chapter summarizes many of the idioms that give JavaScript its charm.