Research
Security News
Threat Actor Exposes Playbook for Exploiting npm to Build Blockchain-Powered Botnets
A threat actor's playbook for exploiting the npm ecosystem was exposed on the dark web, detailing how to build a blockchain-powered botnet.
TypeScript
support includedflow
support includedspy4js provides a stand-alone spy framework. It is decoupled by any dependencies and other assertion frameworks.
spy4js exports only one object called Spy
. The spy instances
are treated as class instances and come with a lot of useful features. See below for more.
Hint: My favorite test framework is Jest. If you are using other frameworks you might get issues related to automatically applied test suite hooks. To overcome this default behaviour see here.
yarn add --dev spy4js
npm install --save-dev spy4js
A spy instance can be initialized differently.
import { Spy } from 'spy4js';
// initialize directly
const spy1 = new Spy();
// initialize directly and supply an identifier for debugging purpose (default: 'the spy')
const spy2 = new Spy('special spy for me');
// initialize by mocking another objects attribute (usually this attribute is a function)
const someObject1 = new Date(2017, 1, 15);
const spy3 = Spy.on(someObject1, 'toJSON');
// (spy name will be accordingly: "the spy on 'toJSON'")
// initialize many by mocking another objects attributes
const someObject2 = new Date(2017, 1, 15);
const someObject2$Mock = Spy.mock(someObject2, 'toJSON', 'toString', 'getDate');
// mock exported functions from other modules
const myModuleMocks = Spy.mockModule('./my-module', 'useMe');
Any spy instance can be configured by overriding the default configuration. For example if you want to configure all spies not to favor own "equals" implementations.
Spy.configure({useOwnEquals: false});
You may apply additional behaviour to every spy. The valid operations here are:
configure
(some external libraries may use own "equals" implementations in an unexpected way)calls
(does make the spy call the provided functions sequentially)returns
(does make the spy return the provided params sequentially)throws
(does make the spy throw an error when called)transparent
(does make the spy call the original method of a mocked object)transparentAfter
(does make the spy call the original method of a mocked object after a certain amount of made calls)reset
(resets the registered calls which were already made)restore
(does make the spy restore the mocked object)All those methods on a spy are designed in a builder pattern. So you may chain any of these configurations. But be aware that some behaviours override existing behaviours.
const spy = Spy.on(someObject, 'someMethod');
// configure it to use NOT own "equals" implementations
spy.configure({useOwnEquals: false});
// make it call any functions
spy.calls(func1, func2, func3);
someObject.someMethod(arg); // returns func1(arg)
someObject.someMethod(arg1, arg2); // returns func2(arg1, arg2)
someObject.someMethod(arg); // returns func3(arg)
someObject.someMethod(arg1, arg2, arg3); // returns func3(arg1, arg2, arg3) // sticks to the last
// make it return any values
spy.returns(value1, value2);
someObject.someMethod(arg); // returns value1
someObject.someMethod(arg1, arg2); // returns value2
someObject.someMethod(arg); // returns value2 // sticks to the last
// make it throw any message (the message is optional)
spy.throws('throw this');
someObject.someMethod(arg); // throws new Error('throw this')
// make it return always the current date and transparentAfter 2 calls
spy.calls(() => new Date()).transparentAfter(2);
someObject.someMethod(arg); // returns new Date()
someObject.someMethod(arg1, arg2); // returns new(er) Date()
someObject.someMethod(arg); // returns someObject.someMethod(arg) // sticks to this behaviour
// make it immediatly transparent
spy.transparent();
// make it reset
spy.reset();
// make it restore
spy.restore(); // other than "transparent" does not control input and output of the mocked function anymore
Even as important are the facts, we want to display:
wasCalled
(does display that the spy was called a specifiable amount of times)wasNotCalled
(does display that the spy was never called)wasCalledWith
(does display that the spy was called at least once like with the provided params)wasNotCalledWith
(does display that the spy was never like with the provided params)hasCallHistory
(does display that the spy was called with the following params in the given order)Those methods on a spy display facts. Facts have to be true, otherwise they will throw an Exception, which displays in a formatted debug message why the given fact was a lie. By writing those facts in your tests, a big refactoring loses its scare.
const spy = new Spy();
spy.wasNotCalled();
// in fact: you never want to call a spy directly for any purpose
// -> therefore using flow this line would complain
spy([1, 'test', {attr: [4]}]);
spy.wasCalled(); // called at least once
spy.wasCalled(1); // called exactly once
spy('with this text');
spy.wasCalled(2); // called exactly 2 times
// the spy was called at least once with equal params
spy.wasCalledWith([1, 'test', {attr: [4]}]);
// the spy was not called with those params
spy.wasNotCalledWith([1, 'test', {attr: [3]}]);
// the spy was called twice with the following params and in same order
spy.hasCallHistory([ [1, 'test', {attr: [4]}] ], 'with this text');
There is one static method that does restore all existing spies in all tests. This is extremely useful to clean up all still existing mocks and also a very comfortable to this automatically after every test (this is done by default).
restoreAll
(does restore every existing spy)Spy.restoreAll();
And also sometimes it is necessary to have access to some of the call arguments with which the spy was called.
getCallArguments
(returns all call arguments for a specified call in an array)getCallArgument
(same as getCallArguments, but returns only a single element of the array)getCallCount
(returns the number of made calls)const spy = new Spy();
// make some calls
spy('string', 1);
spy([1, 2, 3]);
spy();
spy(null);
spy.getCallArguments(/* default = 0 */); // returns ['string', 1]
spy.getCallArgument(/* defaults = (0, 0) */); // returns 'string'
spy.getCallArgument(0, 1); // returns 1
spy.getCallArguments(1); // returns [[1, 2, 3]]
spy.getCallArgument(1); // returns [1, 2, 3]
spy.getCallArguments(2); // returns []
spy.getCallArgument(2); // returns undefined
spy.getCallArguments(3); // returns [null]
spy.getCallArgument(3); // returns null
spy.getCallArguments(4); // throws Exception because less calls were made
spy.getCallArgument(4); // throws same Exception
The last method is showCallArguments
. It is mostly used internally to improve the
debug messages, but can be while you are in a console.log-mania.
Spy(spyName:string = 'the spy') => SpyInstance
The returned Spy instance has his own name-attribute (only) for debugging purpose.
Spy.configure(config: {
useOwnEquals?:boolean,
beforeEach?: (scope: string) => void,
afterEach?: (scope: string) => void,
}) => void
Using this function you may edit the default behaviour spy4js itself.
The scope param will contain the test-suite name, which was provided as first parameter
of the describe
function.
The configuration possibility are:
Spy.on(object:Object, methodName:string) => SpyInstance
Initializing a spy on an object, simply replaces the original function by a spy and stores the necessary information to be able to restore the mocked method.
If the attribute was already spied or is not a function, the Spy will throw an exception to avoid unexpected behaviour. You never want to spy other attributes than functions and for no purpose a spy should ever be spied.
Spy.mock(object:Object, ...methodNames: string[]) => Object (Mock)
Creating an object that references spies for all given methodNames.
Initialize as many spies as required for one and the same object. Only
after Spy.initMocks
gets called, the created mock does affect the given object.
Spy.mockModule(moduleName: string, ...methodNames: string[]) => Object (Mock)
Same as mock but only necessary if you want to mock exported functions.
Spy.initMocks(scope?: string) => void
Does initialize all global and scope-related mocks by applying spies. Mocks can be created with mock or mockModule. This function has not to be called manually, if you rely on the default test suite hooks.
Spy.restoreAll() => void
Does restore all mocked objects to their original state. See restore for further information. This function has not be called manually, if you rely on the default test suite hooks.
Spy.resetAll() => void
Does reset all existing spies. This effects even persistent spies. See reset for further information. This function has not be called manually in between different tests, if you rely on the default test suite hooks.
Spy.IGNORE = $Internal Symbol$
This object can be passed anywhere where you want the "wasCalledWith" or "hasCallHistory" to ignore that object or value for comparison.
spy({prop: 'value', other: 13}, 12);
spy.wasCalledWith(Spy.IGNORE, 12);
spy.wasCalledWith({prop: Spy.IGNORE, other: 13}, 12);
Spy.COMPARE(comparator: (arg: any) => boolean | void) => SpyComparator
This function can be called with a custom comparator and passed anywhere where you want the "wasCalledWith" or "hasCallHistory" to apply your custom comparison. Very useful if the spy gets called with functions that you want to test additionally.
spy(() => ({ prop: 'value', other: 13 }), 12);
spy.wasCalledWith(Spy.COMPARE(fn => fn().prop === 'value'), 12);
spy.wasCalledWith(Spy.COMPARE(fn => {
expect(fn()).toEqual({ prop: 'value', other: 13 });
}), 12);
Spy.MAPPER(from: any | any[], to: any) => SpyComparator
This function can be called in the same places like Spy.COMPARE
. It is not that much
customizable but provides a nice way to evaluate mapper functions. Meaning pure
functions that return some output for given inputs. The function will be called exactly
once for each comparison, so you can even rely on site effects you might want to test,
if you want to use this for non-pure functions.
spy((value: number) => ({ prop: 'here', other: value }), 12);
spy((value: number, num: number) => ({ prop: 'here', value, num }), 12);
spy.wasCalledWith(Spy.MAPPER('foo', { prop: 'here', other: 'foo' }), 12);
spy.wasCalledWith(Spy.MAPPER(['foo', 44], { prop: 'here', value: 'foo', num: 44 }), 12);
spy.configure(config: { useOwnEquals?: boolean, persistent?: boolean }) => (this) SpyInstance
With configure
the spy can be configured. One configuration possibility
is to ignore any equals
methods while comparing objects. There might be libraries which
come with those methods, but do not support ES6 classes or anything else. By default this
configuration is set to favor own equals
implementations while comparing objects.
Another possible configuration is to make the spy persist while other spies have to restore
when "restoreAll" was called. This spy can ONLY RESTORE the mocked object when
you configure it back to be NOT PERSISTENT. This configuration can only be applied to mocking
spies. For Spies created with new Spy()
this configuration will throw an exception.
spy.calls(...functions:Array<Function>) => (this) SpyInstance
The provided functions will be called sequentially in order when the spy will be called.
Meaning spy.calls(func1, func2, func3)
will call first func1
then func2
and the rest
of the time func3
.
spy.returns(...args: Array<any>) => (this) SpyInstance
The provided arguments will be returned sequentially in order when the spy will be called.
Meaning spy.returns(arg1, arg2, arg3)
will return first arg1
then arg2
and the rest
of the time arg3
.
spy.resolves(...args: Array<any>) => (this) SpyInstance
The provided arguments will be resolved sequentially in order when the spy will be called.
Meaning spy.resolves(arg1, arg2, arg3)
will return first Promise.resolve(arg1)
then Promise.resolve(arg2)
and the rest
of the time Promise.resolve(arg3)
.
spy.rejects(...args: Array<?string | Error>) => (this) SpyInstance
The provided arguments will be rejected sequentially in order when the spy will be called.
Meaning spy.rejects('foo', null, new Error('bar'))
will return first Promise.reject(new Error('foo'))
then Promise.reject(new Error('<SPY_NAME> was requested to throw'))
and the rest
of the time Promise.reject(new Error('bar'))
.
spy.throws(message: ?string | Error) => (this) SpyInstance
Perform this on a spy to make it throw an error when called. The error message can be provided but a default is also implemented. If an Error instance gets passed, exactly this one will be thrown.
spy.reset() => (this) SpyInstance
Does reset the registered calls on that spy.
spy.restore() => (this) SpyInstance
Restores the spied object, if existing, to its original state. The spy won't lose any other information. So it is still aware of made calls, can be plugged anywhere else and can still be called anywhere else. But it loses all references to the spied object.
If the spy was configured to be persistent this method will throw an error.
spy.transparent() => (this) SpyInstance
Can be useful with spies on objects. It does make the spy behave like not existing. So the original function of the "mocked" object will be called, but the spy does remember the call information.
spy.transparentAfter(callCount:number) => (this) SpyInstance
Works like transparent but the spy will get transparent after called as
often as specified. Meaning spy.transparentAfter(num)
will not be transparent on the first
num
calls.
spy.wasCalled(callCount: number = 0) => (fact) void
This call does display a fact. So if the spy is violating the fact, it is told to throw an error. The provided argument does represent the registered calls on that spy.
spy.wasNotCalled() => (fact) void
This fact displays that the spy was never called. Directly after the spy was reseted, this fact will be given.
spy.wasCalledWith(...args: Array<any>) => (fact) void
This fact displays that the spy was called at least once with equal arguments.
The equality check is a deep equality check, which (by default) does consider own "equals" implementations.
By supplying Spy.IGNORE
anywhere inside the expected call arguments, you
can avoid that the comparison is further executed. See Spy.IGNORE for further information and examples.
The deep equality check does also recursively iterate to the first difference found and is able to return a string which contains valuable information about the first found difference.
If any difference was detected. The fact is broken and a helpful error message will be displayed. If using monospaced consoles for the output which do support new lines, there will be really neat output. For examples see showCallArguments
spy.wasNotCalledWith(...args: Array<any>) => (fact) void
This fact displays simply the opposite of wasCalledWith.
spy.hasCallHistory(...callHistory: Array<Array<any> | any>) => (fact) void
Works similar to wasCalledWith but instead matches each call one by one in correct order and correct call count. ATTENTION: single argument calls can be provided without wrapping into an array. But e.g. if the single argument is an array itself, than you have to warp it also yourself. (Inspired by jest data providers)
spy.getCallArguments(callNr: number = 0) => Array<any>
Returns the call arguments that were registered on the given call. Meaning
spy.getCallArguments(num)
does return the (num + 1)'th call arguments.
Throws an exception if the provided (callNr
- 1) is bigger than the made calls.
spy.getCallArgument(callNr: number = 0, argNr: number = 0) => any
Same as getCallArguments but returns only a single entry out
of the array of arguments. Most useful in situations where exactly one call param is expected.
If argNr
is given, it returns the (argNr + 1)'th argument of the call.
spy.getCallCount() => number
This method simply returns the number of made calls on the spy.
spy.showCallArguments(additionalInformation: Array<string> = []) => string
This primarily internally used method is responsible for returning formatted informative debug messages when facts are broken. Let's do an example:
const spy = new Spy('my awesome spy');
spy(42, 'test', { attr1: [1, 2, new Date(2017, 1, 20)], attr2: 1337 });
spy(42, 'test', { attr1: [0, 2, new Date(2017, 1, 20)], attr2: 1336 });
spy(42, 'test', { attr1: [1, 2, new Date(2017, 1, 21)], attr2: 1336 });
spy(42, 'tes', { attr1: [1, 2, new Date(2017, 1, 20)], attr2: 1336 });
spy(42, 'test');
The following broken fact...
spy.wasCalledWith(42, 'test', {attr1: [1, 2, new Date(2017, 1, 20)], attr2: 1336});
...would produce the following error output:
Error:
my awesome spy was considered to be called with the following arguments:
--> [42, 'test', {attr1: [1, 2, new Date(1487545200000)], attr2: 1336}]
Actually there were:
call 0: [42, 'test', {attr1: [1, 2, new Date(1487545200000)], attr2: 1337}]
--> 2 / attr2 / different number [1337 != 1336]
call 1: [42, 'test', {attr1: [0, 2, new Date(1487545200000)], attr2: 1336}]
--> 2 / attr1 / 0 / different number [0 != 1]
call 2: [42, 'test', {attr1: [1, 2, new Date(1487631600000)], attr2: 1336}]
--> 2 / attr1 / 2 / different date [new Date(1487631600000) != new Date(1487545200000)]
call 3: [42, 'tes', {attr1: [1, 2, new Date(1487545200000)], attr2: 1336}]
--> 1 / different string ['tes' != 'test']
call 4: [42, 'test']
--> 2 / one was undefined [undefined != {attr1: [1, 2, new Date(1487545200000)], attr2: 1336}]
There you can see that the arguments of the fact (displayed above all others) does not match any of the call arguments on the 5 made calls.
For each call we display additional error information (the first found difference).
If the additional information begins with a -->
there was made a deep equality.
If you would travers with the displayed keys you would be directed to those objects which differ.
In this example the arguments differ for call 0
in -->
the third argument (2
) and
its attribute attr2
because there was a different number.
While recursively traversing down in the deep equality check, the object keys will be reported.
Meaning that 2
is representing the index of the array. So for example if you want to grep the
different objects you could:
const callArgs = spy.getCallArguments(0/* for the 0'th call above*/);
const differentNumber = callArgs[2]['attr2'];
FAQs
Smart, compact and powerful spy test framework
The npm package spy4js receives a total of 3,793 weekly downloads. As such, spy4js popularity was classified as popular.
We found that spy4js demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Research
Security News
A threat actor's playbook for exploiting the npm ecosystem was exposed on the dark web, detailing how to build a blockchain-powered botnet.
Security News
NVD’s backlog surpasses 20,000 CVEs as analysis slows and NIST announces new system updates to address ongoing delays.
Security News
Research
A malicious npm package disguised as a WhatsApp client is exploiting authentication flows with a remote kill switch to exfiltrate data and destroy files.