test: add `common.mustNotMutateObjectDeep()`
Problem
tl;dr: usually we don't want core methods to have side effects of mutating input values.
Let's imagine function having the following flow inside:
function makeRequest(ipaddr, options) {
if (typeof options.port === 'number')
return _mkRq(ipaddr, options);
switch (options.protocol) {
case 'ftp': options.port = 21; break;
case 'http': options.port = 80; break;
default: // unknown, whatever
}
return _mkRq(ipaddr, options);
}
Let's try to use it:
const opts = { headers };
makeRequest(addr, opts); // unknown://addr:unknown --- ok
opts.protocol = 'http';
makeRequest(addr, opts); // http://addr:80 --- got side effects
opts.protocol = 'ftp';
makeRequest(addr, opts); // ftp://addr:80 --- suffered from side effects
To run into such things in tests, we need to perform specific calls in specific order, and it still doesn't guarantee catching. Simple tests like makeRequest(addr, Object.freeze({}))
won't help if mutation is conditional.
Solution
This PR adds common.mustNotMutate()
which improves our ability to test functions for undesired side effects.
Pass an object into it and use returned value in tests whenever it must remain unchanged:
...
opts.protocol = 'http';
makeRequest(addr, common.mustNotMutate(opts)); // http://addr:80 --- testing for side effects
AssertionError [ERR_ASSERTION]: Expected no side effects, got 80 assigned to port
Or make an immutable "view" on any object:
const mFoo = { bar: 'baz' };
const iFoo = mustNotMutate(mFoo);
mFoo.bar = 'qux'; // affects BOTH mFoo and iFoo
iFoo.bar = 'gwak'; // AssertionError
Or keep mutable "view" and make an object immutable:
const realProcessEnv = process.env;
process.env = mustNotMutate(process.env);
realProcessEnv.LANG = 'en_GB'; // affects BOTH realProcessEnv and process.env
process.env.LANG = 'en_GB'; // AssertionError
Example
[WIP] Using this function in some fs
tests: https://github.com/LiviaMedeiros/node/commit/48a8bd155eebe3510f581db95d47c553d4b22add