/ #Node #jest 

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

Looking for a new job? Take Triplebyte’s quiz and have top tech companies pitch you!

Author

Hugo Di Francesco

A Software Engineer and recovering Frontend Engineer who is big on Node.js, queues and Vue(s). He shares practical JavaScript tips for the developer who wants to get things done on Code with Hugo. University College London (UCL), MEng Mathematical Computation Graduate.

Subscribe for Enterprise Node.js and JavaScript Guides

Build your web platform with modern Node.js/JavaScript best-practices, tools and patterns