Generic reading of arguments from multiple constructor calls - javascript

Follow-up question to Read arguments from constructor call:
The accepted solution allows me to get arguments passed into a constructor by defining a wrapper class that captures and exposes the arguments, but this leaves me with the problem of having n wrappers for n constructors.
Is there a way to have 1 function/wrapper/whatever that could work for any number of constructors?
I'll reiterate that I'm pursing this technique specifically to test Webpack plugin configuration, and I'd like to avoid having a separate wrapper for each plugin that I need to test.
Looking for something along the lines of
// ------------------------------------------------------------ a wrapper function?
const someWrapper = () => { /* ... */ }
const plugin1 = new Plugin({ a: 'value' })
const plugin2 = new Plugin2(arg1, arg2, { b: 'anotherValue '})
someWrapper(plugin1).args === [{ a: 'value' }]
someWrapper(plugin2).args === [arg1, arg2, { b: 'anotherValue' }]
// --------------------------------------------------------------- a wrapper class?
class Wrapper { /* ... */ }
const plugin1 = new Wrapper(Plugin, [{ a: 'value' }])
const plugin2 = new Wrapper(Plugin2, [arg1, arg2, { b: 'anotherValue '}])
plugin1.args === [{ a: 'value' }]
plugin2.args === [arg1, arg2, { b: 'anotherValue '}]
// problem with above is the wrapper is being passed to Webpack, not the underlying
// plugin; not sure yet if this would cause webpack to break or not actually
// execute the plugin as intended with a vanilla config
// ---------------------------------------------------------------- something else?

Yes, you can create generic wrapper which will add args property to instance of any passed constructor:
class Plugin {
constructor (arg1, arg2) {
this.arg1 = arg1
this.arg2 = arg2
}
}
function wrapper(initial) {
// Rewrite initial constructor with our function
return function decoratedContructor(...args) {
// Create instance of initial object
const decorated = new initial(...args)
// Add some additional properties, methods
decorated.args = [...args]
// Return instantiated and modified object
return decorated
}
}
const decoratedPlugin = wrapper(Plugin)
const plugin = new decoratedPlugin('argument', { 'argument2': 1 })
console.log(plugin.args)
FYI: it's not safe to add properties without some prefix. Consider adding __ or something like this to your property, because you can accidentally rewrite some inner object property.

I was able to get this working with a modification to #guest271314's suggestion, namely, you need to pass ...initArgs to super(), otherwise webpack will fail with a TypeError: Cannot read property '...' of undefined.
Also took #terales's point into account about making sure to prefix my additional properties.
const exposeConstructorArgs = (Plugin, ...args) => {
const ExposedPlugin = class extends Plugin {
constructor(...initArgs) {
super(...initArgs);
this.__initArgs__ = initArgs;
}
get __initArgs() {
return this.__initArgs__;
}
};
return Reflect.construct(ExposedPlugin, args);
};
// ...
const dllPlugin = exposeConstructorArgs(webpack.DllPlugin, {
name: '[name]',
path: path.join(buildDir, '[name].json'),
});
// ...
const pluginConfig = dllPlugin.__initArgs[0];
expect(pluginConfig.name).toEqual('[name]');

You can use a generic function where class expression is used within function body. Pass reference to the class or constructor and parameters expected to be arguments within the instance to the function call.
function Plugin() {}
function Plugin2() {}
function PluginWrapper(pluginRef, ...args) {
let MyPlugin = class extends pluginRef {
constructor() {
super();
this.args = [...arguments];
}
getArgs() {
return this.args;
}
}
return Reflect.construct(MyPlugin, args);
};
const anInstance = PluginWrapper(Plugin, {
a: 'path'
});
console.log(anInstance.getArgs(), anInstance instanceof Plugin);
const aSecondInstance = PluginWrapper(Plugin2, "arg1", "arg2", {
b: 'anotherPath'
});
console.log(aSecondInstance.getArgs(), aSecondInstance instanceof Plugin2);

Related

Test function assignment inside a class method in Jest

I have a class in which i use strategy pattern it looks something like this:
class Foo {
constructor() {
this.doStuff = function() { /* not used really */ }
}
create(config) {
this.type = config.type;
// assign other variables to this
this.doStuff = StuffStrategy(config.type);
this.doStuff();
}
}
StuffStrategy is a function that returns other functions which use this context differently based on type.
function StuffStrategy(type) {
switch(type) {
case A: return StrategyA;
case B: return StrategyB;
// ...
}
}
function StrategyA() {
if(this.someVarInFoo) {
return 'a thing'
} else
return 'a different thing' + this.otherVarInFoo
}
I assign particular Strategy function inside create method.
Then I would like to test the create method if it calls doStuff.
describe('how create method works', () => {
const instance = new Foo();
const spy = jest.spyOn(instance, 'doStuff');
instance.create(config);
expect(spy).toBeCalled();
});
But when I try to make spy before calling instance.create then it refers to default method assigned in constructor, which gets replaced inside create.
If i make spy after calling instance.create then it will not pick the call
.
I tried to add .bind when defining this.doStuff:
this.doStuff = StuffStrategy(config.type).bind(this);
but it does not work either.
Is there something wrong with my setup?
How can I make this test case work?
You have to spyOn the strategy methods of your Foo class. So for every config.type you check then which strategy method has been called.
export class Foo {
constructor(){
this.doStuff = null;
}
create(config){
this.type = config.type;
// assign other variables to this
this.doStuff = StuffStrategy(config.type);
this.doStuff();
}
strategyA(){...}
strategyB(){...}
StuffStrategy(configtype) {
switch (configtype) {
case "A": return this.strategyA;
case "B": return this.strategyB;
}
}
}
import { Foo } from 'anyPlaceFoo/foo';
describe('Strategy', () => {
it('should call strategy A', () => {
const foo = new Foo();
// here you can spy on every strategy method.
jest.spyOn(foo, 'strategyA');
jest.spyOn(foo, 'strategyB');
foo.create({ type: 'A' });
// check if the selected one has been called but not the others
expect(foo.strategyA).toHaveBeenCalled();
expect(foo.strategyB).not.toHaveBeenCalled();
})
})

How to avoid null/undefined check on JS/Typescript singleton with init function

I'm trying to find a pattern to avoid having to null/undefined check each literal props defined in an Object literal that is dynamically initiated only once at some point.
This object would serve as a singleton in a lifecycle of the application.
Is there a way to make this cleaner and avoid having to check and throw for each method that depends on the init to have happened? And also avoid ! operator on each prop once I know it is properly initiated?
I also want method2 to be able to be called before the init so that I can enqueue some things...
I guess I could switch my Object for a class and constructor (and define an instance as per singleton pattern) but the props for the constructor are needed from outside the class.
My method2 would also not be available until first constructed, unless I move it out of that class.
Any help appreciated.
interface StuffType {
x?: string;
y?: string;
z?: string;
fn?: () => void;
isInit: boolean;
}
const _stuff: StuffType = {
x: undefined,
y: undefined,
z: undefined,
fn: undefined,
isInit: false,
};
const isReady = () => _stuff.isInit;
const init = (x: string, y: string, z: string, fn: () => void) => {
if(_stuff.isInit) return;
_stuff.x = x;
_stuff.y = y;
_stuff.z = z;
_stuff.fn = fn;
};
const method1 = () => {
// depends on xyz, fn being defined
if (!_stuff.isInit) throw new Error('not init');
_stuff.fn!();
_stuff.x!;
_stuff.y!;
_stuff.z!;
};
// All the following methods would depend on init being done
// would require check on init/throw error/bang operator on props
// const method3 = () =>
// const method4 = () =>
// const method5 = () =>
// const method6 = () =>
const method2 = () => {
// does not depend on xyz, fn
};
export const Stuff = {
init,
isReady,
method1,
method2,
//etc...
};
Stuff.method2(); // fine
Stuff.method1(); // throws here
Stuff.init('x', 'y', 'z', () => console.log('init'));
Stuff.method1(); // does not throw
To do this more concisely, I'd put the data to be initialized in a single separate property - that way, you just need to check if that one property exists. This also makes the isInit property superfluous.
interface StuffType {
data?: {
x: string;
y: string;
z: string;
}
fn?: () => void;
}
const _stuff: StuffType = {
fn: undefined,
};
const init = (x: string, y: string, z: string, fn: () => void) => {
_stuff.data ??= { x, y, z };
};
const method1 = () => {
const { data } = _stuff;
if (!data) throw new Error('not init');
_stuff.fn!();
data.x;
data.y;
data.z;
};
I also want method2 to be able to be called before the init so that I can enqueue some things
Nothing in method2's signature shows that it depends on anything else, so no real modification there is necessary; you're free to populate
const method2 = () => {
// does not depend on xyz, fn
};
with whatever you want. (Perhaps you could have a queue variable local to the module, const queue = [], that gets pushed to if method2 is called)
I would avoid a partial type where every property is optional by itself. You can make them dependent on each other by using a discriminated union:
interface Stuff {
isInit: true;
x: string;
y: string;
z: string;
fn: () => void;
}
type MaybeStuff = Stuff | { isInit: false };
const _stuff: MaybeStuff = {
isInit: false,
};
export const method1 = () => {
if (!_stuff.isInit) throw new Error('not initialised');
// depends on x, y, z, fn being initialised
_stuff.fn();
_stuff.x;
_stuff.y;
_stuff.z;
};
However, an even simpler solution is to make _stuff a mutable variable that is initially undefined:
interface Stuff {
x: string;
y: string;
z: string;
fn: () => void;
}
let _stuff: Stuff | undefined; /*
^^^ */
export const isReady = () => _stuff !== undefined;
export function init(x: string, y: string, z: string, fn: () => void) {
if (_stuff) return; // throw new Error('already initialised');
stuff = {x, y, z, fn};
};
export function method1() {
if (!_stuff) throw new Error('not initialised');
// depends on _stuff being initialised
_stuff.fn();
_stuff.x;
_stuff.y;
_stuff.z;
}
export function method2() {
// does not depend on xyz, fn
}
If you do not like repeating the if (!_stuff) … line in all the methods that need the stuff, make a helper function for them:
function get(): Stuff {
if (!_stuff) throw new Error('not initialised');
return _stuff;
}
export function method1() {
const stuff = get(); // depends on _stuff being initialised
stuff.fn();
stuff.x;
stuff.y;
stuff.z;
}
export function method2() {
// does not depend on xyz, fn
}
export function method3() {
const stuff = get(); // depends on _stuff being initialised
…
}
[I have] an Object literal that is dynamically initiated only once at some point. This object would serve as a singleton in a lifecycle of the application. Is there a way to make this cleaner?
Yes, that doesn't sound a lot like a singleton, or at least like one with all of the common known singleton problems. A proper singleton should already be initialised at the start of the application, not at some point in its lifecycle, and it should not need parameters passed to it (exactly once). If it is parameterised, sooner or later you will need (multiple) different instances with different configuration.
So rather, refactor your module into a class, remove the mutable _stuff static (module-scoped) variable and the init method. Construct your object where the parameter values are available, and have that module be responsible for providing the instance to the rest of the application (via a global variable, the singleton pattern, dependency injection, etc.).
Also the method2 should either be a static method of the class if it is stateless, or it should be a method of the module keeping the initialisation state (if it does what you describe, enqueuing things until the initialisation).

How to read instance decorators in typescript?

I can create custom decorator using reflect-metadata and it work fine.
Problem is, that I don`t know how to get all instance decorators.
import 'reflect-metadata';
console.clear();
function readTypes() {
const decorator: MethodDecorator = (target, propertyKey, description) => {
const args = Reflect.getMetadata(
'design:paramtypes',
target,
propertyKey
).map(c => c.name);
const ret = Reflect.getMetadata('design:returntype', target, propertyKey);
console.log(`Arguments type: ${args.join(', ')}.`);
console.log(`Return type: ${ret.name}.`);
};
return decorator;
}
class Foo {}
class Bar {
#readTypes()
public fn(a: number, b: string, c: Foo): boolean {
return true;
}
}
const barInstance = new Bar();
I would like to get all functions with decorator #readTypes from barInstance. How can I do it?
See working example:
https://stackblitz.com/edit/decorators-metadata-example-nakg4c
First of all, you aren't writing any metadata, only reading it. If you want to lookup which properties were decorated, then you have to write metadata to those properties.
For simplicity's sake, let's simplify the decorator to:
// It's a best practice to use symbol as metadata keys.
const isReadTypesProp = Symbol('isReadTypesProp')
function readTypes() {
const decorator: MethodDecorator = (target, propertyKey, description) => {
Reflect.defineMetadata(isReadTypesProp, true, target, propertyKey);
};
return decorator;
}
Now when the decorator is used, it's executed when the class is parsed, not when instances are created. This means that target in the decorator function is actually the prototype of the class constructor function.
In other words target is the same object as
Bar.prototype or barInstance.constructor.prototype or barInstance.__proto__.
Knowing that we can loop over all the property names in the prototype object and look up that metadata we set earlier:
function getReadTypesPropsFromInstance(object: {
constructor: {
prototype: unknown;
};
}) {
const target = object.constructor.prototype;
const keys = Object.getOwnPropertyNames(target);
return keys.filter(key => Reflect.getMetadata(isReadTypesProp, target, key));
}
Which now returns the names of the decorated properties:
const barInstance = new Bar();
console.log(getReadTypesPropsFromInstance(barInstance)); // ["fn"]
Working example

How to augment instances of a mocked constructor in Jest

I'd like to augment, but not completely replace, instances of a mocked constructor in a Jest unit test.
I want to add a few values to the instance, but keep the auto-mocked goodness of Jest.
For example:
A.js
module.exports = class A {
constructor(value) {
this.value = value;
}
getValue() {
return this.value;
}
}
To get some auto-mock awesomeness:
jest.mock('./A');
With the automock, instances have a mocked .getValue() method, but they do not have the .value property.
A documented way of mocking constructors is:
// SomeClass.js
module.exports = class SomeClass {
m(a, b) {}
}
// OtherModule.test.js
jest.mock('./SomeClass'); // this happens automatically with automocking
const SomeClass = require('./SomeClass')
const mMock = jest.fn()
SomeClass.mockImplementation(() => {
return {
m: mMock
}
})
const some = new SomeClass()
some.m('a', 'b')
console.log('Calls to m: ', mMock.mock.calls)
Using that approach for A:
jest.mock('./A');
const A = require('./A');
A.mockImplementation((value) => {
return { value };
});
it('does stuff', () => {
const a = new A();
console.log(a); // -> A { value: 'value; }
});
The nice thing about that is you can do whatever you want to the returned value, like initialize .value.
The downsides are:
You don't get any automocking for free, e.g. I'd need to add .getValue() myself to the instance
You need to have a different jest.fn() mock function for each instance created, e.g. if I create two instances of A, each instance needs its own jest.fn() mock functions for the .getValue() method
SomeClass.mock.instances is not populated with the returned value (GitHub ticket)
One thing that didn't work (I was hoping that maybe Jest did some magic):
A.mockImplementation((value) => {
const rv = Object.create(A.prototype); // <- these are mocked methods
rv.value = value;
return rv;
});
Unfortunately, all instances share the same methods (as one would expect, but it was worth a shot).
My next step is to generate the mock, myself, via inspecting the prototype (I guess), but I wanted to see if there is an established approach.
Thanks in advance.
Turns out this is fixed (as of jest 24.1.0) and the code in the question works, as expected.
To recap, given class A:
A.js
module.exports = class A {
constructor(value) {
this.value = value;
}
setValue(value) {
this.value = value;
}
}
This test will now pass:
A.test.js
jest.mock('./A');
const A = require('./A');
A.mockImplementation((value) => {
const rv = Object.create(A.prototype); // <- these are mocked methods
rv.value = value;
return rv;
});
it('does stuff', () => {
const a = new A('some-value');
expect(A.mock.instances.length).toBe(1);
expect(a instanceof A).toBe(true);
expect(a).toEqual({ value: 'some-value' });
a.setValue('another-value');
expect(a.setValue.mock.calls.length).toBe(1);
expect(a.setValue.mock.calls[0]).toEqual(['another-value']);
});
The following worked for me:
A.mockImplementation(value => {
const rv = {value: value};
Object.setPrototypeOf(rv, A.prototype);
return rv
})

Named default ES6 parameter without destructuration

I'm trying to default options in ES7 using babel. Here is what I can do:
class Foo {
constructor({key='value', foo='bar', answer=42}) {
this.key = key;
this.foo = foo;
this.number = number;
}
}
This might work for this example, but I would like to know how can I assign for very large config objects; here is an example of what I wanna do:
class Foo {
constructor(opts = {key='value', foo='bar', answer=42}) {
this.opts = opts;
}
}
However this does not compile. I tried to do it like this:
class Foo {
constructor(opts = {key:'value', foo:'bar', answer:42}) {
this.opts = opts;
}
}
But then it replaces the whole object, like this:
let foo = new Foo({key: 'foobar'});
console.log(foo.opts);
// {key: 'foobar'} is what is displayed
// When I want {key: 'foobar', foo:'bar', answer:42}
I don't think you can do this with ES6 optional parameters (object as a parameter with optional keys), because when you call the constructor, it's a new object with a new reference. That's because it's being replaced.
But, as a suggestion, if you want to handle a large options object, one common approach is store somewhere a default options Object and merge the object with the one passed when you instantiate it.
Something like that:
class Foo {
constructor(opts) {
this.opts = Object.assign({}, Foo.defaultOptions, opts)
console.log(this.opts)
}
}
Foo.defaultOptions = {
key: 'value',
foo: 'bar',
answer: 42
}
let foo = new Foo({key: 'another value'})
//{ key: 'another value', foo: 'bar', answer: 42 }
You can merge with Object.assign (be aware that it does not perform deep merging - nested objects are replaced).
Or, if you want to declare your default options Object as a class variable (not at the end, after class declaration, or inside constructor), as you're using babel, you can use this plugin and do this:
class Foo {
defaultOptions = {
key: 'value',
foo: 'bar',
answer: 42
}
constructor(opts) {
this.opts = Object.assign({}, this.defaultOptions, opts)
console.log(this.opts)
}
}
It's more readable.
It is
class Foo {
constructor({key='value', foo='bar', answer=42} = {}) {
...
}
}
It is ES6 destructuring feature, not specific to ECMAScript 7 (ECMAScript Next) proposals.
Without destructuring it is usually done with object cloning/merging, Object.assign comes to help:
class Foo {
constructor(opts = {}) {
this.opts = Object.assign({
key: 'value',
foo: 'bar',
answer: 42
}, opts);
}
}

Categories

Resources