I have coded JavaScript short enough time to consider ‘use strict’ a mandatory and obvious feature of the language to use. I always use it unless I forget to.
A while ago I was aware of Object.freeze(). I have been thinking about different ways to exploit this (strict) feature for a while and I now have a very good use case: freeze indata in unit tests to ensure my tested functions don’t incidentally change indata (pure functions are good, pure functions don’t change indata, and it is hard to really guarantee a function in JavaScript is pure).
Imagine I am writing a function that calculates the average and I have a test for it.
const averageOfArray1 = (a) => {
let s = 0;
for ( let i=0 ; i<a.length ; i++ ) s+=a[i];
return s/a.length;
};
describe('test avg', () => {
it('should give the average value of 2', () => {
const a = [1,2,3];
assert.equal(2, averageOfArray1(a) );
});
});
If averageOfArray mutates its input, it would be a serious bug, and the above test would not detect it. Lets look at a different implementation:
const averageOfArray2 = (a) => {
for ( let i=1 ; i<a.length ; i++ ) a[0] += a[i];
return a[0]/a.length;
};
describe('test avg', () => {
it('should give the average value of 2', () => {
const a = [1,2,3];
assert.equal(2, averageOfArray2(a) );
});
});
Some genious “optimized” the function by eliminating an unnecessary variable (s), and the test still passes! However, if the tests where written:
describe('test loop', () => {
it('should give the average value of 2', () => {
const a = Object.freeze([1,2,3]);
assert.equal(2, averageOfArray2(a) );
});
})
the tests would fail! Much better. How do the tests fail? This is what I get:
1) test avg
should give the average value of 2:
AssertionError [ERR_ASSERTION]: 2 == 0.3333333333333333
+ expected - actual
-2
+0.3333333333333333
So it appears that the first element [0] of the array was never changed, thus the return value of 0.3333. But no exception was thrown. If I instead would ‘use strict’ for the entire code:
'use strict';
const assert = require('assert');
const averageOfArray2 = (a) => {
for ( let i=1 ; i<a.length ; i++ ) a[0] += a[i];
return a[0]/a.length;
};
describe('test avg', () => {
it('should give the average value of 2', () => {
const a = Object.freeze([1,2,3]);
assert.equal(2, averageOfArray2(a));
});
});
instead I get:
1) test avg
should give the average value of 2:
TypeError: Cannot assign to read only property '0' of object '[object Array]'
at averageOfArray2 (avg.js:12:45)
at Context.it (avg.js:20:25)
which is what I really wanted.
So it APPEARS to me that without ‘use strict’ the frozen object is not changed, but changing it just fails silently. With ‘use strict’ I get an exception right way, which leads me to the question where I can put use strict? This is what I found:
// 'use strict'; // GOOD
const assert = require('assert');
// 'use strict'; // BAD
const averageOfArray2 = (a) => {
// 'use strict'; // GOOD
let i;
// 'use strict'; // BAD
for ( i=1 ; i<a.length ; i++ ) a[0] += a[i];
return a[0]/a.length;
};
describe('test avg', () => {
// 'use strict'; // BAD
it('should give the average value of 2', () => {
const a = Object.freeze([1,2,3]);
assert.equal(2, averageOfArray2(a));
});
});
That is, ‘use strict’ should be in place where the violation actually takes place. And ‘use strict’ must be placed first in whatever function it is placed, otherwise it is silently ignored! This is probably well known to everyone, but it was not to me.
Conclusion
Object.freeze() is very useful for improved unit tests. However, you should use it together with properly placed ‘use strict’ and that is in the function begin tested (not only the unit test).
And note, if you have done Object.freeze in a unit test, and someone refactors the tested function in a way that it both:
- Mutates the frozen object
- Removes or moves ‘use strict’ to an invalid place
your unit tests may still pass, even though the function is now very dangerous.
0 Comments.