Mocking objects with ES6 Proxies

Mocking objects with ES6 Proxies

All we developers know how important unit tests are. Whether you write them first, as TDD suggests, or you write them after writing the code to be tested, unit tests help you to reason about the functionality you are going to implement and prevent to introduce regressions when you need to change your code. Pure functions and components are the ideal way to implement testable code, but you know that not always it is possible: dependencies, side effects, state are a necessary evil.

In this article, you will learn how to tackle dependencies in your unit tests by using one of the best-known technique: mocking. However, you will learn how to implement mocks in your unit tests without depending on any external library, but by using the Proxy class introduced by ECMAScript 2015 (also known as ES6) specifications.

Unit tests and dependency

According to its name, unit testing requires you to test a unit of code. What is a unit of code is questionable, but commonly it is considered as the smallest part of your application responsible for one functionality. So, for example, the following function can be considered a unit of code since it has only one task, that is to calculate the factorial of a number:

js
function factorial(n) { return n ? n * factorial(n - 1) : 1; }

Writing a unit test for this function is not so difficult. For example, by using a well-known JavaScript unit test framework like Mocha and the assertion library Chai, you could write the following code:

js
describe("Factorial test", function() { it("Factorial of 0 should be 1", function(){ factorial(0).should.equal(1) }); it("Factorial of n should be n*factorial(n-1)", function(){ const randomNumber = Math.floor(Math.random()*(20-1)+1); factorial(randomNumber) .should.equal(randomNumber*factorial(randomNumber-1)) }); );

As per Mocha specifications, the describe() function allows you to define a test suite, while the it() function allows you to define your unit test.

The two unit tests defined above respectively verify the factorial of 0 and the factorial of a randomly generated integer between 1 and 2. They are reasonably sufficient to demonstrate that the code of the factorial() function is correct. You can try this code on CodePen.

However, things become more complicated when the unit of code you need to test relies on other components.

Consider, for example, the following code:

js
function USDtoEUR(amount) { fetch(`http://dummyexchangeservice.com/exchangerate/usd/eur`) .then(function(response) { return response.json(); }) .then(function(exchange) { return amount * exchange.rate; }); }

This function converts an amount of US dollars into the corresponding amount of euro. Its calculation is very simple to test, but it relies on an external service in order to retrieve the exchange rate. To test the calculation made by this function, you should implicitly test the external service, with all potential problems that may arise, such as network issues, deserialization errors and so on. But you just want to focus on the calculation of your function, not on the external service.

This is just an example of code with dependency. You can easily find similar examples when your code uses the file system or accesses a database or manipulates the DOM and so on. So, how can you test your code without testing its dependencies?

Stub, mocks and friends

A common approach to test code with dependencies, avoiding to also test the external components, is by simulating the external component itself. This approach gives you the opportunity to control the behaviour of the external component and to isolate the functionality you want to test.

In JavaScript world, you have plenty of libraries offering the ability to simulate an external component. Among the others, you can use Sinon.js or JsMock or testdouble.

These libraries usually offer several ways to simulate an external component, depending on how you want or need to simulate it.

For example, most of them offer support for the following type of simulation objects:

  • Fake This is an object implementing a fixed set of answers to common requests, simulating the original component but independently from the logic of a test
  • Stub It’s an object implementing the minimal set of methods and/or properties to simulate a component for a set of tests
  • Mock This object simulates a component by implementing just the methods and/or properties involved in a single test

In one of his articles, Martin Fowler explains more in depth the differences among these and other types of simulation objects.

Despite this variety of choice, mocking is the most common approach to simulate an external component in unit tests. As said before, with this approach you implement on the fly just the minimal object interface of the external component required by your code to pass the test. Mock support provided by most libraries is very handy. But do you really need a third-party library to create the mocks you want to implement for your tests? Actually, you could do it without external libraries. In fact, you can write your own mocks by using the Proxy class.

Introducing the Proxy class

The Proxy class allows you to create objects able to intercept access to other objects and optionally to change their default behaviour. Let’s explain this definition with an example.

Suppose you want to track on the console any access to the members of an object. As a first step, you have to define a handler as shown below:

js
const handler = { get(target, propertyName) { console.log("Reading property " + propertyName); return target[propertyName]; }, set(target, propertyName, value) { console.log("Assigning " + value + " to property " + propertyName); target[propertyName] = value; } };

The handler is a plain object with two methods, get() and set(), intercepting respectively the reading and writing accesses to the properties of the object we want to monitor. The handler methods are called traps and allow you to intercept accesses to the target object.

Once you’ve defined the handler, you can create the proxy of an object as shown in the following:

js
let user = {firstName: "John", lastName: "Smith"}; let proxiedUser = new Proxy(user, handler);

Now, any access to the proxiedUser object will be tracked on the console and it will take effect on the original object. So, for example, you will be able to get the following results:

js
let name = proxiedUser.name; //console: Reading property name proxiedUser.name = "Jim"; //console: Assigning Jim to property name console.log(user.name); //console: Jim

Notice that the assignment of a value to the proxied object will be reflected on the original object, since so you defined it in the set() trap.

Of course, this is a very simple case to illustrate the basic principle of using the Proxy class. You can define your fancy traps to define the behaviour you need. For example, in this case, we want to keep the default behaviour of the target object, but in general, we can return or assign to the target’s property any value, thus changing the standard behaviour.

In addition to get() and set(), you can use other traps to make advanced customizations. See the Proxy documentation for more details.

Using proxies to implement mocks

Now that you know the Proxy class, how can you exploit it to implement mocks in your unit tests? Let’s try to explain by starting with an example. Consider the following function:

js
function toggleElement(document, elementId) { const element = document.getElementById(elementId); if (element) { if (element.className === "active") { element.className = "inactive"; } else { element.className = "active"; } } return element; }

This function toggles the status of a DOM element by changing its CSS class from active to inactive and vice versa. Of course, this function depends on the document object that allows you to manipulate the DOM. Since you want to test the main responsibility of your function, i.e. toggling the CSS class of an element, you need to simulate the document object so that you can control it.

In order to test the toggleElement() function, you don’t need to mock the whole document object. Since you just use the getElementById() method, you need to mock only this method. So let’s define a handler like the following:

js
const handler = { get(target, propertyName) { if (propertyName === "getElementById") { return (id) => { return {className: "inactive"} }; } } };

This handler intercepts the getElementById() method and replaces it with an arrow function returning a plain object with the className property set to the “inactive” value. You can see we are mocking the minimum stuff for the test we are going to implement: to verify that the toggleElement() function changes to “active” the value of the element’s property className when it is “inactive”.

With the definition of this handler, you can create the mock for your test and implement it as follows:

js
const proxiedDocument = new Proxy(document, handler); const element = toggleElement(proxiedDocument, "inactiveElement"); should.exist(element); element.className.should.equal("active");

As you can see, the implementation is quite straightforward. You created the proxied object and passed it to the toggleElement() function. Then you used a few assertions to verify that the function actually returned the modified element and that the value of the className property is “active”.

Of course, in order to test the opposite test case, you need to mock the getElementById() with a different behaviour. However, the implementation is very similar. I bet you can easily do it by yourself. Anyway, you can find the complete test cases on CodePen.

A Proxy-based mocking function

As you saw, mocking an object in a unit test requires you to define a handler each time and to create a Proxy for the object by using that handler. That’s a lot of repetitive code making the tests less readable and more error-prone.

How about a function hiding all the code needed to mock an object via the Proxy class? I mean a function that lets you write the body of a test like the following:

js
const proxiedDocument = mock(document, {getElementById: (id) => { return {className: "inactive"} }}); const element = toggleElement(proxiedDocument, "inactiveElement"); should.exist(element); element.className.should.equal("active");

Here, the mock() function does all the needed work to set up your mock.

Well, you can define your mock() function like the one shown below:

js
function mock(object, mockedInterface) { const handler = { mockedInterface, get(target, propertyName) { const mockedMember = this.mockedInterface[propertyName]; return mockedMember? mockedMember : target[propertyName]; } }; return new Proxy(object, handler); }

The function takes two arguments: the object to be mocked and an object containing the definitions of the members you want to mock, the mocked interface. It defines a handler having a new custom property: the mockedInterface property. Remember that a handler is just a plain object. Notice that the handler’s mockedInterface property is directly mapped to the mockedInterface argument. This property acts as a repository for the definitions of the target object members that will be replaced at runtime. In fact, when an attempt to access the mocked object property is made, the get() method returns a mocked definition for that member, if it exists, otherwise it returns the original member. As you can see, this simplifies a lot the code for a unit test.

You can find the example of the unit tests using the mock() function on CodePen.

Summary

In conclusion, in this article you’ve seen that writing unit tests for functionalities that rely on other components is quite difficult, and particularly it requires you to implicitly test functionalities that are not specific of the unit of code you are going to test. You learned that the common way to test functionalities with dependencies is to simulate the external component. You saw that different types of object simulation exist, but mocking is the most common approach. You also learned how to use the Proxy class and how to use it to implement a mock for an object in your unit tests.

Read similar articles