Skip to main content

Plugin

mocktomata tries to handle most use cases out of the box.

But there could be cases that the code you use has certain semantics that mocktomata could not handle.

In those cases, you can either create an adapter, or write a plugin to handle those cases.

Inside mocktomata, all JavaScript syntax are handled by internal plugins.

Package, Module, and Plugins

The word "plugin" typically refers to a plugin package (e.g. a eslint-plugin-harmony is a plugin package), and the plugin itself. To make sure we are clear about what are we referring to, consider this:

A plugin package exports a plugin module which register one or more plugins.

For example, you need to write a plugin for node-fetch (you don't have to, we just use it as an example).

You will create a package like mocktomata-plugin-node-fetch, which expose a PluginModule, which register a node-fetch plugin:

// `mocktomata-plugin-node-fetch/ts/index.ts`
import nodeFetchPlugin from './nodeFetchPlugin'

export default {
activate(context) {
context.register(nodeFetchPlugin)
}
}

And you use it by adding it to your configuration:

// mocktomata.json
{
"plugins": ["mocktomata-plugin-node-fetch"]
}

To make your plugin more discoverable, you can add the keyword mocktomata-plugin in your package.json:

// package.json
{
"keywords": [
"mocktomata-plugin"
]
}

PluginModule

Your plugin should expose a PluginModule as its default export.

The PluginModule contains a single method: activate():

import type { Mocktomata } from 'mocktomata'

export default {
activate(context: Mocktomata.ActivationContext): void { }
}

activate()

The activate() method will be called when the plugin is loaded.

The Mocktomata.ActivationContext it receives contain a register() function, which you can use to register one or more SpecPlugin:

import type { Mocktomata, SpecPlugin } from 'mocktomata'
import { pluginA, pluginB } from './plugins'

export default {
activate({ register }: Mocktomata.ActivationContext) {
register(pluginA)
register(pluginB)
}
}

SpecPlugin

The SpecPlugin is the type of a plugin:

export type SpecPlugin<S = any, M = any> = {
/**
* Name of the plugin. This is needed only if there are multiple plugins in a package.
*/
name?: string,
/**
* Indicates if the plugin can handle the specified subject.
*/
support(subject: unknown): boolean,
/**
* Creates a spy that captures the interactions with the specified subject.
* @param context Provides tools needed to record the subject's behavior.
* @param subject The subject to spy.
*/
createSpy(context: SpecPlugin.SpyContext<M>, subject: S): S,
/**
* Creates a stub in place of the specified subject.
* @param context Provides tools needed to reproduce the subject's behavior.
* @param meta Meta data of the subject.
* This is created in `createSpy() -> record.declare()` and is used to make the stub looks like the subject.
*/
createStub(context: SpecPlugin.StubContext, subject: S, meta: M): S,
}

The name property is optional, if your plugin package only has one plugin. In that case, the package name will be used as the name of the plugin.

If you have more than one plugins in your plugin package, specify the name and each plugin will be named as packageName/name.

The support() function is where you tell mocktomata that your plugin can handle the specific spec subject.

createSpy() should return a spy that captures the behavior.

The SpecPlugin.SpyContext contains a few functions for you to capture various behaviors.

The createStub() should return a stub that simulate the behavior.

Like SpecPlugin.SpyContext, SpecPlugin.StubContext contains a few functions for you to simulate various behaviors.

Example: objectPlugin

Now, let's take a look at the implementation of objectPlugin to understand how it works:

export const objectPlugin: SpecPlugin<Record<string | number, any>, string> = {
name: 'object',
support: subject => subject !== null && typeof subject === 'object',
createSpy: ({ getProperty, setProperty, setMeta }, subject) => {
setMeta(metarize(subject))
return new Proxy(subject, {
get(_: any, key: string) {
return hasProperty(subject, key)
? getProperty({ key }, () => subject[key])
: undefined
},
set(_, key: string, value: any) {
return setProperty({ key, value }, value => subject[key] = value)
}
})
},
createStub: ({ getProperty, setProperty }, _, meta) => {
return new Proxy(demetarize(meta), {
get(_: any, key: string) {
return getProperty({ key })
},
set(_, key: string, value: any) {
return setProperty({ key, value })
}
})
}
}

For objectPlugin, the name is needed because it is an internal plugin. You can ignore that.

The support() shows that it work with object type except null.

In createSpy(), it set the metadata of the subject using setMeta().

The metarize() and demetarize() are just some internal serialization function. You can set the metadata to whatever you want, as long as it is JSON compatible.

Then it returns a Proxy and handles the get() and set() requests. It returns undefined if the subject does not have that property, so that it won't record some noise not related to the subject.

In createStub(), it recreate the base of the Proxy by deserializing the metadata, and replay the get() and set() calls as they happens.

So you can see that it is not very complicated to write a plugin.

Of course, there are cases it will be more complicated than objectPlugin, say for example you need to deal with some asynchronous calls as in promisePlugin.

If you have any questions, feel free to ask in the discussions