Completed
Push — master ( 44d532...dfbaad )
by Esaú
01:39
created

hierarchy-helper.js ➔ instanceDefinedOrNull   B

Complexity

Conditions 10
Paths 4

Size

Total Lines 20

Duplication

Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
cc 10
c 3
b 0
f 0
nc 4
dl 0
loc 20
rs 7.2765
nop 8

How to fix   Complexity    Many Parameters   

Complexity

Complex classes like hierarchy-helper.js ➔ instanceDefinedOrNull often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

1
// spec/helpers/hierarchy-helper.js
2
"use strict";
3
4
// :: DEPENDENCIES
5
6
const path = require("path");
7
const root = path.dirname(path.dirname(__dirname));
8
9
module.exports = (hierarchy) => {
10
    // check parameters
11
    if (!Array.isArray(hierarchy)) {
12
        throw new Error("hierarchy must be an array");
13
    }
14
15
    // load dependencies
16
    let deps      = hierarchy.map(value => require(path.join(root, value + ".js")));
17
    let klassName = hierarchy.pop();
18
    let Klass     = deps.pop();
19
    const first   = (hierarchy.length === 0);
20
    const third   = (hierarchy.length >= 3);
21
22
    // :: TESTING
23
24
    // test the last class in the hierarchy tree
25
    describe(klassName, () => {
26
27
        // :: INHERITED PROTOTYPE
28
29
        // all inherit from Object
30
        it("should inherit from 'Object'", () => {
31
            expect(new Klass()).toEqual(jasmine.any(Object));
32
        });
33
34
        // check the hierarchy tree
35
        for (let i = 0; i < hierarchy.length; i += 1) {
36
            it("should inherit from '" + hierarchy[i] + "'", () => {
37
                expect(new Klass()).toEqual(jasmine.any(deps[i]));
38
            });
39
        }
40
41
        // check inherited properties
42
        if (!first) {
43
            it("should have a prototype property named 'name'", () => {
44
                expect(Klass.prototype).toHaveString("name");
45
            });
46
47
            it("should have a prototype property named 'message'", () => {
48
                expect(Klass.prototype).toHaveString("message");
49
            });
50
51
            it("should have a prototype property named 'code'", () => {
52
                expect(Klass.prototype).toHaveMember("code");
53
            });
54
        }
55
56
        // check Object methods
57
        it("should have a prototype method named 'toString()'", () => {
58
            expect(Klass.prototype).toHaveMethod("toString");
59
        });
60
61
        // check inherited methods
62
        if (!first) {
63
            it("should have a prototype method named 'native()'", () => {
64
                expect(Klass.prototype).toHaveMethod("native");
65
            });
66
        }
67
68
        // :: EXTENDED PROTOTYPE
69
70
        // check extended properties
71
        if (first) {
72
            it("should have a prototype property named 'name'", () => {
73
                expect(Klass.prototype).toHaveString("name");
74
            });
75
76
            it("should have a prototype property named 'message'", () => {
77
                expect(Klass.prototype).toHaveString("message");
78
            });
79
80
            it("should have a prototype property named 'code'", () => {
81
                expect(Klass.prototype).toHaveMember("code");
82
            });
83
84
            it("should have a prototype method named 'native()'", () => {
85
                expect(Klass.prototype).toHaveMethod("native");
86
            });
87
        }
88
89
        // :: PROTOTYPE VALUES
90
91
        it("should have the 'class' name in the prototype property named 'name'", () => {
92
            expect(Klass.prototype.name).toEqual(klassName);
93
        });
94
95
        it("should have a dummy default value as message", () => {
96
            expect(Klass.prototype.message).toEqual("thrown");
97
        });
98
99
        it("should have a null default value as code", () => {
100
            expect(Klass.prototype.code).toBeNull();
101
        });
102
103
        // :: CONSTRUCTOR
104
105
        it("should instantiate without parameters", () => {
106
            instanceNoParameters(Klass, testNoErrors, third);
107
            testNoErrors(() => new Klass());
108
        });
109
110
        it("should instantiate with parameters", () => {
111
            instanceParameters(Klass, testNoErrors, testNoErrors, testNoErrors, third);
112
        });
113
114
        // use the tests according to the hierarchy level
115
        if (third) {
116
            testThird(Klass);
117
        } else {
118
            test(Klass);
119
        }
120
121
    });
122
123
};
124
125
// Tests that a function doesn't throw any Error
126
function testNoErrors(fn) {
127
    expect(fn).not.toThrowError("parameter 'name' must be a 'string'");
128
    expect(fn).not.toThrowError("parameter 'message' must be a 'string'");
129
    expect(fn).not.toThrowError("parameter 'code' must be a 'number'");
130
}
131
132
// Tests instantiation of a class without parameters (undefined, null or none).
133
function instanceNoParameters(Klass, fn, third) {
134
    let arg1, arg2, arg3, test;
135
    test = (() => new Klass(arg1, arg2, arg3));
136
    for (let i = 0; i < 2; i += 1) {
137
        arg1 = (i % 2 === 0 ? undefined : null);
138
        for (let j = 0; j < 2; j += 1) {
139
            arg2 = (j % 2 === 0 ? undefined : null);
140
            if (third) {
141
                fn(test);
142
            } else {
143
                for (let e = 0; e < 2; e += 1) {
144
                    arg3 = (e % 2 === 0 ? undefined : null);
145
                    fn(test);
146
                }
147
            }
148
        }
149
    }
150
}
151
152
// Tests instantiation of a class with parameters.
153
function instanceParameters(Klass, fn1, fn2, fn3, third) {
154
    let arg1, arg2, arg3, test3, args1, args2, args3;
155
    const test1 = (() => new Klass(arg1));
156
    const test2 = (() => new Klass(arg1, arg2));
157
    if (third) {
158
        test3 = (() => null);
159
        args1 = [undefined, null, Klass.prototype.message];
160
        args2 = [undefined, null, Math.round(Math.random() * 0xFFFFFFFF)];
161
        args3 = [];
162
    } else {
163
        test3 = (() => new Klass(arg1, arg2, arg3));
164
        args1 = [undefined, null, Klass.prototype.name];
165
        args2 = [undefined, null, Klass.prototype.message];
166
        args3 = [undefined, null, Math.round(Math.random() * 0xFFFFFFFF)];
167
    }
168
    for (let i = 0; i < args1.length; i += 1) {
169
        arg1 = args1[i];
170
        fn1(test1);
171
        for (let j = 0; j < args2.length; j += 1) {
172
            arg2 = args2[j];
173
            fn2(test2);
174
            for (let e = 0; !third && e < args3.length; e += 1) {
175
                arg3 = args3[e];
176
                fn3(test3);
177
            }
178
        }
179
    }
180
}
181
182
// Loops for each parameter of the Klass constructor.
183
// If the iteration is even, the parameter is defined.
184
// If the iteration is odd, the parameter is null.
185
function instanceDefinedOrNull(Klass, name, message, code, fn1, fn2, fn3, third) {
186
    for (let i = 0; i < 2; i += 1) {
187
        const even1   = (i % 2 === 0);
188
        const arg1    = (even1 ? (third ? message : name) : null);
189
        const source1 = new Klass(arg1);
190
        fn1(source1, even1);
191
        for (let j = 0; j < 2; j += 1) {
192
            const even2   = (j % 2 === 0);
193
            const arg2    = (even2 ? (third ? code : message) : null);
194
            const source2 = new Klass(arg1, arg2);
195
            fn2(source2, even1, even2);
196
            for (let e = 0; !third && e < 2; e += 1) {
197
                const even3   = (e % 2 === 0);
198
                const arg3    = (even3 ? code : null);
199
                const source3 = new Klass(arg1, arg2, arg3);
200
                fn3(source3, even1, even2, even3);
201
            }
202
        }
203
    }
204
}
205
206
// Loops for each parameter of the Klass constructor using wrong types to test Error throwing.
207
function instanceThrowErrors(Klass, fn1, fn2, fn3, third) {
208
    let arg1, arg2, arg3, test33, test32, test31, test21, test22, test11, len1, len2, len3;
209
    const noStr = [{}, true, false, 42, 3.1416, -42, -3.1416, () => null];
210
    const noNmb = [{}, true, false, '', "qwerty", () => null];
211
    len1        = noStr.length;
212
    if (third) {
213
        len2   = noNmb.length;
214
        len3   = 0;
215
        test33 = test32 = test31 = (() => null);
216
    } else {
217
        len2   = len1;
218
        len3   = noNmb.length;
219
        test33 = (() => new Klass(arg1, arg2, arg3));
220
        test32 = (() => new Klass(null, arg2, arg3));
221
        test31 = (() => new Klass(null, null, arg3));
222
    }
223
    test22 = (() => new Klass(arg1, arg2));
224
    test21 = (() => new Klass(null, arg2));
225
    test11 = (() => new Klass(arg1));
226
    if (typeof Symbol === "function") {
227
        noStr.push(Symbol("symbol"));
228
        noNmb.push(Symbol("symbol"));
229
    }
230
    for (let i = 0; i < len1; i += 1) {
231
        arg1 = noStr[i];
232
        fn1(test11);
233
        for (let j = 0; j < len2; j += 1) {
234
            arg2 = (third ? noNmb[j] : noStr[j]);
235
            fn2(test21, test22);
236
            for (let e = 0; !third && e < len3; e += 1) {
237
                arg3 = noNmb[e];
238
                fn3(test31, test32, test33);
239
            }
240
        }
241
    }
242
}
243
244
// Tests classes that are a third level subclass, meaning that they require 2 arguments (message and code).
245
function testThird(Klass) {
246
247
    // :: CONSTRUCTOR
248
249
    it("should throw an Error if 'message' or 'code' are invalid parameters", () => {
250
        instanceThrowErrors(Klass, (test11) => {
251
            expect(test11).toThrowError("parameter 'message' must be a 'string'");
252
        }, (test21, test22) => {
253
            expect(test21).toThrowError("parameter 'code' must be a 'number'");
254
            expect(test22).toThrowError("parameter 'message' must be a 'string'");
255
        }, null, true);
256
    });
257
258
    // :: MEMBER PROPERTIES
259
260
    const message = "asdf";
261
    const code    = Math.round(Math.random() * 0xFFFFFFFF);
262
263
    it("should have all correct properties once instantiated", () => {
264
        instanceDefinedOrNull(Klass, null, message, code, (instance, even) => {
265
            if (even) {
266
                expect(instance.name).toEqual(Klass.prototype.name);
267
                expect(instance.message).toEqual(message);
268
            } else {
269
                expect(instance.name).toEqual(Klass.prototype.name);
270
                expect(instance.message).toEqual(Klass.prototype.message);
271
            }
272
            expect(instance.code).toBeNull();
273
        }, (instance, even1, even2) => {
274
            expect(instance.name).toEqual(Klass.prototype.name);
275
            expect(instance.message).toEqual(even1 ? message : Klass.prototype.message);
276
            expect(instance.code).toEqual(even2 ? code : null);
277
        }, null, true);
278
    });
279
280
    // :: MEMBER METHODS
281
282
    it("#toString()", () => {
283
        instanceDefinedOrNull(Klass, null, message, code, (instance, even) => {
284
            let exp;
285
            if (even) {
286
                exp = Klass.prototype.name + ": " + message + '.';
287
            } else {
288
                exp = Klass.prototype.name + ": " + Klass.prototype.message + '.';
289
            }
290
            expect(instance.toString()).toEqual(exp);
291
        }, (instance, even1, even2) => {
292
            let exp;
293
            exp  = Klass.prototype.name;
294
            exp += (even2 ? " (0x" + code.toString(16) + "):" : ':' ) + ' ';
295
            exp += (even1 ? message : Klass.prototype.message) + '.';
296
            expect(instance.toString()).toEqual(exp);
297
        }, null, true);
298
    });
299
300
    it("#native()", () => {
301
        instanceDefinedOrNull(Klass, null, message, code, (instance, even) => {
302
            const exp = (even ? message : Klass.prototype.message);
303
            expect(instance.native()).toEqual(new Error(exp));
304
        }, (instance, even1) => {
305
            const exp = (even1 ? message : Klass.prototype.message);
306
            expect(instance.native()).toEqual(new Error(exp));
307
        }, null, true);
308
    });
309
310
}
311
312
// Tests classes that require 3 arguments (name, message and code).
313
function test(Klass) {
314
315
    // :: CONSTRUCTOR
316
317
    it("should throw an Error if 'message' or 'code' are invalid parameters", () => {
318
        instanceThrowErrors(Klass, (test11) => {
319
            expect(test11).toThrowError("parameter 'name' must be a 'string'");
320
        }, (test21, test22) => {
321
            expect(test22).toThrowError("parameter 'name' must be a 'string'");
322
            expect(test21).toThrowError("parameter 'message' must be a 'string'");
323
        }, (test31, test32, test33) => {
324
            expect(test33).toThrowError("parameter 'name' must be a 'string'");
325
            expect(test32).toThrowError("parameter 'message' must be a 'string'");
326
            expect(test31).toThrowError("parameter 'code' must be a 'number'");
327
        }, false);
328
    });
329
330
    // :: MEMBER PROPERTIES
331
332
    const name    = "qwerty";
333
    const message = "asdf";
334
    const code    = Math.round(Math.random() * 0xFFFFFFFF);
335
336
    it("should have all correct properties once instantiated", () => {
337
        instanceDefinedOrNull(Klass, name, message, code, (instance, even) => {
338
            if (even) {
339
                expect(instance.name).toEqual(name);
340
                expect(instance.message).toEqual(Klass.prototype.message);
341
            } else {
342
                expect(instance.name).toEqual(Klass.prototype.name);
343
                expect(instance.message).toEqual(Klass.prototype.message);
344
            }
345
            expect(instance.code).toBeNull();
346
        }, (instance, even1, even2) => {
347
            expect(instance.name).toEqual(even1 ? name : Klass.prototype.name);
348
            expect(instance.message).toEqual(even2 ? message : Klass.prototype.message);
349
            expect(instance.code).toBeNull();
350
        }, (instance, even1, even2, even3) => {
351
            expect(instance.name).toEqual(even1 ? name : Klass.prototype.name);
352
            expect(instance.message).toEqual(even2 ? message : Klass.prototype.message);
353
            expect(instance.code).toEqual(even3 ? code : null);
354
        }, false);
355
    });
356
357
    // :: MEMBER METHODS
358
359
    it("#toString()", () => {
360
        instanceDefinedOrNull(Klass, name, message, code, (instance, even) => {
361
            let exp;
362
            if (even) {
363
                exp = name + ": " + Klass.prototype.message + '.';
364
            } else {
365
                exp = Klass.prototype.name + ": " + Klass.prototype.message + '.';
366
            }
367
            expect(instance.toString()).toEqual(exp);
368
        }, (instance, even1, even2) => {
369
            let exp;
370
            exp  = (even1 ? name : Klass.prototype.name) + ':';
371
            exp += ' ' + (even2 ? message : Klass.prototype.message) + '.';
372
            expect(instance.toString()).toEqual(exp);
373
        }, (instance, even1, even2, even3) => {
374
            let exp;
375
            exp  = (even1 ? name : Klass.prototype.name);
376
            exp += (even3 ? " (0x" + code.toString(16) + "):" : ':') + ' ';
377
            exp += (even2 ? message : Klass.prototype.message) + '.';
378
            expect(instance.toString()).toEqual(exp);
379
        }, false);
380
    });
381
382
    it("#native()", () => {
383
        instanceDefinedOrNull(Klass, name, message, code, (instance) => {
384
            const exp = Klass.prototype.message;
385
            expect(instance.native()).toEqual(new Error(exp));
386
        }, (instance, even1, even2) => {
387
            const exp = (even2 ? message : Klass.prototype.message);
388
            expect(instance.native()).toEqual(new Error(exp));
389
        }, (instance, even1, even2) => {
390
            const exp = (even2 ? message : Klass.prototype.message);
391
            expect(instance.native()).toEqual(new Error(exp));
392
        }, false);
393
    });
394
395
}