Excuse me for creating a new question, I was not able to find a question addressing this matter.
I am having difficulties testing my dependency injection using mocha and experimental es6+ decorators transpiled using babel. The class property decorator is being called before it should've been.
injection.test.js (mocha test, using --require babel-register
)
import * as DependencyInjection from '../build/decorators/DependencyInjection';
@DependencyInjection.Injectable(service => service.injected = true)
class SampleService {
property = 'default';
constructor(property, ...data) {
this.property = property || this.property;
}
}
class Dependant {
/** @type {SampleService} */
@DependencyInjection.Inject(SampleService)
sampleService;
}
describe('dependency injection', () => {
describe('is decoratored as injectable', () => {
it('should be injectable', done => done(SampleService.injectable ? void 0 : new Error('Injectable is not set')));
it('should contain a predicate', done => done(SampleService.predicate ? void 0 : new Error('Predicate is not set')));
});
describe('inject injectable', () => {
it ('should inject the injectable provider', done => {
const dependant = new Dependant();
done(!!dependant.sampleService ? void 0 : new Error('Injectable provider was not injected'));
})
});
});
When running the test, the decorated class is transformed as inteded. However, the sampleService
property of the instance of Dependant
, created in the second test, is undefined.
The decorator in question should be called/invoked once an instance of the class is being created, but the decorator is called when the class is defined and the property is decorated. The expected behaviour is maintained when using TypeScript
.
Below I've listed the (simplified) decorators and my babel configuration.
.babelrc
{
"presets": [
"env",
"stage-0",
"es2017"
],
"plugins": [
"syntax-decorators",
"transform-decorators-legacy",
["transform-runtime", {
"polyfill": false,
"regenerator": true
}]
]
}
exported decorator Inject (targeting class property
):
exports.Inject = (typeFunction, ...data) => {
return function (target, propertyName) {
try {
const injected = target[propertyName] = new typeFunction(data);
if ('predicate' in typeFunction && typeof typeFunction.predicate === 'function') {
typeFunction.predicate(injected);
}
}
catch (err) {
throw new Error(err.message || err);
}
};
};
exported decorator Injectable (targeting class
):
exports.Injectable = (predicate) => {
return function (target) {
const provider = target;
provider.injectable = true;
if (predicate && typeof predicate === 'function') {
provider.predicate = predicate;
}
};
};
I still haven't found the leading cause why it creates a new instance of the class when decorating the class property. However, I have found a working solution to my problem. Within mocha, using --require babel-register
and the legacy decorator plugin, the class property decorator makes use of the PropertyDescriptor
. Changing the simplified Inject
decorator to the following has solved my problem of not instantiating my decorated class property.
exports.Inject = (typeFunction, ...data) => {
return function (target, propertyName, descriptor) {
let value = null;
try {
const injected = value = target[propertyName] = new typeFunction(...data);
if ('predicate' in typeFunction && typeof typeFunction.predicate === 'function') {
typeFunction.predicate(injected);
}
}
catch (err) {
throw new Error(err.message || err);
}
if (descriptor) {
delete descriptor.initializer;
delete descriptor.writable;
descriptor.value = value;
}
};
};
Deleting the writable
property is neccesary.
The following tests...
const assert = require('assert');
const chai = require('chai');
import * as DependencyInjection from '../build/decorators/DependencyInjection';
@DependencyInjection.Injectable(service => service.injected = true)
class SampleService {
property = 'default';
constructor(property, ...data) {
this.property = property || this.property;
}
}
class Dependant {
/** @type {SampleService} */
@DependencyInjection.Inject(SampleService)
sampleService;
}
class Dependant2 {
/** @type {SampleService} */
@DependencyInjection.Inject(SampleService, 'overloaded')
sampleService;
}
describe('dependency injection', () => {
describe('is decoratored as injectable', () => {
it('should be injectable', done => done(SampleService.injectable ? void 0 : new Error('Injectable is not set')));
it('should contain a predicate', done => done(SampleService.predicate ? void 0 : new Error('Predicate is not set')));
});
describe('inject at decorated class property', () => {
it('should inject the injectable provider at the decorated property', () => {
const dependant = new Dependant();
chai.expect(dependant.sampleService).to.be.instanceof(SampleService, 'Injectable provider was not injected');
chai.assert.isTrue(dependant.sampleService.injected, 'The predicate of the injectable service was not set');
chai.expect(dependant.sampleService.property).to.equal('default', 'The initial value of \'property\' was not \'default\'');
});
it('should inject the injectable provider with overloaded constructor arguments at the decorated property', () => {
const dependant = new Dependant2();
chai.expect(dependant.sampleService).to.be.instanceOf(SampleService, 'Injectable provider was not injected');
chai.assert.isTrue(dependant.sampleService.injected, 'The predicate of the injectable service was not set');
chai.expect(dependant.sampleService.property).to.equal('overloaded', 'The value of \'property\' was not overloaded');
});
});
describe('inject at manual target and property', () => {
it('should inject the injectable provider at the targeting value', () => {
const inject = DependencyInjection.Inject(SampleService);
const target = {};
let err = null;
try {
inject(target, 'service');
} catch (e) {
err = e;
}
chai.assert.isNull(err, 'Expected injection to pass');
chai.expect(target.service).to.be.instanceOf(SampleService, 'Injectable provider was not injected');
chai.assert.isTrue(target.service.injected, 'The predicate of the injectable service was not set');
chai.expect(target.service.property).to.equal('default', 'The initial value of \'property\' was not \'default\'');
});
it('should inject the injectable provider with overloaded constructor arguments at the targeting value', () => {
const inject = DependencyInjection.Inject(SampleService, 'overloaded');
const target = {};
let err = null;
try {
inject(target, 'service');
} catch (e) {
err = e;
}
chai.assert.isNull(err, 'Expected injection to pass');
chai.expect(target.service).to.be.instanceOf(SampleService, 'Injectable provider was not injected');
chai.assert.isTrue(target.service.injected, 'The predicate of the injectable service was not set');
chai.expect(target.service.property).to.equal('overloaded', 'The value of \'property\' was not overloaded');
});
it('should not inject anything at the targeting value', () => {
const inject = DependencyInjection.Inject();
const target = {};
let err = null;
try {
inject(target, 'service');
} catch (e) {
err = e;
}
chai.expect(err).to.be.instanceof(Error);
chai.assert.notExists(target.service);
});
});
});
...output the following result:
dependency injection
is decoratored as injectable
√ should be injectable
√ should contain a predicate
inject at decorated class property
√ should inject the injectable provider at the decorated property
√ should inject the injectable provider with overloaded constructor arguments at the decorated property
inject at manual target and property
√ should inject the injectable provider at the targeting value
√ should inject the injectable provider with overloaded constructor arguments at the targeting value
√ should not inject anything at the targeting value
7 passing (29ms)