Framing is supposed to be easy, but sometimes there are subtle issues.
Dependency Injection is a form of Inversion of Control where object dependencies are instantiated and passed to dependent objects on behalf of the developer by a framework.
Classically, programs make the calls into libraries. This requires knowledge of how to instantiate or reference objects from the library in the calling code. With Inversion of Control the framework makes the calls to instantiate or reference objects. The inversion is that the framework is making the calls on behalf of the program. This benefits the application by providing a separation of concerns regarding business logic and how to reference and call libraries.
Dependency Injection decouples business logic from object instantiation logic. This keeps the code cleaner and easier to maintain as it's easier to replace and reuse objects that are injected. Further, this simplifies unit testing in that it is much easier to test the objects in isolation.
Sometimes the components you think are loading are not the components that are actually loading. Perhaps there are more components than what you expected or other components are missing.
The --debug argument passed to your application will provide you information like where Framing is searching for components and components have been loaded.
node app.js --debug
Framing will block until a component finishes initialization. There is no timeout for a blocking component. If your application has not finished initializing, then are likely one or more components not returning by calling the callback for the initialize function or resolving the returned Promise object.
module.exports.initialize = (imports, callback) => {
callback({ /* component interface */ });
// or
return new Promise((resolve, reject) => {
resolve({ /* component interface */ });
});
}
Framing will order component initialization by:
- By dependencies, i.e., whether a components dependencies have all initialized.
- The order of the directories that Framing searched in.
- The order that the system returned the component folders for each directory.
The first component to be loaded by Framing, will be the component that is used. Framing will keep the components that it is initializing unique by component name. The order at which this uniqueness is applied is the order of precedence for the components.
In many cases, the package name and the Framing name will be the same value, but when they diverge, it would generally be best to use the Framing name rather than the package name. The package name must always be unique whereas the framing name does not have to be. This allows multiple components to share the same interface and be interchangeable.
If your component has thrown an exception in a synchronous manner, Framing will catch the error and report it accordingly. However, if the error occurs asynchronously, then the error must be reported to Framing through either the callback for the initialize function or by rejecting the returned Promise object.
module.exports.initialize = () => {
throw new Error('fail');
// or
return new Promise((resolve, reject) => {
reject(new Error('fail'));
});
};
Components are actually very easy to test because they have been written so that their dependencies are injected at runtime. This makes it very easy to create simple mocks or stubs for the component to use during the test. Because the component initialization is asynchronous, the test will have to be treated as asynchronous.
// logger stub
const loggerStub = {
log: () => {}
};
describe('My awesome component', () => {
describe('When tested', () => {
it('does what it ought to.', (done) => {
require('./my-component').initialize({
logger: loggerStub
}).then(component => {
expect(component.doSomething()).to.be.true;
done();
});
});
});
});
Sometimes, it makes more sense to test the private functions of the component. Because the public interface is passed through the initialization and not through the module.exports, the module.exports can be used to expose various private functions for unit testing.
/// my-component.js
function somePrivateFunction() {
return 'test';
}
module.exports.somePrivateFunction = somePrivateFunction;
module.exports.initialize = () => {
return Promise.resolve({
somePublicFunction: () => somePrivateFunction()
});
};
/// my-component.test.js
describe('My awesome component', () => {
describe('When tested', () => {
it('does what it ought to.', () => {
expect(require('./my-component').somePrivateFunction()).to.equal('test');
});
});
});