decorator-dependency-injection 1.0.2 → 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/release.yml +1 -1
- package/README.md +68 -22
- package/index.js +129 -63
- package/package.json +1 -1
- package/test/injection.test.js +164 -7
- package/test/injectionLazy.test.js +249 -0
- package/test/mock.test.js +224 -6
- package/test/proxy.test.js +59 -2
package/README.md
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
# Decorator Dependency Injection
|
|
2
|
-
[](http://badge.fury.io/js/decorator-dependency-injection)
|
|
3
|
-
[](https://github.com/mallocator/decorator-dependency-injection/actions/workflows/node.js.yml)
|
|
4
2
|
|
|
3
|
+
[](http://badge.fury.io/js/decorator-dependency-injection)
|
|
4
|
+
[](https://github.com/mallocator/decorator-dependency-injection/actions/workflows/release.yml)
|
|
5
5
|
|
|
6
6
|
## Description
|
|
7
7
|
|
|
8
|
-
With [TC39](https://github.com/tc39/proposal-decorators) reaching stage 3
|
|
8
|
+
With the [TC39 proposal-decorators](https://github.com/tc39/proposal-decorators) reaching stage 3, it's time to start
|
|
9
|
+
thinking about how we can use them in our projects. One of the most common patterns in JavaScript is dependency
|
|
10
|
+
injection. This pattern is used to make our code more testable and maintainable. This library provides simple decorators
|
|
11
|
+
to help you inject dependencies into your classes and mock them for testing.
|
|
9
12
|
|
|
10
13
|
## Installation
|
|
11
14
|
|
|
@@ -13,21 +16,26 @@ With [TC39](https://github.com/tc39/proposal-decorators) reaching stage 3 on the
|
|
|
13
16
|
npm install decorator-dependency-injection
|
|
14
17
|
```
|
|
15
18
|
|
|
16
|
-
Until we reach stage 4, you will need to enable the decorators proposal in your project. You can do this by adding the
|
|
19
|
+
Until we reach stage 4, you will need to enable the decorators proposal in your project. You can do this by adding the
|
|
20
|
+
following babel transpiler options to your `.babelrc` file.
|
|
17
21
|
|
|
18
22
|
```json
|
|
19
23
|
{
|
|
20
|
-
"plugins": [
|
|
24
|
+
"plugins": [
|
|
25
|
+
"@babel/plugin-proposal-decorators"
|
|
26
|
+
]
|
|
21
27
|
}
|
|
22
28
|
```
|
|
23
29
|
|
|
24
|
-
To run your project with decorators enabled you will need to use the babel transpiler. You can do this by running the
|
|
30
|
+
To run your project with decorators enabled, you will need to use the babel transpiler. You can do this by running the
|
|
31
|
+
following command in your project root.
|
|
25
32
|
|
|
26
33
|
```bash
|
|
27
34
|
npx babel-node index.js
|
|
28
35
|
```
|
|
29
36
|
|
|
30
|
-
Finally, for running tests with decorators enabled you will need to use the babel-jest package. You can do this by
|
|
37
|
+
Finally, for running tests with decorators enabled, you will need to use the babel-jest package. You can do this by
|
|
38
|
+
adding the following configuration to your `package.json` file.
|
|
31
39
|
|
|
32
40
|
```json
|
|
33
41
|
{
|
|
@@ -43,20 +51,21 @@ Other testing frameworks may require a different configuration.
|
|
|
43
51
|
|
|
44
52
|
For a full example of how to set up a project with decorators, see this project's ```package.json``` file.
|
|
45
53
|
|
|
46
|
-
|
|
47
54
|
## Usage
|
|
48
55
|
|
|
49
|
-
There are
|
|
56
|
+
There are two ways of specifying injectable dependencies: ```@Singleton``` and ```@Factory```:
|
|
50
57
|
|
|
51
58
|
### Singleton
|
|
52
59
|
|
|
53
|
-
The ```@Singleton``` decorator is used to inject a single instance of a dependency into a class. This is useful when you
|
|
60
|
+
The ```@Singleton``` decorator is used to inject a single instance of a dependency into a class. This is useful when you
|
|
61
|
+
want to share the same instance of a class across multiple classes.
|
|
54
62
|
|
|
55
63
|
```javascript
|
|
56
|
-
import {
|
|
64
|
+
import {Singleton} from 'decorator-dependency-injection';
|
|
57
65
|
|
|
58
66
|
@Singleton
|
|
59
|
-
class Dependency {
|
|
67
|
+
class Dependency {
|
|
68
|
+
}
|
|
60
69
|
|
|
61
70
|
class Consumer {
|
|
62
71
|
@Inject(Dependency) dependency // creates an instance only once
|
|
@@ -65,25 +74,48 @@ class Consumer {
|
|
|
65
74
|
|
|
66
75
|
### Factory
|
|
67
76
|
|
|
68
|
-
The ```@Factory``` decorator is used to inject a new instance of a dependency into a class each time it is requested.
|
|
77
|
+
The ```@Factory``` decorator is used to inject a new instance of a dependency into a class each time it is requested.
|
|
78
|
+
This is useful when you want to create a new instance of a class each time it is injected.
|
|
69
79
|
|
|
70
80
|
```javascript
|
|
71
|
-
import {
|
|
81
|
+
import {Factory} from 'decorator-dependency-injection';
|
|
72
82
|
|
|
73
83
|
@Factory
|
|
74
|
-
class Dependency {
|
|
84
|
+
class Dependency {
|
|
85
|
+
}
|
|
75
86
|
|
|
76
87
|
class Consumer {
|
|
77
88
|
@Inject(Dependency) dependency // creates a new instance each time a new Consumer is created
|
|
78
89
|
}
|
|
79
90
|
```
|
|
80
91
|
|
|
92
|
+
### LazyInject
|
|
93
|
+
|
|
94
|
+
```@Inject``` annotated properties are evaluated during instance initialization. That means that all properties should
|
|
95
|
+
be accessible in the constructor. That also means that we're creating an instance no matter if you access the property
|
|
96
|
+
or not. If you want to only create an instance when you access the property, you can use the ```@LazyInject```
|
|
97
|
+
decorator. This will create the instance only when the property is accessed for the first time. Note that this also
|
|
98
|
+
works from the constructor, same as the regular ```@Inject```.
|
|
99
|
+
|
|
100
|
+
```javascript
|
|
101
|
+
import {LazyInject} from 'decorator-dependency-injection';
|
|
102
|
+
|
|
103
|
+
@Singleton
|
|
104
|
+
class Dependency {
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
class Consumer {
|
|
108
|
+
@LazyInject(Dependency) dependency // creates an instance only when the property is accessed
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
81
112
|
## Passing parameters to a dependency
|
|
82
113
|
|
|
83
|
-
You can pass parameters to a dependency by using the ```@Inject``` decorator with a function that returns the
|
|
114
|
+
You can pass parameters to a dependency by using the ```@Inject``` decorator with a function that returns the
|
|
115
|
+
dependency.
|
|
84
116
|
|
|
85
117
|
```javascript
|
|
86
|
-
import {
|
|
118
|
+
import {Factory, Inject} from 'decorator-dependency-injection';
|
|
87
119
|
|
|
88
120
|
@Factory
|
|
89
121
|
class Dependency {
|
|
@@ -98,14 +130,15 @@ class Consumer {
|
|
|
98
130
|
}
|
|
99
131
|
```
|
|
100
132
|
|
|
101
|
-
While this is most useful for Factory dependencies, it can also be used with Singleton dependencies. However, parameters
|
|
133
|
+
While this is most useful for Factory dependencies, it can also be used with Singleton dependencies. However, parameters
|
|
134
|
+
will only be passed to the dependency the first time it is created.
|
|
102
135
|
|
|
103
136
|
## Mocking dependencies for testing
|
|
104
137
|
|
|
105
138
|
You can mock dependencies by using the ```@Mock``` decorator with a function that returns the mock dependency.
|
|
106
139
|
|
|
107
140
|
```javascript
|
|
108
|
-
import {
|
|
141
|
+
import {Factory, Inject, Mock} from 'decorator-dependency-injection'
|
|
109
142
|
|
|
110
143
|
@Factory
|
|
111
144
|
class Dependency {
|
|
@@ -138,10 +171,22 @@ resetMock(Dependency)
|
|
|
138
171
|
const consumer = new Consumer() // prints 'real'
|
|
139
172
|
```
|
|
140
173
|
|
|
141
|
-
|
|
174
|
+
### Resetting Mocks
|
|
175
|
+
|
|
176
|
+
The `resetMock` utility function allows you to remove any active mock for a dependency and restore the original
|
|
177
|
+
implementation. This is useful for cleaning up after tests or switching between real and mock dependencies.
|
|
178
|
+
|
|
179
|
+
```javascript
|
|
180
|
+
import {resetMock} from 'decorator-dependency-injection';
|
|
181
|
+
|
|
182
|
+
resetMock(Dependency); // Restores the original Dependency implementation
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
You can also use the ```@Mock``` decorator as a proxy instead of a full mock. Any method calls not implemented in the
|
|
186
|
+
mock will be passed to the real dependency.
|
|
142
187
|
|
|
143
188
|
```javascript
|
|
144
|
-
import {
|
|
189
|
+
import {Factory, Inject, Mock} from 'decorator-dependency-injection'
|
|
145
190
|
|
|
146
191
|
@Factory
|
|
147
192
|
class Dependency {
|
|
@@ -192,4 +237,5 @@ npm test
|
|
|
192
237
|
|
|
193
238
|
- 1.0.0 - Initial release
|
|
194
239
|
- 1.0.1 - Automated release with GitHub Actions
|
|
195
|
-
- 1.0.2 - Added proxy option to @Mock decorator
|
|
240
|
+
- 1.0.2 - Added proxy option to @Mock decorator
|
|
241
|
+
- 1.0.3 - Added @LazyInject decorator
|
package/index.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @typedef {Object} InstanceContext
|
|
3
|
-
* @property {
|
|
4
|
-
* @property {
|
|
5
|
-
* @property {
|
|
6
|
-
* @property {Object} [instance] The instance if
|
|
7
|
-
* @property {Object} [originalInstance] The original instance if
|
|
8
|
-
* @property {boolean} [proxy=false] If true, the mock
|
|
3
|
+
* @property {'singleton'|'factory'} type - The type of the instance.
|
|
4
|
+
* @property {Function} clazz - The class constructor for the instance.
|
|
5
|
+
* @property {Function} [originalClazz] - The original class if this is a mock.
|
|
6
|
+
* @property {Object} [instance] - The singleton instance, if created.
|
|
7
|
+
* @property {Object} [originalInstance] - The original instance if this is a mock.
|
|
8
|
+
* @property {boolean} [proxy=false] - If true, the mock will proxy to the original class for undefined methods/properties.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
/** @type {Map<string|Class, InstanceContext>} */
|
|
@@ -16,22 +16,26 @@ const instances = new Map()
|
|
|
16
16
|
* Singleton instances only ever have one instance created via the @Inject decorator.
|
|
17
17
|
*
|
|
18
18
|
* @param {string} [name] The name of the singleton. If not provided, the class will be used as the key.
|
|
19
|
-
* @return {(function(
|
|
19
|
+
* @return {(function(Function, {kind: string}): void)}
|
|
20
20
|
* @example @Singleton() class MySingleton {}
|
|
21
21
|
* @example @Singleton('customName') class MySingleton {}
|
|
22
22
|
* @throws {Error} If the injection target is not a class
|
|
23
|
-
* @throws {Error} If a singleton with the same name is already defined
|
|
24
|
-
* @throws {Error} If
|
|
23
|
+
* @throws {Error} If a singleton or factory with the same name is already defined
|
|
24
|
+
* @throws {Error} If the target is not a class constructor
|
|
25
25
|
*/
|
|
26
26
|
export function Singleton(name) {
|
|
27
27
|
return function (clazz, context) {
|
|
28
|
-
if (context.kind !==
|
|
28
|
+
if (context.kind !== 'class') {
|
|
29
29
|
throw new Error('Invalid injection target')
|
|
30
30
|
}
|
|
31
|
-
if (
|
|
32
|
-
throw new Error('
|
|
31
|
+
if (typeof clazz !== 'function' || !clazz.prototype) {
|
|
32
|
+
throw new Error('Target must be a class constructor')
|
|
33
33
|
}
|
|
34
|
-
|
|
34
|
+
const key = name ?? clazz
|
|
35
|
+
if (instances.has(key)) {
|
|
36
|
+
throw new Error('A different class is already registered under this name. This may be possibly a circular dependency. Try using @InjectLazy')
|
|
37
|
+
}
|
|
38
|
+
instances.set(key, {clazz, type: 'singleton'})
|
|
35
39
|
}
|
|
36
40
|
}
|
|
37
41
|
|
|
@@ -40,22 +44,26 @@ export function Singleton(name) {
|
|
|
40
44
|
* Factory instances are created via the @Inject decorator. Each call to the factory will create a new instance.
|
|
41
45
|
*
|
|
42
46
|
* @param {string} [name] The name of the factory. If not provided, the class will be used as the key.
|
|
43
|
-
* @return {(function(
|
|
47
|
+
* @return {(function(Function, {kind: string}): void)}
|
|
44
48
|
* @example @Factory() class MyFactory {}
|
|
45
49
|
* @example @Factory('customName') class MyFactory {}
|
|
46
50
|
* @throws {Error} If the injection target is not a class
|
|
47
|
-
* @throws {Error} If a factory with the same name is already defined
|
|
48
|
-
* @throws {Error} If
|
|
51
|
+
* @throws {Error} If a factory or singleton with the same name is already defined
|
|
52
|
+
* @throws {Error} If the target is not a class constructor
|
|
49
53
|
*/
|
|
50
54
|
export function Factory(name) {
|
|
51
55
|
return function (clazz, context) {
|
|
52
|
-
if (context.kind !==
|
|
56
|
+
if (context.kind !== 'class') {
|
|
53
57
|
throw new Error('Invalid injection target')
|
|
54
58
|
}
|
|
55
|
-
if (
|
|
56
|
-
throw new Error('
|
|
59
|
+
if (typeof clazz !== 'function' || !clazz.prototype) {
|
|
60
|
+
throw new Error('Target must be a class constructor')
|
|
61
|
+
}
|
|
62
|
+
const key = name ?? clazz
|
|
63
|
+
if (instances.has(key)) {
|
|
64
|
+
throw new Error('A different class is already registered under this name, This may be possibly a circular dependency. Try using @InjectLazy')
|
|
57
65
|
}
|
|
58
|
-
instances.set(
|
|
66
|
+
instances.set(key, {clazz, type: 'factory'})
|
|
59
67
|
}
|
|
60
68
|
}
|
|
61
69
|
|
|
@@ -65,52 +73,67 @@ export function Factory(name) {
|
|
|
65
73
|
*
|
|
66
74
|
* @param {string|Class} clazzOrName The singleton or factory class or name
|
|
67
75
|
* @param {*} params Parameters to pass to the constructor. Recommended to use only with factories.
|
|
68
|
-
* @return {(function(
|
|
76
|
+
* @return {(function(*, {kind: string, name: string}): void)}
|
|
69
77
|
* @example @Inject(MySingleton) mySingleton
|
|
70
78
|
* @example @Inject("myCustomName") myFactory
|
|
71
79
|
* @throws {Error} If the injection target is not a field
|
|
72
80
|
* @throws {Error} If the injected field is assigned a value
|
|
73
81
|
*/
|
|
74
82
|
export function Inject(clazzOrName, ...params) {
|
|
75
|
-
return function(initialValue, context) {
|
|
76
|
-
if (context.kind
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
if (instanceContext.instance) {
|
|
84
|
-
return instanceContext.instance
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
const instance = new instanceContext.clazz(...params)
|
|
88
|
-
|
|
89
|
-
if (instanceContext.type === 'singleton') {
|
|
90
|
-
if (instanceContext.originalClazz && instanceContext.proxy) {
|
|
91
|
-
instanceContext.instance = getProxy(instance, new instanceContext.originalClazz(...params))
|
|
92
|
-
} else {
|
|
93
|
-
instanceContext.instance = instance
|
|
94
|
-
}
|
|
95
|
-
return instanceContext.instance
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
if (instanceContext.type === 'factory') {
|
|
99
|
-
if (instanceContext.originalClazz && instanceContext.proxy) {
|
|
100
|
-
return getProxy(instance, new instanceContext.originalClazz(...params))
|
|
101
|
-
} else {
|
|
102
|
-
return instance
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
throw new Error('Unexpected injection type')
|
|
83
|
+
return function (initialValue, context) {
|
|
84
|
+
if (context.kind !== 'field') {
|
|
85
|
+
throw new Error('Invalid injection target')
|
|
86
|
+
}
|
|
87
|
+
return function (initialValue) {
|
|
88
|
+
if (initialValue) {
|
|
89
|
+
throw new Error('Cannot assign value to injected field')
|
|
107
90
|
}
|
|
108
|
-
|
|
91
|
+
const instanceContext = getContext(clazzOrName)
|
|
92
|
+
return getInjectedInstance(instanceContext, params)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Inject a singleton or factory instance lazily into a class field. You can also provide parameters to the constructor.
|
|
99
|
+
* If the instance is a singleton, it will only be created once with the first set of parameters it encounters.
|
|
100
|
+
* @param {string|Class} clazzOrName The singleton or factory class or name
|
|
101
|
+
* @param {*} params Parameters to pass to the constructor. Recommended to use only with factories.
|
|
102
|
+
* @return {(function(*, {kind: string, name: string, addInitializer: Function}): void)}
|
|
103
|
+
* @example @InjectLazy(MySingleton) mySingleton
|
|
104
|
+
* @example @InjectLazy("myCustomName") myFactory
|
|
105
|
+
* @throws {Error} If the injection target is not a field
|
|
106
|
+
* @throws {Error} If the injected field is assigned a value
|
|
107
|
+
*/
|
|
108
|
+
export function InjectLazy(clazzOrName, ...params) {
|
|
109
|
+
const cache = new WeakMap()
|
|
110
|
+
return (initialValue, context) => {
|
|
111
|
+
if (context.kind !== 'field') {
|
|
109
112
|
throw new Error('Invalid injection target')
|
|
110
113
|
}
|
|
114
|
+
context.addInitializer(function () {
|
|
115
|
+
Object.defineProperty(this, context.name, {
|
|
116
|
+
get() {
|
|
117
|
+
if (!cache.has(this)) {
|
|
118
|
+
const instanceContext = getContext(clazzOrName)
|
|
119
|
+
const value = getInjectedInstance(instanceContext, params)
|
|
120
|
+
cache.set(this, value)
|
|
121
|
+
}
|
|
122
|
+
return cache.get(this)
|
|
123
|
+
},
|
|
124
|
+
configurable: true,
|
|
125
|
+
enumerable: true
|
|
126
|
+
})
|
|
127
|
+
})
|
|
111
128
|
}
|
|
112
129
|
}
|
|
113
130
|
|
|
131
|
+
/**
|
|
132
|
+
* Get a proxy for the mock instance. This allows the mock to call methods on the original class if they are not defined in the mock.
|
|
133
|
+
* @param {Object} mock The mock instance
|
|
134
|
+
* @param {Object} original The original class instance
|
|
135
|
+
* @return {*|object} The proxy instance
|
|
136
|
+
*/
|
|
114
137
|
function getProxy(mock, original) {
|
|
115
138
|
return new Proxy(mock, {
|
|
116
139
|
get(target, prop, receiver) {
|
|
@@ -126,20 +149,20 @@ function getProxy(mock, original) {
|
|
|
126
149
|
* Mark a class as a mock. This will replace the class with a mock instance when injected.
|
|
127
150
|
* @param {string|Class} mockedClazzOrName The singleton or factory class or name to be mocked
|
|
128
151
|
* @param {boolean} [proxy=false] If true, the mock will be a proxy to the original class. Any methods not defined in the mock will be called on the original class.
|
|
129
|
-
* @return {(function(
|
|
152
|
+
* @return {(function(Function, {kind: string}): void)}
|
|
130
153
|
* @example @Mock(MySingleton) class MyMock {}
|
|
131
154
|
* @example @Mock("myCustomName") class MyMock {}
|
|
132
155
|
* @throws {Error} If the injection target is not a class
|
|
133
156
|
* @throws {Error} If the injection source is not found
|
|
134
157
|
*/
|
|
135
158
|
export function Mock(mockedClazzOrName, proxy = false) {
|
|
136
|
-
return function(clazz, context) {
|
|
137
|
-
if (context.kind !==
|
|
159
|
+
return function (clazz, context) {
|
|
160
|
+
if (context.kind !== 'class') {
|
|
138
161
|
throw new Error('Invalid injection target')
|
|
139
162
|
}
|
|
140
163
|
const instanceContext = getContext(mockedClazzOrName)
|
|
141
164
|
if (instanceContext.originalClazz) {
|
|
142
|
-
|
|
165
|
+
throw new Error('Mock already defined, reset before mocking again')
|
|
143
166
|
}
|
|
144
167
|
instanceContext.originalClazz = instanceContext.clazz
|
|
145
168
|
instanceContext.proxy = proxy
|
|
@@ -147,11 +170,22 @@ export function Mock(mockedClazzOrName, proxy = false) {
|
|
|
147
170
|
}
|
|
148
171
|
}
|
|
149
172
|
|
|
173
|
+
/**
|
|
174
|
+
* Internal: Get the context for a given class or name.
|
|
175
|
+
*
|
|
176
|
+
* @param {string|Class} mockedClazzOrName - The class or name to look up.
|
|
177
|
+
* @returns {InstanceContext}
|
|
178
|
+
* @throws {Error} If the context is not found.
|
|
179
|
+
*/
|
|
150
180
|
function getContext(mockedClazzOrName) {
|
|
151
181
|
if (instances.has(mockedClazzOrName)) {
|
|
152
182
|
return instances.get(mockedClazzOrName)
|
|
153
183
|
} else {
|
|
154
|
-
|
|
184
|
+
const available = Array.from(instances.keys()).map(k => typeof k === 'string' ? k : k.name).join(', ')
|
|
185
|
+
throw new Error(
|
|
186
|
+
`Cannot find injection source for "${mockedClazzOrName?.name || mockedClazzOrName}". ` +
|
|
187
|
+
`Available: [${available}]`
|
|
188
|
+
)
|
|
155
189
|
}
|
|
156
190
|
}
|
|
157
191
|
|
|
@@ -160,7 +194,7 @@ function getContext(mockedClazzOrName) {
|
|
|
160
194
|
*/
|
|
161
195
|
export function resetMocks() {
|
|
162
196
|
for (const instanceContext of instances.values()) {
|
|
163
|
-
|
|
197
|
+
restoreOriginal(instanceContext)
|
|
164
198
|
}
|
|
165
199
|
}
|
|
166
200
|
|
|
@@ -169,7 +203,7 @@ export function resetMocks() {
|
|
|
169
203
|
* @param {string|Class} clazzOrName The singleton or factory class or name to reset
|
|
170
204
|
*/
|
|
171
205
|
export function resetMock(clazzOrName) {
|
|
172
|
-
|
|
206
|
+
restoreOriginal(getContext(clazzOrName))
|
|
173
207
|
}
|
|
174
208
|
|
|
175
209
|
/**
|
|
@@ -177,14 +211,46 @@ export function resetMock(clazzOrName) {
|
|
|
177
211
|
* @param {InstanceContext} instanceContext The instance context to reset
|
|
178
212
|
* @private
|
|
179
213
|
*/
|
|
180
|
-
function
|
|
214
|
+
function restoreOriginal(instanceContext) {
|
|
181
215
|
if (!instanceContext) {
|
|
182
216
|
throw new Error('Cannot find injection source with the provided name')
|
|
183
217
|
}
|
|
184
218
|
if (instanceContext.originalClazz) {
|
|
185
219
|
instanceContext.clazz = instanceContext.originalClazz
|
|
186
|
-
instanceContext.instance
|
|
220
|
+
delete instanceContext.instance
|
|
187
221
|
delete instanceContext.originalClazz
|
|
188
222
|
delete instanceContext.originalInstance
|
|
189
223
|
}
|
|
190
224
|
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Get the injected instance based on the context and parameters.
|
|
228
|
+
* @param {InstanceContext} instanceContext The instance context
|
|
229
|
+
* @param {Array} params The parameters to pass to the constructor
|
|
230
|
+
* @return {Object} The injected instance
|
|
231
|
+
*/
|
|
232
|
+
function getInjectedInstance(instanceContext, params) {
|
|
233
|
+
if (instanceContext.type === 'singleton' && !instanceContext.originalClazz && instanceContext.instance) {
|
|
234
|
+
return instanceContext.instance
|
|
235
|
+
}
|
|
236
|
+
let instance
|
|
237
|
+
try {
|
|
238
|
+
instance = new instanceContext.clazz(...params)
|
|
239
|
+
} catch (err) {
|
|
240
|
+
if (err instanceof RangeError) {
|
|
241
|
+
throw new Error(
|
|
242
|
+
`Circular dependency detected for ${instanceContext.clazz.name || instanceContext.clazz}. ` +
|
|
243
|
+
`Use @InjectLazy to break the cycle.`
|
|
244
|
+
)
|
|
245
|
+
}
|
|
246
|
+
throw err
|
|
247
|
+
}
|
|
248
|
+
if (instanceContext.proxy && instanceContext.originalClazz) {
|
|
249
|
+
const originalInstance = new instanceContext.originalClazz(...params)
|
|
250
|
+
instance = getProxy(instance, originalInstance)
|
|
251
|
+
}
|
|
252
|
+
if (instanceContext.type === 'singleton') {
|
|
253
|
+
instanceContext.instance = instance
|
|
254
|
+
}
|
|
255
|
+
return instance
|
|
256
|
+
}
|
package/package.json
CHANGED
package/test/injection.test.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {Factory, Inject, Singleton} from '../index.js'
|
|
1
|
+
import {Factory, Inject, resetMocks, Singleton} from '../index.js'
|
|
2
2
|
|
|
3
3
|
describe('Injection via fields', () => {
|
|
4
4
|
@Singleton()
|
|
@@ -46,6 +46,12 @@ describe('Injection via fields', () => {
|
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
afterEach(() => {
|
|
50
|
+
TestFactory.calls = 0
|
|
51
|
+
TestSingleton.calls = 0
|
|
52
|
+
resetMocks()
|
|
53
|
+
})
|
|
54
|
+
|
|
49
55
|
it('should inject factory', () => {
|
|
50
56
|
class TestInjectionFactory {
|
|
51
57
|
@Inject(TestFactory) testFactory
|
|
@@ -83,7 +89,53 @@ describe('Injection via fields', () => {
|
|
|
83
89
|
new TestInjectionFactoryParams()
|
|
84
90
|
})
|
|
85
91
|
|
|
86
|
-
|
|
92
|
+
it('should cache factory instance on repeated accesses', () => {
|
|
93
|
+
class TestRepeatedFactoryAccess {
|
|
94
|
+
@Inject(TestFactory) testFactory
|
|
95
|
+
|
|
96
|
+
constructor() {
|
|
97
|
+
const instance1 = this.testFactory
|
|
98
|
+
const instance2 = this.testFactory
|
|
99
|
+
expect(instance1).toBe(instance2)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
new TestRepeatedFactoryAccess()
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it('should create distinct factory instances for different fields in the same object', () => {
|
|
107
|
+
class TestMultipleFactoryInjection {
|
|
108
|
+
@Inject(TestFactory) testFactory1
|
|
109
|
+
@Inject(TestFactory) testFactory2
|
|
110
|
+
|
|
111
|
+
constructor() {
|
|
112
|
+
// Access both properties to trigger initialization.
|
|
113
|
+
const one = this.testFactory1
|
|
114
|
+
const two = this.testFactory2
|
|
115
|
+
expect(one).not.toBe(two)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
new TestMultipleFactoryInjection()
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
it('should inject the same singleton instance for different fields in the same object', () => {
|
|
123
|
+
class TestMultipleSingletonInjection {
|
|
124
|
+
@Inject(TestSingleton) testSingleton1
|
|
125
|
+
@Inject(TestSingleton) testSingleton2
|
|
126
|
+
|
|
127
|
+
constructor() {
|
|
128
|
+
// Access both properties to trigger initialization.
|
|
129
|
+
const one = this.testSingleton1
|
|
130
|
+
const two = this.testSingleton2
|
|
131
|
+
expect(one).toBe(two)
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
new TestMultipleSingletonInjection()
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
@Singleton('named')
|
|
87
139
|
class NamedSingleton {
|
|
88
140
|
static calls = 0
|
|
89
141
|
|
|
@@ -94,7 +146,7 @@ describe('Injection via fields', () => {
|
|
|
94
146
|
|
|
95
147
|
it('should inject named singleton', () => {
|
|
96
148
|
class TestInjectionNamedSingleton {
|
|
97
|
-
@Inject(
|
|
149
|
+
@Inject('named') namedSingleton
|
|
98
150
|
|
|
99
151
|
constructor() {
|
|
100
152
|
expect(this.namedSingleton).toBeInstanceOf(NamedSingleton)
|
|
@@ -103,7 +155,7 @@ describe('Injection via fields', () => {
|
|
|
103
155
|
}
|
|
104
156
|
|
|
105
157
|
class TestInjectionNamedSingleton2 {
|
|
106
|
-
@Inject(
|
|
158
|
+
@Inject('named') namedSingleton
|
|
107
159
|
|
|
108
160
|
constructor() {
|
|
109
161
|
expect(this.namedSingleton).toBeInstanceOf(NamedSingleton)
|
|
@@ -115,7 +167,7 @@ describe('Injection via fields', () => {
|
|
|
115
167
|
new TestInjectionNamedSingleton2()
|
|
116
168
|
})
|
|
117
169
|
|
|
118
|
-
@Factory(
|
|
170
|
+
@Factory('named2')
|
|
119
171
|
class NamedFactory {
|
|
120
172
|
static calls = 0
|
|
121
173
|
params
|
|
@@ -128,7 +180,7 @@ describe('Injection via fields', () => {
|
|
|
128
180
|
|
|
129
181
|
it('should inject named factory', () => {
|
|
130
182
|
class TestInjectionNamedFactory {
|
|
131
|
-
@Inject(
|
|
183
|
+
@Inject('named2') namedFactory
|
|
132
184
|
|
|
133
185
|
constructor() {
|
|
134
186
|
expect(this.namedFactory).toBeInstanceOf(NamedFactory)
|
|
@@ -137,7 +189,7 @@ describe('Injection via fields', () => {
|
|
|
137
189
|
}
|
|
138
190
|
|
|
139
191
|
class TestInjectionNamedFactory2 {
|
|
140
|
-
@Inject(
|
|
192
|
+
@Inject('named2') namedFactory
|
|
141
193
|
|
|
142
194
|
constructor() {
|
|
143
195
|
expect(this.namedFactory).toBeInstanceOf(NamedFactory)
|
|
@@ -149,4 +201,109 @@ describe('Injection via fields', () => {
|
|
|
149
201
|
new TestInjectionNamedFactory2()
|
|
150
202
|
expect(result.namedFactory.params).toEqual([])
|
|
151
203
|
})
|
|
204
|
+
|
|
205
|
+
it('should cache named factory instance on repeated accesses', () => {
|
|
206
|
+
class TestRepeatedNamedFactoryAccess {
|
|
207
|
+
@Inject('named2') namedFactory
|
|
208
|
+
|
|
209
|
+
constructor() {
|
|
210
|
+
const instance1 = this.namedFactory
|
|
211
|
+
const instance2 = this.namedFactory
|
|
212
|
+
expect(instance1).toBe(instance2)
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
new TestRepeatedNamedFactoryAccess()
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
it('should throw if @Inject is applied to a method', () => {
|
|
220
|
+
expect(() => {
|
|
221
|
+
// noinspection JSUnusedLocalSymbols
|
|
222
|
+
class BadInjection {
|
|
223
|
+
@Inject('something')
|
|
224
|
+
someMethod() {
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}).toThrow()
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
it('should handle circular dependencies gracefully', () => {
|
|
231
|
+
@Singleton()
|
|
232
|
+
class A {
|
|
233
|
+
@Inject('B') b
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
@Singleton('B')
|
|
237
|
+
class B {
|
|
238
|
+
@Inject(A) a
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
expect(() => new A()).toThrow(/Circular dependency detected.*@InjectLazy/)
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
it('should throw if decorator is used on non-class object', () => {
|
|
245
|
+
expect(() => {
|
|
246
|
+
const obj = {}
|
|
247
|
+
Inject('something')(obj, 'field')
|
|
248
|
+
}).toThrow()
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
it('should throw a helpful error for eager circular dependencies', () => {
|
|
252
|
+
@Factory()
|
|
253
|
+
class A2 {
|
|
254
|
+
@Inject('B2') b
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
@Singleton('B2')
|
|
258
|
+
class B2 {
|
|
259
|
+
@Inject(A2) a
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
expect(() => new A2()).toThrow(/Circular dependency detected.*@InjectLazy/)
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
it('should inject into symbol-named fields', () => {
|
|
266
|
+
const sym = Symbol('sym')
|
|
267
|
+
|
|
268
|
+
@Singleton()
|
|
269
|
+
class S {
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
class Test {
|
|
273
|
+
@Inject(S) [sym]
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const t = new Test()
|
|
277
|
+
expect(t[sym]).toBeInstanceOf(S)
|
|
278
|
+
})
|
|
279
|
+
|
|
280
|
+
it('should not leak injected properties to prototype', () => {
|
|
281
|
+
@Singleton()
|
|
282
|
+
class S {
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
class Test {
|
|
286
|
+
@Inject(S) dep
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// noinspection JSUnusedLocalSymbols
|
|
290
|
+
const t = new Test()
|
|
291
|
+
expect(Object.prototype.hasOwnProperty.call(Test.prototype, 'dep')).toBe(false)
|
|
292
|
+
})
|
|
293
|
+
|
|
294
|
+
it('should handle undefined/null/complex params in factory', () => {
|
|
295
|
+
@Factory()
|
|
296
|
+
class F {
|
|
297
|
+
constructor(...params) {
|
|
298
|
+
this.params = params
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
class Test {
|
|
303
|
+
@Inject(F, undefined, null, {a: 1}) dep
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const t = new Test()
|
|
307
|
+
expect(t.dep.params).toEqual([undefined, null, {a: 1}])
|
|
308
|
+
})
|
|
152
309
|
})
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import {Factory, InjectLazy, resetMocks, Singleton} from '../index.js'
|
|
2
|
+
|
|
3
|
+
describe('Lazy Injection via fields', () => {
|
|
4
|
+
@Singleton()
|
|
5
|
+
class TestLazySingleton {
|
|
6
|
+
static calls = 0
|
|
7
|
+
|
|
8
|
+
constructor() {
|
|
9
|
+
TestLazySingleton.calls++
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
it('should lazily inject singleton', () => {
|
|
14
|
+
class TestLazySingletonInjection {
|
|
15
|
+
@InjectLazy(TestLazySingleton) testSingleton
|
|
16
|
+
|
|
17
|
+
constructor() {
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const instance = new TestLazySingletonInjection()
|
|
22
|
+
expect(TestLazySingleton.calls).toBe(0) // Not constructed until access
|
|
23
|
+
const first = instance.testSingleton
|
|
24
|
+
expect(first).toBeInstanceOf(TestLazySingleton)
|
|
25
|
+
expect(TestLazySingleton.calls).toBe(1)
|
|
26
|
+
// Repeated access returns the same instance.
|
|
27
|
+
const second = instance.testSingleton
|
|
28
|
+
expect(first).toBe(second)
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
@Factory()
|
|
32
|
+
class TestLazyFactory {
|
|
33
|
+
static calls = 0
|
|
34
|
+
params
|
|
35
|
+
|
|
36
|
+
constructor(...params) {
|
|
37
|
+
TestLazyFactory.calls++
|
|
38
|
+
this.params = params
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
afterEach(() => {
|
|
43
|
+
TestLazyFactory.calls = 0
|
|
44
|
+
TestLazySingleton.calls = 0
|
|
45
|
+
resetMocks()
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('should lazily inject factory with caching on first access per field', () => {
|
|
49
|
+
class TestLazyFactoryInjection {
|
|
50
|
+
@InjectLazy(TestLazyFactory) testFactory
|
|
51
|
+
|
|
52
|
+
constructor() {
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const inst = new TestLazyFactoryInjection()
|
|
57
|
+
expect(TestLazyFactory.calls).toBe(0)
|
|
58
|
+
const first = inst.testFactory
|
|
59
|
+
expect(first).toBeInstanceOf(TestLazyFactory)
|
|
60
|
+
expect(TestLazyFactory.calls).toBe(1)
|
|
61
|
+
expect(inst.testFactory).toBe(first)
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('should create distinct factory instances for different fields', () => {
|
|
65
|
+
class TestMultipleLazyFactoryInjection {
|
|
66
|
+
@InjectLazy(TestLazyFactory) testFactory1
|
|
67
|
+
@InjectLazy(TestLazyFactory) testFactory2
|
|
68
|
+
|
|
69
|
+
constructor() {
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const instance = new TestMultipleLazyFactoryInjection()
|
|
74
|
+
expect(TestLazyFactory.calls).toBe(0)
|
|
75
|
+
const one = instance.testFactory1
|
|
76
|
+
const two = instance.testFactory2
|
|
77
|
+
expect(one).toBeInstanceOf(TestLazyFactory)
|
|
78
|
+
expect(two).toBeInstanceOf(TestLazyFactory)
|
|
79
|
+
expect(one).not.toBe(two)
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('should pass constructor parameters when using lazy injection', () => {
|
|
83
|
+
class TestLazyFactoryParamsInjection {
|
|
84
|
+
@InjectLazy(TestLazyFactory, 'param1', 'param2') testFactory
|
|
85
|
+
|
|
86
|
+
constructor() {
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
TestLazyFactory.calls = 0
|
|
91
|
+
const instance = new TestLazyFactoryParamsInjection()
|
|
92
|
+
expect(TestLazyFactory.calls).toBe(0)
|
|
93
|
+
const factoryInst = instance.testFactory
|
|
94
|
+
expect(factoryInst).toBeInstanceOf(TestLazyFactory)
|
|
95
|
+
expect(factoryInst.params).toEqual(['param1', 'param2'])
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('should not initialize dependency if property is never accessed', () => {
|
|
99
|
+
class TestNeverAccess {
|
|
100
|
+
@InjectLazy(TestLazySingleton) testSingleton
|
|
101
|
+
|
|
102
|
+
constructor() {
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
TestLazySingleton.calls = 0
|
|
107
|
+
new TestNeverAccess() // Do not access testSingleton.
|
|
108
|
+
expect(TestLazySingleton.calls).toBe(0)
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
@Singleton('lazyNamedSingleton')
|
|
112
|
+
class NamedLazySingleton {
|
|
113
|
+
static calls = 0
|
|
114
|
+
|
|
115
|
+
constructor() {
|
|
116
|
+
NamedLazySingleton.calls++
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
it('should lazily inject named singleton', () => {
|
|
121
|
+
class TestLazyNamedSingletonInjection {
|
|
122
|
+
@InjectLazy('lazyNamedSingleton') namedSingleton
|
|
123
|
+
|
|
124
|
+
constructor() {
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const instance = new TestLazyNamedSingletonInjection()
|
|
129
|
+
expect(NamedLazySingleton.calls).toBe(0)
|
|
130
|
+
const first = instance.namedSingleton
|
|
131
|
+
expect(first).toBeInstanceOf(NamedLazySingleton)
|
|
132
|
+
expect(NamedLazySingleton.calls).toBe(1)
|
|
133
|
+
expect(instance.namedSingleton).toBe(first)
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
@Factory('lazyNamedFactory')
|
|
137
|
+
class NamedLazyFactory {
|
|
138
|
+
static calls = 0
|
|
139
|
+
params
|
|
140
|
+
|
|
141
|
+
constructor(...params) {
|
|
142
|
+
NamedLazyFactory.calls++
|
|
143
|
+
this.params = params
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
it('should lazily inject named factory', () => {
|
|
148
|
+
class TestLazyNamedFactoryInjection {
|
|
149
|
+
@InjectLazy('lazyNamedFactory') namedFactory
|
|
150
|
+
|
|
151
|
+
constructor() {
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
NamedLazyFactory.calls = 0
|
|
156
|
+
const instance = new TestLazyNamedFactoryInjection()
|
|
157
|
+
expect(NamedLazyFactory.calls).toBe(0)
|
|
158
|
+
const first = instance.namedFactory
|
|
159
|
+
expect(first).toBeInstanceOf(NamedLazyFactory)
|
|
160
|
+
expect(NamedLazyFactory.calls).toBe(1)
|
|
161
|
+
expect(instance.namedFactory).toBe(first)
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
it('should not expose any internal caching artifacts', () => {
|
|
165
|
+
class TestLazyEnum {
|
|
166
|
+
@InjectLazy(TestLazySingleton) lazyProp
|
|
167
|
+
|
|
168
|
+
constructor() {
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const inst = new TestLazyEnum()
|
|
173
|
+
// noinspection JSUnusedLocalSymbols: Force lazy initialization.
|
|
174
|
+
const _ = inst.lazyProp
|
|
175
|
+
const keys = Object.keys(inst)
|
|
176
|
+
expect(keys).toContain('lazyProp')
|
|
177
|
+
expect(keys.length).toBe(1)
|
|
178
|
+
const symbols = Object.getOwnPropertySymbols(inst)
|
|
179
|
+
expect(symbols.length).toBe(0)
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
it('should throw if @InjectLazy is applied to a method', () => {
|
|
183
|
+
expect(() => {
|
|
184
|
+
// noinspection JSUnusedLocalSymbols
|
|
185
|
+
class BadLazy {
|
|
186
|
+
@InjectLazy('something')
|
|
187
|
+
someMethod() {
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}).toThrow()
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
it('should handle circular dependencies (lazy)', () => {
|
|
194
|
+
@Singleton()
|
|
195
|
+
class A {
|
|
196
|
+
@InjectLazy('B') b
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
@Singleton('B')
|
|
200
|
+
class B {
|
|
201
|
+
@InjectLazy(A) a
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
expect(() => new A()).not.toThrow()
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
it('should inject into symbol-named fields (lazy)', () => {
|
|
208
|
+
const sym = Symbol('sym')
|
|
209
|
+
|
|
210
|
+
@Singleton()
|
|
211
|
+
class S {
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
class Test {
|
|
215
|
+
@InjectLazy(S) [sym]
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const t = new Test()
|
|
219
|
+
expect(t[sym]).toBeInstanceOf(S)
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
it('should not leak lazy injected properties to prototype', () => {
|
|
223
|
+
@Singleton()
|
|
224
|
+
class S {
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
class Test {
|
|
228
|
+
@InjectLazy(S) dep
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// noinspection JSUnusedLocalSymbols
|
|
232
|
+
const t = new Test()
|
|
233
|
+
expect(Object.prototype.hasOwnProperty.call(Test.prototype, 'dep')).toBe(false)
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
it('should allow circular dependencies with lazy injection', () => {
|
|
237
|
+
@Singleton()
|
|
238
|
+
class A1 {
|
|
239
|
+
@InjectLazy('B1') b
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
@Factory('B1')
|
|
243
|
+
class B1 {
|
|
244
|
+
@InjectLazy(A1) a
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
expect(() => new A1()).not.toThrow()
|
|
248
|
+
})
|
|
249
|
+
})
|
package/test/mock.test.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {Factory, Inject, InjectLazy, Mock, resetMock, resetMocks, Singleton} from '../index.js'
|
|
2
2
|
|
|
3
3
|
describe('Mocking', () => {
|
|
4
4
|
@Singleton()
|
|
@@ -24,12 +24,12 @@ describe('Mocking', () => {
|
|
|
24
24
|
@Mock(ToBeMockedSingleton)
|
|
25
25
|
class MockedSingleton {
|
|
26
26
|
op() {
|
|
27
|
-
return '
|
|
27
|
+
return 'mocked1'
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
const result = new TestInjection()
|
|
32
|
-
expect(result.toBeMockedSingleton.op()).toBe('
|
|
32
|
+
expect(result.toBeMockedSingleton.op()).toBe('mocked1')
|
|
33
33
|
expect(result.toBeMockedSingleton.op2).toBe.undefined
|
|
34
34
|
|
|
35
35
|
resetMocks()
|
|
@@ -38,6 +38,30 @@ describe('Mocking', () => {
|
|
|
38
38
|
expect(result2.toBeMockedSingleton.op2()).toBe('original2')
|
|
39
39
|
})
|
|
40
40
|
|
|
41
|
+
// New tests for lazy injection with mocks (non-proxied)
|
|
42
|
+
it('should inject a lazy mock singleton', () => {
|
|
43
|
+
@Mock(ToBeMockedSingleton)
|
|
44
|
+
class MockedSingletonLazy {
|
|
45
|
+
op() {
|
|
46
|
+
return 'mocked2'
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
class TestInjectionLazy {
|
|
51
|
+
@InjectLazy(ToBeMockedSingleton) lazyMockedSingleton
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const instance = new TestInjectionLazy()
|
|
55
|
+
const result = instance.lazyMockedSingleton
|
|
56
|
+
expect(result.op()).toBe('mocked2')
|
|
57
|
+
expect(result.op2).toBe.undefined
|
|
58
|
+
|
|
59
|
+
resetMocks()
|
|
60
|
+
const instance2 = new TestInjectionLazy()
|
|
61
|
+
expect(instance2.lazyMockedSingleton.op()).toBe('original')
|
|
62
|
+
expect(instance2.lazyMockedSingleton.op2()).toBe('original2')
|
|
63
|
+
})
|
|
64
|
+
|
|
41
65
|
@Factory()
|
|
42
66
|
class ToBeMockedFactory {
|
|
43
67
|
op() {
|
|
@@ -65,13 +89,207 @@ describe('Mocking', () => {
|
|
|
65
89
|
expect(result2.toBeMockedFactory.op()).toBe('original')
|
|
66
90
|
})
|
|
67
91
|
|
|
92
|
+
// New tests for lazy injection with mock factory
|
|
93
|
+
it('should inject a lazy mock factory', () => {
|
|
94
|
+
@Mock(ToBeMockedFactory)
|
|
95
|
+
class MockedFactoryLazy {
|
|
96
|
+
op() {
|
|
97
|
+
return 'mocked'
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
class TestInjectionFactoryLazy {
|
|
102
|
+
@InjectLazy(ToBeMockedFactory) lazyMockedFactory
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const instance = new TestInjectionFactoryLazy()
|
|
106
|
+
const result = instance.lazyMockedFactory
|
|
107
|
+
expect(result.op()).toBe('mocked')
|
|
108
|
+
|
|
109
|
+
resetMocks()
|
|
110
|
+
const instance2 = new TestInjectionFactoryLazy()
|
|
111
|
+
expect(instance2.lazyMockedFactory.op()).toBe('original')
|
|
112
|
+
})
|
|
113
|
+
|
|
68
114
|
it('should throw an error if a mock is not a singleton or factory', () => {
|
|
69
115
|
expect(() => {
|
|
70
116
|
@Mock(ToBeMockedFactory)
|
|
71
|
-
class Mocked1 {
|
|
117
|
+
class Mocked1 {
|
|
118
|
+
}
|
|
72
119
|
|
|
73
120
|
@Mock(ToBeMockedFactory)
|
|
74
|
-
class Mocked2 {
|
|
121
|
+
class Mocked2 {
|
|
122
|
+
}
|
|
75
123
|
}).toThrow('Mock already defined, reset before mocking again')
|
|
76
124
|
})
|
|
77
|
-
|
|
125
|
+
|
|
126
|
+
// Edge case: Resetting specific mocks
|
|
127
|
+
it('should reset only the specified mock', () => {
|
|
128
|
+
@Singleton()
|
|
129
|
+
class A {
|
|
130
|
+
value() {
|
|
131
|
+
return 'A'
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
@Singleton()
|
|
136
|
+
class B {
|
|
137
|
+
value() {
|
|
138
|
+
return 'B'
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
@Mock(A)
|
|
143
|
+
class MockA {
|
|
144
|
+
value() {
|
|
145
|
+
return 'mockA'
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
@Mock(B)
|
|
150
|
+
class MockB {
|
|
151
|
+
value() {
|
|
152
|
+
return 'mockB'
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
class Test {
|
|
157
|
+
@Inject(A) a
|
|
158
|
+
@Inject(B) b
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const t = new Test()
|
|
162
|
+
expect(t.a.value()).toBe('mockA')
|
|
163
|
+
expect(t.b.value()).toBe('mockB')
|
|
164
|
+
|
|
165
|
+
resetMock(A)
|
|
166
|
+
const t2 = new Test()
|
|
167
|
+
expect(t2.a.value()).toBe('A')
|
|
168
|
+
expect(t2.b.value()).toBe('mockB')
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
it('should use the latest mock when multiple mocks are applied', () => {
|
|
172
|
+
@Singleton()
|
|
173
|
+
class Original {
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
@Mock(Original)
|
|
177
|
+
class Mock1 {
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
resetMock(Original)
|
|
181
|
+
|
|
182
|
+
@Mock(Original)
|
|
183
|
+
class Mock2 {
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
class Test {
|
|
187
|
+
@Inject(Original) dep
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const t = new Test()
|
|
191
|
+
expect(t.dep).toBeInstanceOf(Mock2)
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
it('should restore the original after unmocking', () => {
|
|
195
|
+
@Singleton()
|
|
196
|
+
class Orig {
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
@Mock(Orig)
|
|
200
|
+
class Mocked {
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
resetMock(Orig)
|
|
204
|
+
|
|
205
|
+
class Test {
|
|
206
|
+
@Inject(Orig) dep
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const t = new Test()
|
|
210
|
+
expect(t.dep).toBeInstanceOf(Orig)
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
it('should inject subclass correctly', () => {
|
|
214
|
+
@Singleton()
|
|
215
|
+
class Base {
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
class Sub extends Base {
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
@Mock(Base)
|
|
222
|
+
class SubMock extends Sub {
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
class Test {
|
|
226
|
+
@Inject(Base) dep
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const t = new Test()
|
|
230
|
+
expect(t.dep).toBeInstanceOf(Sub)
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
it('should use latest mock for lazy injection', () => {
|
|
234
|
+
@Singleton()
|
|
235
|
+
class Orig {
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
@Mock(Orig)
|
|
239
|
+
class Mock1 {
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
resetMock(Orig)
|
|
243
|
+
|
|
244
|
+
@Mock(Orig)
|
|
245
|
+
class Mock2 {
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
class Test {
|
|
249
|
+
@InjectLazy(Orig) dep
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const t = new Test()
|
|
253
|
+
expect(t.dep).toBeInstanceOf(Mock2)
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
it('should restore original after unmocking (lazy)', () => {
|
|
257
|
+
@Singleton()
|
|
258
|
+
class Orig {
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
@Mock(Orig)
|
|
262
|
+
class Mocked {
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
resetMock(Orig)
|
|
266
|
+
|
|
267
|
+
class Test {
|
|
268
|
+
@InjectLazy(Orig) dep
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const t = new Test()
|
|
272
|
+
expect(t.dep).toBeInstanceOf(Orig)
|
|
273
|
+
})
|
|
274
|
+
|
|
275
|
+
it('should inject subclass correctly (lazy)', () => {
|
|
276
|
+
@Singleton()
|
|
277
|
+
class Base {
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
class Sub extends Base {
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
@Mock(Base)
|
|
284
|
+
class SubMock extends Sub {
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
class Test {
|
|
288
|
+
@InjectLazy(Base) dep
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const t = new Test()
|
|
292
|
+
expect(t.dep).toBeInstanceOf(Sub)
|
|
293
|
+
})
|
|
294
|
+
})
|
|
295
|
+
|
package/test/proxy.test.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {Factory, Inject, InjectLazy, Mock, resetMocks, Singleton} from '../index.js'
|
|
2
2
|
|
|
3
3
|
describe('Proxy Mocking', () => {
|
|
4
4
|
@Singleton()
|
|
@@ -70,4 +70,61 @@ describe('Proxy Mocking', () => {
|
|
|
70
70
|
expect(result2.toBeProxiedFactory.op()).toBe('original')
|
|
71
71
|
expect(result2.toBeProxiedFactory.op2()).toBe('original2')
|
|
72
72
|
})
|
|
73
|
-
|
|
73
|
+
|
|
74
|
+
it('should inject a lazy proxy singleton', () => {
|
|
75
|
+
@Mock(ToBeProxiedSingleton, true)
|
|
76
|
+
class ProxiedSingletonLazy {
|
|
77
|
+
op() {
|
|
78
|
+
return 'mocked'
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
class TestInjectionLazy {
|
|
83
|
+
@InjectLazy(ToBeProxiedSingleton) lazyProxiedSingleton
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const instance = new TestInjectionLazy()
|
|
87
|
+
const result = instance.lazyProxiedSingleton
|
|
88
|
+
expect(result.op()).toBe('mocked')
|
|
89
|
+
expect(result.op2()).toBe('original2')
|
|
90
|
+
|
|
91
|
+
resetMocks()
|
|
92
|
+
const instance2 = new TestInjectionLazy()
|
|
93
|
+
expect(instance2.lazyProxiedSingleton.op()).toBe('original')
|
|
94
|
+
expect(instance2.lazyProxiedSingleton.op2()).toBe('original2')
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
it('should inject a lazy proxy factory', () => {
|
|
98
|
+
@Factory()
|
|
99
|
+
class ToBeProxiedFactoryLazy {
|
|
100
|
+
op() {
|
|
101
|
+
return 'original'
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
op2() {
|
|
105
|
+
return 'original2'
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
class TestInjectionFactoryLazy {
|
|
110
|
+
@InjectLazy(ToBeProxiedFactoryLazy) lazyProxiedFactory
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
@Mock(ToBeProxiedFactoryLazy, true)
|
|
114
|
+
class ProxiedFactoryLazy {
|
|
115
|
+
op() {
|
|
116
|
+
return 'mocked'
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const instance = new TestInjectionFactoryLazy()
|
|
121
|
+
const result = instance.lazyProxiedFactory
|
|
122
|
+
expect(result.op()).toBe('mocked')
|
|
123
|
+
expect(result.op2()).toBe('original2')
|
|
124
|
+
|
|
125
|
+
resetMocks()
|
|
126
|
+
const instance2 = new TestInjectionFactoryLazy()
|
|
127
|
+
expect(instance2.lazyProxiedFactory.op()).toBe('original')
|
|
128
|
+
expect(instance2.lazyProxiedFactory.op2()).toBe('original2')
|
|
129
|
+
})
|
|
130
|
+
})
|