DEV Community

Cover image for JavaScript Object.defineProperty for a function: create mock object instances in Jest or AVA
Hugo Di Francesco
Hugo Di Francesco

Posted on • Originally published at codewithhugo.com on

JavaScript Object.defineProperty for a function: create mock object instances in Jest or AVA

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 equal ObjectId('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 🙂.

unsplash-logo
rawpixel

Latest comments (0)