Decorate the @decorator to exclude in unit testing

One side effect of using decorators is that a decorator will run when running unit tests run against the [decorated] function. I wanted a way to signal that a decorator should be ignored (ie, should not apply itself to the underlying function) for a certain condition (where, the condition in this case is if we’re in a unit testing context).

I’m sure we could have a debate about whether this is a good thing or not, but in this case, the decorator was doing cross-cutting work that wasn’t directly related to the unit under test, so I wanted to test each in isolation.

My decorator was a stand-alone function [originally] and decorator syntax isn’t directly supported on stand-alone functions (only functions on object literals or methods of a class). So initially I wrapped my decorator in a function which would precede the decorator function at runtime and achieve my goal. So here’s essentially what that looked like:


const decoratorWrapper = (decorator) => {
return (target, key, descriptor) => {
descriptor.value = function (target, key, descriptor) {
if (window.jasmine) {
return descriptor;
}
return decorator(target, key, descriptor);
};
return descriptor;
};
};
const myDecorator function () {
// Here we're wrapping our decorator essentially in another decorator
return decoratorWrapper((target, key, descriptor) => {
const originalFunction = descriptor.value;
descriptor.value = async function () {
// do stuff before the decorated function
result = await originalFunction.apply(this, arguments);
// do stuff after the decorated function
return result;
};
return descriptor;
});
}

So while that worked, I wasn’t thrilled because I was essentially running a decorator-like function imperatively with too much ceremony. So then I looked at putting my decorator in an object literal (to satisfy the decorator syntax which doesn’t support stand-alone function decorators directly)  but then the descriptor takes on a slightly different shape, with an initializer property replacing the value property. Anyhow, I’m not thrilled syntactically with this but here’s the decorate-the-decorator approach, whereby our wrapper becomes a true decorator (in it’s own module), and our custom decorator just had to be tweaked a tiny bit into ES6 class syntax and to break up the decorator factory from the actual decorator function itself. We’re still able to construct the decorator class, then export the decorator function, such that consumers will never know about the weird syntax implementation in the decorator. Here’s what that looks like:


// ignoreDuringUnitTestDecorator.js
const ignoreDuringUnitTest () => (target, key, descriptor) => {
const originalFunction = descriptor.value;
descriptor.value = function (target, key, descriptor) {
if (window.jasmine) {
return descriptor;
}
return originalFunction(target, key, descriptor);
};
return descriptor;
};
// myCustomDecorator.js
class MyCustomDecorator {
constructor() {
this.decoratorFactory = this.decoratorFactory.bind(this);
}
@ignoreDuringUnitTest()
decorator(target, key, descriptor) {
const originalFunction = descriptor.value;
descriptor.value = async function () {
// do stuff before decorated function
result = await originalFunction.apply(this, arguments);
// do stuff before decorated function
return result;
};
return descriptor;
}
decoratorFactory() {
return this.decorator;
}
}
// Exporting as a default means consuming modules can import your decorator in a typical fashion
// without being aware of the weirdness of the class approach in here.
export default new VerifyWorkflowState().decoratorFactory;

And then we can decorate any function in a module as:


import myCustomDecorator from "./myCustomDecorator";
class Whatever {
@myCustomDecorator
myFunction(){
//….
}
}

And the decorator will run normally when run outside our jasmine unit testing context or will be skipped if so.

NOTE: I did add a tiny bit extra (not shown) to allow this exclusion to be temporarily paused so I could actually unit test the decorator itself too 🙂