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:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 🙂