This post goes through how to use Object.defineProperty
to mock how constructors create methods, ie. non-enumerable properties that are functions.
The gist of Object.defineProperty
use with a function value boils down to:
const obj = {}
Object.defineProperty(obj, 'yes', { value: () => Math.random() > .5 })
console.log(obj) // {}
console.log(obj.yes()) // false or true depending on the call :D
As you can see, the yes
property is not enumerated, but it does exist. That's great for setting functions as method mocks.
It’s useful to testing code that uses things like Mongo’s ObjectId
. We don’t want actual ObjectIds strewn around our code. Although I did create an app that allows you generate ObjectId compatible values (see it here Mongo ObjectId Generator).
All the test and a quick explanation of what we’re doing and why we’re doing it, culminating in our glorious use of Object.defineProperty
, is on GitHub github.com/HugoDF/mock-mongo-object-id. Leave it a star if you’re a fan 🙂 .
Testing Mongo ObjectId
You use them in your persistence layer, you usually want to convert a string to an ObjectId using the ObjectId()
constructor
See the following snippet:
const { ObjectID, MongoClient } = require('mongodb')
const mongoClient = new MongoClient()
async function getUserIdFromSession(sessionId) {
const session = await (await mongoClient.connect()).collection('sessions').findOne({
_id: ObjectId(sessionId)
});
return session.userId && session.userId.toString();
}
A naive mock
An naive mock implementation would be:
const mockObjectId = data => data;
We’re relying on the fact that the .toString
method exists on strings:
'myString'.toString() // 'myString'
The issue is that it’s not an object, so it behaves differently
A better mock
So those are our 3 requirements:
-
toString()
should exist and return whatever’s passed into the constructor - it should be an object
-
ObjectId('a')
should deep equalObjectId('a')
const test = require('ava')
const mockObjectId = data => {
return {
name: data,
toString: () => data
};
}
test("toString() returns right value", t => {
t.is(mockObjectId("foo").toString(), "foo");
});
test("it's an object", t => {
const actual = mockObjectId("foo");
t.is(typeof actual, "object");
});
test.failing("two objectIds with same value are equal", t => {
const first = mockObjectId("foo");
const second = naiveObjectId("foo");
t.deepEqual(first, second);
});
Failure:
Difference:
{
name: 'foo',
- toString: Function toString {},
+ toString: Function toString {},
}
toString
is a new function for each mock instance… which means they’re not deep equal.
The right mock
const test = require("ava");
const mockObjectId = data => {
const oid = {
name: data
};
Object.defineProperty(oid, "toString", {
value: () => data
});
return oid;
};
test("toString() returns right value", t => {
t.is(mockObjectId("foo").toString(), "foo");
});
test("it's an object", t => {
const actual = mockObjectId("foo");
t.is(typeof actual, "object");
});
test("two objectIds with same value are equal", t => {
const first = mockObjectId("foo");
const second = mockObjectId("foo");
t.deepEqual(first, second);
});
How Object.defineProperty saved our bacon
It’s about enumerability. We want to mock an Object, with methods on it, without the methods appearing when people enumerate through it.
Object.defineProperty
allows you to set whether or not the property is enumerable, writable, and configurable as well as a value or a get/set (getter/setter) pair (see MDN Object.defineProperty).
There are 2 required descriptor (configuration) values: configurable
(if true, the property can be modified or deleted, false by default), enumerable
(if true, it will show during enumeration of the properties of the object, false by default).
There are also some optional descriptor (configuration) values: value
(value associated with property, any JS type which includes function), writable
(can this property be written to using the assignment operator, false by default), get
and set
(which are called to get and set the property).
This is an example call:
const o = {}
Object.defineProperty(
o,
'me',
{
value: 'Hugo',
writable: false, // default: false
}
)
console.log(o.me) // 'Hugo'
All the code is up at github.com/HugoDF/mock-mongo-object-id. Leave it a star if you’re a fan 🙂.