(Updated: )
/ #node #ava #Testing 

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 🙂 .

Table of Contents

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-logorawpixel

Author

Hugo Di Francesco

Co-author of "Professional JavaScript", "Front-End Development Projects with Vue.js" with Packt, "The Jest Handbook" (self-published). Hugo runs the Code with Hugo website helping over 100,000 developers every month and holds an MEng in Mathematical Computation from University College London (UCL). He has used JavaScript extensively to create scalable and performant platforms at companies such as Canon, Elsevier and (currently) Eurostar.

Get The Jest Handbook (100 pages)

Take your JavaScript testing to the next level by learning the ins and outs of Jest, the top JavaScript testing library.