2.5 TDD Example: Object.defineProperty for object mocking

This section will go 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() > 0.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.

We’ll test-drive the development of a Mock Mongo ObjectId instance and show how we can leverage Object.defineProperty to create mock object instances with properties that aren’t enumerated.

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: 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 mockObjectId = data => {
  return {
    name: data,
    toString: () => data
  };
};

test('toString() returns right value', () => {
  expect(mockObjectId('foo').toString()).toEqual('foo');
});
test("it's an object", () => {
  const actual = mockObjectId('foo');
  expect(typeof actual).toEqual('object');
});
test('two objectIds with same value are equal', () => {
  const first = mockObjectId('foo');
  const second = mockObjectId('foo');
  expect(first).toEqual(second);
});

We get a failure on the last test, note that we need to override the testRegex since this is a failing test, it’s been stored under src/02.05-mock-object-id.failing.js which doesn’t match Jest’s default test matching regular expression.

npx jest --testRegex ".*\.failing\.js" src/02.05-mock-object-id.failing.js
 FAIL  src/02.05-mock-object-id.failing.js
  ✓ toString() returns right value (4ms)
  ✓ it's an object
  ✕ two objectIds with same value are equal (11ms)

  ● two objectIds with same value are equal

    expect(received).toEqual(expected) // deep equality

    Expected: {"name": "foo", "toString": [Function toString]}
    Received: serializes to the same string

      16 |   const first = mockObjectId('foo');
      17 |   const second = mockObjectId('foo');
    > 18 |   expect(first).toEqual(second);
         |                 ^
      19 | });
      20 |

      at Object.<anonymous> (src/02.05-mock-object-id.failing.js:18:17)

Test Suites: 1 failed, 1 total
Tests:       1 failed, 2 passed, 3 total

Our issue is Expected: {"name": "foo", "toString": [Function toString]}, what is happening is that toString is a new function for each mock instance… which means they’re not equal (by reference).

The right mock

const mockObjectId = data => {
  const oid = {
    name: data
  };
  Object.defineProperty(oid, 'toString', {
    value: () => data
  });
  return oid;
};

test('toString() returns right value', () => {
  expect(mockObjectId('foo').toString()).toEqual('foo');
});
test("it's an object", t => {
  const actual = mockObjectId('foo');
  expect(typeof actual).toEqual('object');
});
test('two objectIds with same value are equal', () => {
  const first = mockObjectId('foo');
  const second = naiveObjectId('foo');
  expect(first).toEqual(second);
});

Which gives us a green test.

npx jest src/02.05-mock-object-id.test.js
 PASS  src/02.05-mock-object-id.test.js
  ✓ toString() returns right value (4ms)
  ✓ it's an object (1ms)
  ✓ two objectIds with same value are equal (1ms)

Test Suites: 1 passed, 1 total
Tests:       3 passed, 3 total

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'

This section has gone through how we can create mocks using Object.defineProperty.

The next section looks at how to mock out JavaScript module imports.

Jump to table of contents