Create a proxy which can mock any object, function, class, etc. with infinite depth and combinations.
- About
- Installation & Importing
- Examples
- API Documentation
recursiveProxyMock([overrides]) => Proxy
ProxySymbol
hasPathBeenVisited(proxy, path) => boolean
hasPathBeenCalledWith(proxy, path, args) => boolean
getVisitedPathData(proxy, path) => ProxyData[] | null
resetMock(proxy)
replayProxy(proxy, target)
listAllProxyOperations(proxy) => ProxyData[]
listAllProxyPaths(proxy) => ProxyPath[]
- ProxyPath
- Caveats
- More examples
- Browser/Node Support
- Performance & Size
- Updates and Maintenance
This is an easy-to-use library which enables you to instantly mock anything. Any properties, functions, classes, etc will be instantly mocked with one line. Useful when you need to provide a mock, but don't care about the implementation. With this library you can then do many other advanced things such as: overriding certain operations, inspect what operations occurred, replay all operations onto another object, and more!
Recursive Proxy Mock is a JavaScript Proxy that can handle literally anything. This is best explained with examples. Read on!
NOTE: This library is not published to NPM as it turns out the use-cases for this are few and far between. If you need it though, builds are available for download via GitHub Releases. You can
npm install
that with a path to the locally downloaded.tgz
file.
import { recursiveProxyMock } from "recursive-proxy-mock";
// OR
const { recursiveProxyMock } = require("recursive-proxy-mock");
import { recursiveProxyMock } from "recursive-proxy-mock";
const mock = recursiveProxyMock();
// Access any properties on the mock object
mock.a.b.c;
// Call any functions
mock.d().test();
// Set any properties
mock.person.name = "Jason";
// Construct any classes
new mock.MyClass();
// Or do any combination
const value = mock.getThing("yes", 42);
value.something.else = true;
value.something.else().and.now().val = "WOW";
import { recursiveProxyMock, ProxySymbol } from "recursive-proxy-mock";
const mock = recursiveProxyMock([
{
path: ["person", "name"],
value: "Jason",
},
{
path: ["person", "greet", ProxySymbol.APPLY],
value: (name) => {
console.log(`Hi ${name}`);
},
},
{
path: ["a", "b", "c", ProxySymbol.CONSTRUCT, "d", ProxySymbol.APPLY, "e", "f"],
value: 123,
},
]);
// Normal behavior is still preserved
mock.anything.else.something.value().exists;
// When an override path is matched, that value is used instead
console.log(mock.person.name); // "Jason"
// An override will "bail out" from the mock
console.log(mock.person.name.length); // 5
// You can override any of the Proxy traps (eg: function calls, constructor, etc)
mock.person.greet("Phil"); // "Hi Phil"
// You can infinitely chain overrides to access deep properties
console.log(new mock.a.b.c().d().e.f); // 123
import { recursiveProxyMock, hasPathBeenVisited, ProxySymbol } from "recursive-proxy-mock";
const mock = recursiveProxyMock();
mock.a.b.c("hi", true, 7);
// Check if a path has ever been accessed on a mock object
if (hasPathBeenVisited(mock, ["a", "b", "c", ProxySymbol.APPLY])) {
console.log("Yes it has!");
}
// Get the details about every time a path was visited on a mock object
const pathData = getVisitedPathData(mock, ["a", "b", "c", ProxySymbol.APPLY]);
/*
pathData = [
{
args: ["hi", true, 7],
...etc
}
]
*/
// Check specifically for call arguments
if (hasPathBeenCalledWith(mock, ["a", "b", "c", ProxySymbol.APPLY], ["hi", true, 7])) {
console.log("Yup!");
}
import { recursiveProxyMock, replayProxy } from "recursive-proxy-mock";
const mock = recursiveProxyMock();
// Queue up operations on a mock
mock.metrics.pageLoad(Date.now());
mock.users.addUser("name");
// Sometime later once the module is loaded
replayProxy(mock, apiModule);
Main function to create the recursive proxy mock object.
overrides
- [optional] - an array of objects that containpath
andvalue
.path
- see the ProxyPath section for more details.value
- the value to return instead of another recursive proxy. This needs to match the type of the path. So if your path ends inProxySymbol.APPLY
this value must be a function which will be called with whatever arguments the proxy was called with.
See Example Above for more details.
This function takes one generic type which will be the return type of the proxy. This is not required and the return value will be any
by default.
type MyObject = {
name: string;
details: {
value: number;
};
};
// TypeScript thinks that the mock has the shape of `MyObject`
const myObjectMock = recursiveProxyMock<MyObject>();
Object containing a Symbol for each of the Proxy Handler functions. All handlers are supported and have the same name as the Proxy handler function but in UPPER_CASE
. The only exception is GET
which is the default when no symbol is specified. All property access on an object is a GET
by default.
See the Proxy Handler functions documentation for a list of all methods, names, and when they are each used.
These symbols are used to construct paths for the following functions:
-
recursiveProxyMock
- thepath
property of the override object -
hasPathBeenVisited
- thepath
argument to check if that has ever been visited -
getVisitedPathData
- thepath
argument to query path data for all visits to that path -
There is also a special
ProxySymbol.WILDCARD
which can be used to match 0+ path segments. This is especially useful when mocking a chainable library where the same method can be called with many different paths. So the path forclick()
in$("div").css("color", "blue").click()
could be expressed as[ProxySymbol.WILDCARD, "click", ProxySymbol.APPLY]
.- Do not include a wildcard at the end of the path, only the beginning or middle.
-
You can also use the built-in
Symbol.toPrimitive
which can appear as theprop
of theGET
proxy handler.
Function to check if a certain path was ever visited. Useful in conjunction with test assertions.
proxy
- the root proxy object that was returned fromrecursiveProxyMock
path
- see the ProxyPath section for more details.- Returns:
true
if the specified path has ever been visited on the proxy object,false
if not.
Function to check if a certain path was ever called (as a function or class constructor). Useful in conjunction with test assertions. Very similar to hasPathBeenVisited
but specific for method calls and designed to be easier to use than parsing getVisitedPathData
yourself.
proxy
- the root proxy object that was returned fromrecursiveProxyMock
path
- see the ProxyPath section for more details. The path must end inProxySymbol.APPLY
orProxySymbol.CONSTRUCT
.args
- an array of arguments that should have been passed to the specified path- Returns:
true
if the specified path has ever been called on the proxy object,false
if not.
Function to get details about every time a path was visited. Useful in conjunction with test assertions to get the number of visits, arguments passed, etc.
proxy
- the root proxy object that was returned fromrecursiveProxyMock
path
- see the ProxyPath section for more details.- Returns: Array of
ProxyData
objects, one for each time the path was visited on the proxy object.null
if it was never visited.
A ProxyData
object contains any relevant details about the operation. For example SET
will contain the name of the prop
that was assigned to and the value
that was assigned to it. Or CONSTRUCT
will contain the array of args
that were passed to the constructor.
APPLY
,CONSTRUCT
:args
- array of arguments that were passed to the function or constructor
DELETE_PROPERTY
,GET_OWN_PROPERTY_DESCRIPTOR
,HAS
,GET
:prop
- the name of the property which was accessed/operated on
SET
:prop
- the name of the property which was accessed/operated onvalue
- value that was assigned to theprop
DEFINE_PROPERTY
:prop
- the name of the property which was accessed/operated ondescriptor
- the descriptor object that was passed to theObject.defineProperty
call. Descriptor documentation.
- All other handlers:
- No useful additional information is available
Resets the internally tracked proxy operations. See the Mock a dependency using Jest example for a common usage of this method.
proxy
- the root proxy object that was returned fromrecursiveProxyMock
Replay every operation performed on a proxy mock object onto a target object. This can effectively let you time travel to queue up any actions and replay them as many times as you would like. Every property accessor, every function call, etc will be replayed onto the target.
proxy
- the root proxy object that was returned fromrecursiveProxyMock
target
- any object/function/class etc which will be operated on in the same way that theproxy
object was.
A debug function which lists the raw "proxy stack" of every operation that was performed on the mock. This is an array of ProxyData objects which have metadata that is used to power all of the other functions. For example, every object has a parent
property which contains a number. This number will be the same as some other object's self
property. Using those two values you can construct a tree containing every path that was accessed on the object.
This is exposed primarily for debugging or curiosity and shouldn't be relied on. If you find yourself needing to use the data here, create an Issue explaining your use-case and we may add a function to support that directly.
proxy
- the root proxy object that was returned fromrecursiveProxyMock
- Returns: Array of ProxyData objects for every operation that was performed on the mock.
A debug function which lists every path and sub-path that was visited on the mock. This is an array of ProxyPath arrays which is useful to manually inspect what operations took place and find the correct paths to use for the other APIs.
proxy
- the root proxy object that was returned fromrecursiveProxyMock
- Returns: Array of ProxyPath arrays with one entry for every path or sub-path that was visited.
Whenever a method accepts a path
it is an array of properties and symbols to define a request path on the mock object. (See ProxySymbol for more details.)
- Examples:
mock.test.abc
=>["test", "abc"]
mock.test.abc()
=>["test", "abc", ProxySymbol.APPLY]
mock.a()().b = 7
=>["a", ProxySymbol.APPLY, ProxySymbol.APPLY, "b", ProxySymbol.SET]
delete mock.prop
=>["prop", ProxySymbol.DELETE_PROPERTY]
new mock.obj.Car
=>["obj", "Car", ProxySymbol.CONSTRUCT]
- You cannot override
ProxySymbol.IS_EXTENSIBLE
,ProxySymbol.PREVENT_EXTENSIONS
, orProxySymbol.GET_PROTOTYPE_OF
. Their values are fixed as they must always match the proxy target.
JSDOM doesn't implement the Canvas element so if you are testing code that is drawing on a canvas, it'll crash as soon as it tries to interact with the canvas context. You can use recursiveProxyMock
to mock every method/property on the context for both 2d and WebGL. None of them will do anything, but the code will no longer crash and you can assert that the required functions were called.
import { recursiveProxyMock, hasPathBeenVisited, ProxySymbol } from "recursive-proxy-mock";
const mock = recursiveProxyMock();
global.HTMLCanvasElement.prototype.getContext = () => mock;
const canvas = document.createElement("canvas");
const context = canvas.getContext("webgl"); // JSDOM doesn't implement this
context.clear(context.COLOR_BUFFER_BIT); // This would normally crash
// Check that `context.clear` has been called
hasPathBeenVisited(mock, ["clear", ProxySymbol.APPLY]);
import { recursiveProxyMock, resetMock } from "recursive-proxy-mock";
const mockInstance = recursiveProxyMock();
beforeEach(() => {
resetMock(mockInstance);
});
jest.doMock("my-dependency", () => {
return mockInstance;
});
import { recursiveProxyMock } from "recursive-proxy-mock";
const $ = recursiveProxyMock();
$("div").append("<p>Content</p>").css("color", "blue").click();
import { recursiveProxyMock, ProxySymbol, hasPathBeenVisited } from "recursive-proxy-mock";
function logoutHandler(req, res) {
req.session.destroy(() => {
res.redirect("/");
});
}
const req = recursiveProxyMock([
{
path: ["session", "destroy", ProxySymbol.APPLY],
value: (callback) => {
callback();
},
},
]);
const res = recursiveProxyMock();
logoutHandler(req, res);
// Assert that res.redirect() has been called
expect(hasPathBeenVisited(res, ["redirect", ProxySymbol.APPLY])).toStrictEqual(true);
Out of the box we support all modern browsers and any currently maintained version of Node. Unfortunately Proxy cannot be polyfilled, so supporting a browser like Internet Explorer is completely out of the question.
It's important to note that Proxies are far slower than most alternatives. We wouldn't recommend to use this for performance-critical code. The library is heavily tree-shakable so the average bundle size will be just a few KBs.
This library is "done". Unless there are bugs to fix or important features being requested, I have no plans to keep updating it. It solves a problem (albeit an uncommon one) and if you need it, it should work for you as-is. No need for unnecessary updates. :)