Sometimes I’m not satisfied with the way things are. I wish they could be a little bit different, just for a moment. I deal with this desire in my personal life by sighing and gazing through a rain-dotted window. When writing code, I take a more productive approach: I use seams.
During application development, there are plenty of times when you’d like to introduce some artificial behavior, “just for a moment.” For instance:
- Skip slow/expensive computation to speed up tests for unrelated behaviors
- Force failure conditions that you do not normally control (i.e. bad network connectivity) to make sure your application responds gracefully
- Isolate distinct modules during test execution so failures in one place don’t trigger related-but-distracting failures in dependent code
- Use pre-seeded data sources (e.g. a “staging” database)
- Circumvent access-controlled dependencies (e.g. OAuth services)
Maintain a reasonably-sized application, and you’ll have to account for
concerns like this before long. The most obvious approach is the most direct:
just edit the application logic in-place. Maybe you comment out a couple lines,
change a value, or extend a conditional expression with || true
.
I’d like to talk about why this practice should be avoided and how applying programming “seams” can make your code safer, easier to read, and easier to maintain. It’s also a whole lot more satisfying than gazing through a window.
On the Sly
So what do I have against the “direct modification” approach? A few things, actually.
They can be tricky to recreate. If you want to demonstrate something to a colleague, you might ask them to make a similar change. It’s possible that they do so in a slightly different way, and this can have subtle effects on what they experience.
For all but the smallest modifications, though, you’ll most likely use a more formal code sharing mechanism. But whether you find yourself e-mailing patch files to your coworkers or maintaining a “dev” branch of your application, this can itself be a hassle.
You could alleviate that problem by introducing branching logic in the production code itself, relying on some special “development mode” setting:
if (process.env.NODE_ENV === 'test') {
// Ad-hoc list of operations specific for test environments
} else {
// The real-life behavior of your application
}
I’ve seen this pattern in many projects, but I’ve never gotten used to it. It negatively impacts the code’s readability, forcing developers to routinely consider the testing environment as though it were of equal importance to the business logic itself. I’m a firm believer that tests ought to be accommodating; it’s hard enough to structure code when you are simply trying to satisfy business needs. “Obtrusive” or “pushy” testing environments add a whole new dimension of constraints that are only indirectly related to the problem at hand (and may account for some developers’ outright dismissal of automated testing).
Code review also becomes noisier with in-lined branching because modifying the artificial behavior will require changing the application source files. This means reviewers will have to be especially alert about unintended changes to the “production mode” behavior.
And in any case, these changes detract from authenticity and the integrity of
the application environment. There’s a real risk (be it from an accidental
commit to master
or a simple typo in an environment configuration) of
deploying these changes to the production system. While disabling
authentication may be real convenient while you test your code, your users
probably won’t appreciate you sharing that convenience with the rest of the
world.
Enter the Seam
Thinking in terms of “seams” can help you identify stronger methods of dynamic behavior modification. Michael C. Feathers offers a nice definition in Working Effectively with Legacy Code:
A seam is a place where you can alter behavior in your program without editing in that place.
This is getting a little abstract (I can see your eyes glazing over). Let’s talk about what this actually looks like in JavaScript.
A Brief Taxonomy
When thinking about seams in JavaScript, four programming patterns come to mind: preprocessing, methods, function parameters, and modules.
Preprocessing seams occur when the source code is authored to be transformed in some domain-specific way during the build process. So instead of writing,
var host = 'https://api.bocoup.com';
You might write:
var host = 'API_HOST';
…with the intention of specifying a valid URL for host
using text
substitution during every build. This approach adds a lot of power to the build
process, allowing you to (for instance) use a URL like http://api.local
for
local development, but still interact with https://api.bocoup.com
in your
production application.
This kind of seam is nice because it can be made extremely conspicuous. The previous example happened to be expressed as valid JavaScript, but you could instead define a wild-and-crazy substitution pattern such as:
var host = %%API_HOST%%;
This makes the fact that a substitution is occurring much more apparent. It also makes it very difficult to accidentally ship code prior to substitution–that code won’t even parse.
But depending on your perspective, you may consider this kind of obviousness as
a weakness. It’s really difficult to ignore %%API_HOST%%
when reading an
“ostensibly-JavaScript” file, which violates the “unobtrusive” trait that I
mentioned earlier.
Method seams, on the other hand, are quite a bit less obtrusive. In
object-oriented languages, this kind of substitution is commonly done though
subclassing. Subclassing is still possible in JavaScript (even without ES2015
class
syntax), but it doesn’t have to be so formal, either. Thanks to weak
typing and functions being first-class values, we can modify objects directly
in an ad-hoc way. When the behavior you would like to modify is defined as a
method on an object, you can over-write the method definition itself, no
additional structure necessary:
// Source: https://www.xkcd.com/221/
Algorithm.randomInt = function() {
return 4;
};
Here, we’re modifying the behavior of a supposed Algorithm
object by swapping
its random number generator with one that is just slightly more predictable.
This ability extends to prototypes, so if you need to change the behavior for
an entire class of objects, you can:
// Although `WeatherMan` may have been written to communicate with some
// external climate information service, this behavior may be slow, unreliable,
// or simply unavailable.
WeatherMan.prototype.report = function(callback) {
setTimeout(function() {
callback("It's darn cold today.");
}, 10);
};
// ...but now *all* "weathermen" will report consistent (albeit
// disappointing) weather patterns.
This pattern is so prevalent in testing environments that you can find tools dedicated to supporting it. Sinon.JS, for example, includes features like “spies”, “stubs”, and “mocks” that make exploiting method seams a snap.
Unfortunately, leveraging this seam requires runtime access to the target object(s). So if your project is built with Browserify (for example), you may not be able to swap methods immediately because most of your application’s variable references are encapsulated. This isn’t a deal-breaker, though; it just requires a little more care in how you expose your application’s internals.
Function parameter seams can be exploited wherever one function delegates
to another object that it receives as an argument. By way of demonstration, you
might have a Login
widget that allows users to authenticate with your
application.
var Login = function(auth) {
this.auth = auth;
};
// (...)
Login.prototype.onSubmit = function() {
this.auth.authenticate(function(err) {
if (err) {
this.displayError(err);
return;
}
this.emit('authorized');
});
};
Your application code would then wire these two components together:
window.myApp.login = new Login(new Auth());
Just like with method seams, this is a great way to “stub out” an arbitrary amount of functionality (as opposed to modifying a value). I’ve found it to be slightly more robust, though, because it often promotes looser coupling between your “stub” implementation and the real deal.
function FakeAuth() {
// etc.
}
// "Authenticate" by setting the token to a dummy value, preserving the
// method's asynchronous interface.
FakeAuth.prototype.authenticate = function fakeAuthenticate(done) {
setTimeout(function() {
document.cookie = 'token=1234';
done(null);
}, 0);
};
But it still suffers from that same drawback–you can only muck with this seam
in contexts where you are creating the Login
instance. For unit tests, this
is not a problem at all. Here’s an example of unit test “setup” code which just
exactly that:
var Login = require('../src/login');
var FakeAuth = require('./stubs/auth');
setup(function() {
this.subject = new Login(new FakeAuth());
});
// (tests for `Login` using `this.subject` follow...)
But if you want to change the behavior in your running application, you’ll need to take another tack.
Module seams avoid this problem by operating through the application module system itself.
Using this approach requires that you observe some sort of module system in your application code. In my experience, the most common setups are AMD modules via RequireJS, CommonJS modules via Node.js, CommonJS modules via Browserify, and ES2015 modules via Webpack.
The pattern is similar regardless of which module system you choose:
- Organize the functionality-to-be-modified within a single module
- Write your application code to use that module directly
- When executing tests or running in “development mode,” exploit the seam by dynamically modifying the module value
Unfortunately, every module system has a different mechanism for step 3. Module system APIs are outside the scope of this blog post, but I don’t want to leave you high-and-dry! Here’s a starting point for each system:
- I’ve described how this is done with AMD in an earlier post on this blog, Effective Unit Testing with AMD, so AMD users should refer to that post.
- Node.js exposes a little-known-but-stable property on the
require
function,require.cache
. You may over-write properties of that object to swap out the value that other modules receive when they execute, for example,require('./my-module')
- Browserify version 13 (the latest at the time of this writing) defines a
Node.js API that gives a lot of power over how it creates “bundles.” Of
particular relevance is the
require
method. Together with theexpose
option, you can override the values exported by any of your application’s modules. - Webpack version 2 supports an option named
resolve.alias
that allows you to specify what file is used when your application code requires any given module.
Regardless of the API, this approach may sound more coarse-grained than the method seam since it operates on modules. It’s not all-or-nothing, though. You could use this pattern to override targeted methods and properties. To do so, create a module that requires another, overrides the properties, and then exports the partially-modified version.
Here’s an example of such a module using Node.js:
var AppMath = require('./math');
// Copy all application methods:
for (var prop in AppMath) {
module.exports[prop] = AppMath[prop];
}
// Override one specific method:
module.exports.factorial = function(num) { return num * 2; };
A more concerning drawback is the seam’s implicit nature. I’ve carried on about tests being accommodating, but you might say, “you’ve gone too far, Mike.” By leveraging the module system–the plumbing of your application, really–the substitution mechanism becomes completely hidden from view. Other developers may be surprised to learn that any “funny business” is going on. This means that accidental breakage may be most likely with this seam. The good news is that because the seam operates at such a low level, “breakage” would be hard to miss–processes would exit and builds would fail.
I actually view this as a unique strength of this seam. Module seams may be the only option when you are interested in modifying the behavior of your top-level application code. For some, this is just further motivation to encapsulate code in reusable structures such as functions and classes. That is a great idea, but it’s not always an option. For those cases, module seams are perfect.
Not What it Seams
On the surface, it may look as though I am simply re-arranging code. After all, some of the problems I mentioned before are still present.
Danger of shipping the wrong “mode”
Because the seams exist in your application code, it’s still possible that the “fake” logic finds its way to production.
Seams are generally less susceptible to this risk because the switching mechanism itself is not hard-coded into the application. When you express these concerns with a seam, default behavior is overridden externally (e.g. by your test runner, by your build system, etc.). There is more friction involved in enabling these overrides, and that is a good thing for code safety.
Distracting implementation details
I made a fuss about how code like if (process.env.NODE_ENV) {}
is unnatural and distracting. One might argue that through the introduction of additional indirection, using seams in this way is also unnatural and distracting.
In most cases, code organization principles would motivate a seam long before testability concerns came into the picture. Setting up a database connection? Maybe the database name should be in a declarative configuration file. Logging a user interaction? Maybe you should use an interface as a facade for different analytics providers. So while seams may be used as a motivation to re-organize code, the benefits of the new structuring extend well beyond the enabling of this technique.
An Apt Metaphor
Just like its physical analog, a programming seam is a natural result of the production process. In the hands of a professional, it can be exploited to make drastic modifications that the original designers did not need to account for. Push it too far, though, and you’ll end up with a big hole where the armpit is supposed to go. So be careful!
Denim stock courtesy dschmieding