I’ve worked with many various testing platforms in the past. I tried to reduce the number of third-party dependencies in my projects. I still needed testing of some sort, and started out simple. However, I needed to test various aspects of my code, mock objects, monitor functions, and control the reporting. The logic was complex enough that it merited its own little project, separate from the library I was testing.
You can simply import “run” and call it with an object describing the folder where tests are located, and the file pattern to look for.
import { run } from '@codejamboree/js-test';
run({
folderPath: 'build/src',
testFilePattern: /\.test\.js$/
}).then(() => {
console.log('done');
});
Tests are rather simple. They are exported functions. If an error is thrown, the test fails. If the test takes too long to execute, it is considered as failing. For tests that are expected to take a long time, you can override the timeout by adding a timeoutMs property to the test.
There are a few special names to handle setup and teardown of each test, or the module as a whole. In addition, you can prefix test names to skip or focus on the test. A focused test will be the only test ran in the entire test run.
export const passingTest = () => { let a = 1 + 2;}
export const failingTest = () => throw new Error('Fail');
export const promiseTest = () => new Promise(
(resolve, reject) => setTimeout(resolve, 100));
promiseTest.timeoutMs = 200;
export const asyncTest = async () => await promiseTest();
asyncTest.timeoutMs = 200;
export const f_focusedTest = () => {}
export const runningTest = () => {}
export const x_skippedTest = () => {}
// Setup
export const beforeAll = () => {}
export const beforeEach = () => {}
// Teardown
export const afterEach = () => {}
export const afterAll = () => {}
Besides setup/teardown in the test files themselves, you can also do the same for the whole run, as well as setup the timeout for tests.
Another feature is that you can setup a replacement for the file name so that just the parsed name of the file will be displayed.
run({
folderPath: 'build/src',
testFilePattern: /([xf]_)?(.*)\.test\.js$/,
testFileReplacement: '$2',
timeoutMs: 1000,
beforeAll: () => {},
beforeSuite: () => {},
beforeEach: () => {},
afterEach: () => {},
afterSuite: () => {},
afterAll: () => {}
})
Expect
The expectation helper was written to do comparisons and throw errors when the expectations are not met, along with additional details about the failure. Rather than writing out the entire error stack, ExpectationErrors write out just the basic details of what failed.
export const test = () => {
const target = "123";
expect(target).is("123");
expect(target).equals(123);
expect(target).above("100");
expect(target).below("150");
expect(target).within("100", "150");
expect(target).isFunction(); // error
expect(target).lengthOf(3);
expect(target).startsWith("1");
expect(target).endsWith("3");
expect(target).includes("2");
}
export const testInstance = () => {
const target = new Date();
expect(target).instanceOf(Date);
expect(target).instanceOf('Date');
}
export const testErrors = () => {
const message = 'This is an error';
const customError = new Error(message);
const target = () => {
throw customError;
};
expect(target).toThrow();
expect(target).toThrow(message);
expect(target).toThrow(customError);
}
You can add some context as to what is being evaluated.
export const test = () => {
const target = "123";
expect(target, 'checking is').is("123");
expect(target, 'comparing equal').equals(123);
expect(target, 'third check').isFunction(false);
}
You can also negate the expectations.
export const test = () => {
const target = "123";
expect(target).not().is(123);
expect(target).not().equals("432");
expect(target).not().above("150");
expect(target).not().below("100");
expect(target).not().within("200", "250");
expect(target).not().isFunction();
expect(target).not().lengthOf(42);
expect(target).not().startsWith("3");
expect(target).not().endsWith("1");
expect(target).not().not().not().not().not().includes("9");
}
Mock Functions
What is testing without mocking? A function mocking utility is provided to monitor all calls. You can set the default response, or add your own custom logic.
import { expect, mockFunction } from '@codejamboree/js-test';
export const test = () => {
const target = mockFunction();
target("apple", "banana");
target("pizza");
expect(target.called()).is(true);
expect(target.callCount()).is(2);
expect(target.callAt(0)).equals(["apple", "banana"]);
expect(target.callArg(1, 0)).is("pizza");
}
export const testValue = () => {
const target = mockFunction();
target.returns("pickles");
const value = target();
expect(value).is("pickles");
}
export const testLogic = () => {
const target = mockFunction(
(food) => {
return `I like ${food}`;
}
);
const value = target("snacks");
expect(value).is("I like snacks");
}
Faking It
There are more utilities that are more for monitoring or controlling the behavior of nondeterministic functions. These utilities may eventually make it into their own project, but they are fairly handy as part of the testing framework.
- standardUtils: Allows you to hide output to the standard out/error stream (console.log)
- spy: capture write arguments
- unspy: stop capturing write arguments
- skipWrite: don’t allow calls to standard out/error stream
- allowWrite: allow calls to standard out/error stream
- writes: get write arguments caught by spy
- writeAt: get write argument at a specific index
- writeAt(0) for first
- writeAt(-1) // can go backwards to get last write
- typeAt: gets the destination of the arguments (error or standard)
- clearCaptured: clears all write arguments captured
- spyAndHide: starts spy, and skips writes
- restore: stops spy, allow writes, and clears captured
- processUtils: Allows high-resolution time to be faked (console.time/timeLog/timeEnd)
- freeze: freezes hrtime() at its current setting
- set: sets hrtime() to a custom value
- restore: restore original hrtime()
- performanceUtils: allows performance.now() to be faked
- freeze: freezes now() at its current setting
- set: sets now() to a custom value
- restore: restore original now()
- dateUtils: allows Date() to be faked
- freeze: freezes the current Date() at is current time
- set: sets a new time to represent “now”
- restore: restore original Date()
- chronoUtils: allows control over Date, performance.now() and process.hrtime()
- freeze: freeze as current time
- set: set to specific time
- restore: restore all time based objects/functions
- mathRandomUtils: allows Math.random() to be faked
- setValue: sets a constant value to be returned
- setValues: sets an array of values to cycle through
- setFunction: sets a custom function to perform the generation
- prng: sets a determined pattern based off of a given seed
- Pseudo-random number generator (PRNG)
- Specifically a Linear Congruential Generator (LCG)
- Configured as a Park-Miller LCG (aka the Minimal Standard)
- restore: restore the original Math.random()
- httpUtils: allows http/https request() to be faked
- setClientRequest: change fake requests to custom class
- setIncomingMessage: change fake incoming message to custom class
- mock: fake http/https request
- setChunks: set simulated data to be received
- setChunks([‘Ch’, ‘un’, ‘ks’]);
- setResponseData: set simulated data to be received, and broken apart
- setResponseData(JSON.stringify(data), 1024) // 1024 byte chunks
- setStatus: set status code and message
- note: calls to anything except restore will wire up the request.
The HTTP utilities work for the most part. However, there are some bits missing in the FakeClientRequest and FakeIncomingMessage. I’ve mostly set it up to work with behavior that I’ve been testing with in my other projects.
