Completed
Push — master ( 1c64ed...ec7ccd )
by Esaú
01:44
created

hierarchy-helper.js ➔ instanceDefinedOrNull   B

Complexity

Conditions 10
Paths 4

Size

Total Lines 20

Duplication

Lines 0
Ratio 0 %

Importance

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