On Toothpaste and Testing

I often call the approach of bottom-up testing toothpaste testing. You start from the bottom and slowly squeeze your way to the top. It goes hand in hand with toothpaste refactoring, which follows the same pattern and is supported by toothpaste testing.

Toothpaste refactoring provides an alternative to the legacy rescue approach of using characterization tests to drive down from the top. Both have their places. I have seen many occasions in which seemingly beneficial refactorings—such as adding dependency injection or changing signatures—percolate down through the call hierarchy only to be stopped by some strange construct. Additionally, top-down refactoring runs the risk of going long periods of time without your testing safety net, as you break layer after layer, changing its shape.

Working from the bottom up requires some level of confidence—or at least optimism—that the lower-level entities are valuable to test. You will also have to do a lot of reverse engineering of intent. But if you think the overall design and implementation are sound and just missing tests, the toothpaste approach works well.

The other utility functions to parse the date and time strings are a little more complicated, so I decided to turn my attention to the public $.timepicker functions. Starting with timezoneOffsetNumber(), testing uncovered a seeming discrepancy of intent against the comments [83b6840].

Moving to timezoneOffsetString() [7bc2133] uncovered an imbalanced range validation for which the upper boundary of the range was checked but not the lower value. I enhanced the documentation to explain the “magic” numbers in the code that corresponded to the highest and lowest actual time zone offsets in minutes. Once test protection was in place, I was also able to eliminate more “magic” numbers to simplify zero-padding the digits in the result string.

Testing the log() function triggered the first use of Jasmine’s built-in spy capability [1adc385]. Timepicker’s logging is a thin wrapper that makes sure that the console exists before trying to log through it. I believe this accounts for a Firefox compatibility issue. For testing, I had to determine whether the requested message was output with and without window.console being defined. Technically, you cannot prove that the function did not output the message. I relied on the white-box knowledge that it uses console.log() to verify that it did not call that method. Fortunately, you can spy on these kinds of “system” functions as easily as any other function.

Covering timezoneAdjust() with tests [b23be92] allowed the safe simplification of the computation in [af26afc]. I tested this function somewhat reluctantly. The JavaScript Date object seems incomplete with respect to its time zone handling. Its API omits the ability to change the time zone. The constructor sets it to the browser’s time zone, from which point it remains read-only. Methods like this one try to compensate by adjusting the minutes by the change in offset. This allows most date math operations to yield useful results in a pragmatic way, but the underlying principle is flawed from an international date- and time-handling perspective. However, JavaScript has this limitation, and the rest of us need to live with it.

While the handleRange() function is complex, the value of the other range functions is simply to call it in the right way. I tested the callers independently using the spy feature [9254a37]. Listing 15-4 shows an example of this usage. I refactored the tests into a subgroup in [afb4c98] and deferred handleRange() itself by adding a placeholder [f363202].

Listing 15-4: Using a spy to verify the value added by a function that simply calls another function with the right arguments

var startTime = $('<p>start</p>'),
    endTime = $('<p>end</p>'),
    options = {};

beforeEach(function() {
  spyOn($.timepicker, 'handleRange'),
});

it('timeRange calls handleRange the right way', function() {
  $.timepicker.timeRange(startTime, endTime, options);

  expect($.timepicker.handleRange)
    .toHaveBeenCalledWith('timepicker',
      startTime, endTime, options);
});

Ignoring the explicit documentation in front of my face, I initially passed strings as the startTime and endTime parameters, but fortunately IntelliJ corrected me.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.148.144.228