Skip to content

CodeJamboree/js-test

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

67 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

js-test

A simple test platform.

  • Run tests
    • Setup/Teardown all or individual tests
    • Run the same test with multiple test cases
    • Set timeout limits on all or individual tests
    • Randomize test order
    • Isolate or skip tests
  • Expectation helper
  • Mock functions
  • Spy & Fake standard output/error writes
  • Fake date creation
  • Fake performance now
  • Fake process hrtime
  • Fake random values
  • Fake http/https request

Running tests

The following will find all files in the src folder that end with .test.js, and run them.

import { run } from '@codejamboree/js-test';

run({
  folderPath: 'src',
  testFilePattern: /\.test\.js$/
}).then(() => {
  console.log('done');
})

Setup and tear down the entire test run, individual modules, or individual tests.

{
  beforeAll: () => {
    // I run before any test in the whole test-run begins
  },
  beforeSuite: () => {
    // I run before any of the tests in a module runs
  },
  beforeEach: () => {
    // I run before each test runs
  },
  afterEach: () => {
    // I run after each test completes
  },
  afterSuite: () => {
    // I run after all tests in a moudle completes
  },
  afterAll: () => {
    // I run after all tests in the entire project completes
  }
}

Other options are available, as well as the final results are returned.

import { run } from '@codejamboree/js-test';

run({
  folderPath: 'src',
  testFilePattern: /$([xf]_)?(.*)\.test\.js$/,
  testFileReplacement: '$2', // replacer for filename pattern
  timeoutMs: 300, // Limit time for each test to run
  failFast: true, // Stop all testing once a test fails
  randomOrder: true // Randomize the test order
}).then(results => {
  console.log('Failed', results.failed);
  console.log('Passed', results.passed);
  console.log('Skipped', results.skipped);
  console.log('Total', results.total);
  results.failures.forEach(failure => {
    console.group(failure.name);
    console.log('File', failure.filePath);
    console.log('Error', failure.error);
    console.groupEnd();
  });
})

Test Suite

A test suite is an individual file that exports functions as tests.

export const test1 = () => {
  let a = 1 + 2;
}
export const failingTest = () => {
  throw new Error('I have my reasons');
}
export const asyncTest = async () => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve("done");
    }, 100);
  });
}
asyncTest.timeoutMs = 200; // override timeout

Setup/Teardown

Special methods are ran before/after each test, or the entire set of tests in the file if present.

export const beforeAll = () => {}
export const beforeEach = () => {}
export const afterEach = () => {}
export const afterAll = () => {}
export const test = () => {}
test.before = () => {}
test.after = () => {}

Testing Lifecycle

  • runOptions.beforeAll()
  • loop through suites
    • runOptions.beforeSuite()
    • suite.beforeAll()
    • loop through each test / test case
      • runOptions.beforeEach()
      • suite.beforeEach()
      • test.before()
      • setTimeout
        • test()
      • test.after()
      • suite.afterEach()
    • suite.afterAll()
    • runOptions.afterSuite()
  • runOptions.afterAll()

Skipping Tests

Tests can be ignored by preceeding the name with an x_, or flagging the .skip property

export const test = () => {
  // I will run
}
export const x_stest = () => {
  // I will not run
}
export const flagged = () => {};
flagged.skip = true; // ensure flagged will not run

Focused Tests

Tests can be isolated by preceeding the name with an f_ or flagging the .focus property, causing all other tests to be skipped.

export const f_test = () => {
  // I will run
}
export const test = () => {
  // I will not run
}
export const flagged = () => {};
flagged.focus = true; // ensure flagged will run

Files and folders that contain tests may be renamed with f_ and x_ to focus and skip the entire file/directory of tests.

Test Cases

A test may have multiple test cases assigned to use repeated testing logic with different testing vectors. In this scenario, your test will have arguments, and an array of arguments to be tested will be assigned to the functions .testCases property.

export const test = (a, b, sum) => {
  if(a + b !== sum) {
    throw new Error(`${a + b} was not ${sum}`);
  }
}
test.testCases = [
  [37, 5, 42],
  [51, 75, 126],
  [22, 27, 49],
  [32, 56, 88],
  [83, 12, 95]
]

When tests are ran with random order, the order of test cases will also be random.

Expectation

An expectation helper assists in commong checks.

import { expect } from '@codejamboree/js-test';

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);
}

Additional details can help narrow down source of failure.

export const test = () => {
  const target = "123";
  expect(target, 'checking is').is("123");
  expect(target, 'comparing equal').equals(123);
  expect(target, 'third check').isFunction(false);
}

Negate method swaps 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().includes("9");
}

Mock Functions

Detect when functions were called, what they were called with, and control their response and behavior.

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");
}

Standard Utility

Inspect write arguments to the standard output & standard error, and prevent them from being written.

import { expect, standardUtils } from '@codejamboree/js-test';

export const afterEach = () => {
  standardUtils.restore();
}

export const test = () => {
  console.log("You can see me");

  standardUtils.skipWrite();
  console.log("You can't see me");

  standardUtils.spy();
  console.log("Hidden 1");
  console.warn("Hidden 2");

  expect(standardUtils.writes(), 'spied').equals([
    'Hidden 1\n',
    "\u001b[33mHidden 2\u001b[39m\n"
  ]);
  expect(standardUtils.writeAt(0), 'start 1')
    .is('Hidden 1\n');
  expect(standardUtils.writeAt(1), 'start 2')
    .is("\u001b[33mHidden 2\u001b[39m\n");

  expect(standardUtils.typeAt(0)).is('standard');
  expect(standardUtils.typeAt(1)).is('error');

  standardUtils.unspy();
  console.log('Still hidden');
  expect(standardUtils.writeAt(-1), 'last')
    .is("\u001b[33mHidden 2\u001b[39m\n");

  standardUtils.allowWrite();
  console.log("You can see me again!");
  expect(standardUtils.writes(), 'all writes').equals([
    'Hidden 1\n',
    "\u001b[33mHidden 2\u001b[39m\n"
  ]);

  standardUtils.clearCaptured();
  expect(standardUtils.writes(), 'writes').equals([]);

}

Date Utility

Freeze time in place, or set it to a specific time.

import { expect, dateUtils } from '@codejamboree/js-test';

export const afterEach = () => {
  dateUtils.restore();
}

export const timeFrozen = async () => {
  dateUtils.freeze();
  return new Promise((resolve) => {
    const date = new Date();
    setTimeout(() => {
      expect(date.getTime()).is(new Date().getTime());
      resolve();
    }, 100);
  });
}

export const customTime = async () => {
  dateUtils.set(Date.UTC(1975, 4, 28, 3, 15, 1, 184));
  expect(new Date().toISOString()).is('1975-05-28T03:15:01.184Z');
}

export const timeRestored = () => {
  dateUtils.set(Date.UTC(1975, 4, 28, 3, 15, 1, 184));

  expect(new Date()).instanceOf('FakeDate');
  expect(new Date().toISOString()).is('1975-05-28T03:15:01.184Z');

  dateUtils.restore();
  
  expect(new Date()).not().instanceOf('FakeDate');
  expect(new Date().toISOString()).not().is('1975-05-28T03:15:01.184Z');
}

Performance Utility

Fake performance now.

export const test = () => {
  performanceUtils.freeze();
  const now1 = performance.now();
  const now2 = performance.now();
  expect(now1).is(now2);

  performanceUtils.set(123);
  expect(performance.now()).is(123);

  performanceUtils.restore();
  const now3 = performance.now();
  const now4 = performance.now();
  expect(now3).not().equals(now4);
}

Process Utility

Fake process high-resolution time

export const testFrozen = () => {
  standardUtils.spyAndHide();
  processUtils.freeze();
  console.time(label);
  console.timeEnd(label);
  expect(standardUtils.writeAt(-1)).equals(`${label}: 0ms\n`);
  processUtils.restore();
  standardUtils.restore();
}
export const testCustom = () => {
  standardUtils.spyAndHide();
  processUtils.set([1.000, 0]);
  console.time(label);
  processUtils.set([1.001, 0]);
  console.timeEnd(label);
  expect(standardUtils.writeAt(-1)).equals(`${label}: 1ms\n`);
  processUtils.restore();
  standardUtils.restore();
}

Chrono Utility

Freeze time-based methods and objects

export const test = () => {

  chronoUtils.freeze();

  const time1 = process.hrtime();
  const time2 = process.hrtime();
  expect(time1).equals(time2);

  const now1 = performance.now();
  const now2 = performance.now();
  expect(now1).equals(now2);

  const date1 = new Date();
  const date2 = new Date();
  expect(date1).equals(date2);

  chornoUtils.restore();
}

Math Random Utility

Ensure "random" numbers are deterministic.

// Psuedo Random Number Generator
mathRandomUtils.prng(8675309);
expect(Math.random()).is(0.8961716736183369);
expect(Math.random()).is(0.957318503389749);
expect(Math.random()).is(0.6520864715110913);

// Seed again
mathRandomUtils.prng(8675309);
expect(Math.random()).is(0.8961716736183369);

// Constant value
mathRandomUtils.setValue(0.3);
expect(Math.random()).is(0.3);
expect(Math.random()).is(0.3);

// List of values
mathRandomUtils.setValues([0.3, 0.7, 0.4]);
expect(Math.random()).is(0.3);
expect(Math.random()).is(0.7);
expect(Math.random()).is(0.4);
// Restarts at first value
expect(Math.random()).is(0.3);

// Custom function
let value = 0.3;
mathRandomUtils.setFunction(() => 0.1 + value);
expect(Math.random()).is(0.4);
value = 0.8;
expect(Math.random()).is(0.9);

// Cleanup
mathRandomUtils.restore();

Http Utility

Mimic requests and responses from the http/https request methods.

NOTE: Some methods/logic are missing from FakeClientRequest and FakeIncomingMessage.

export const afterEach = () => {
  httpUtils.restore();
}

export const status = async () => new Promise<void>((resolve, reject) => {
  httpUtils.setStatus(123, "The Status");
  const request = https.request("https://codejamboree.com");
  request.on('response', res => {
    expect(res.statusCode).is(123);
    expect(res.statusMessage).is('The Status');
    resolve();
  });
  request.on('error', reject);
  request.end();
});

export const chunks = async () => new Promise<void>((resolve) => {
  httpUtils.setChunks([
    'first',
    'second',
    'third'
  ]);
  /* As binary data
  httpUtils.setChunks([
    [ 0x66, 0x69, 0x72, 0x73, 0x74       ],
    [ 0x73, 0x65, 0x63, 0x6f, 0x6e, 0x64 ],
    [ 0x74, 0x68, 0x69, 0x72, 0x64       ]
  ]);
  */
  const chunksReceived: any[] = [];
  const request = https.request("https://codejamboree.com");
  request.on('response', res => {
    res.on('data', chunk => {
      chunksReceived.push(new TextDecoder().decode(chunk));
    });
    res.on('end', () => {
      expect(chunksReceived).equals([
        'first',
        'second',
        'third'
      ]);
      resolve();
    });
  });
  request.end();
});

export const callback = async () => new Promise<void>((resolve) => {
  httpUtils.mock();

  const callback = (res: http.IncomingMessage) => {
    res.on('end', () => {
      resolve();
    });
  };

  const request = https.request("https://codejamboree.com", callback);
  request.end();
});


export const postDataWithEncodedResponse = async () => new Promise<void>((resolve) => {
  httpUtils.mock();
  const responseData = JSON.stringify({ message: "Received!" });
  httpUtils.setResponseData(responseData);
  /* As binary data
  httpUtils.setResponseData([
    0x7b, 0x22, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67,
    0x65, 0x22, 0x3a, 0x20, 0x22, 0x52, 0x65, 0x63, 
    0x65, 0x69, 0x76, 0x65, 0x64, 0x21, 0x22, 0x7d
  ]);
  // 4 byte chunks
  httpUtils.setResponseData(responseData, 4);
  */

  const callback = (res: http.IncomingMessage) => {
    let receivedData: string = '';
    res.setEncoding('utf8');
    res.on('data', (chunk: string) => {
      receivedData += chunk;
    });
    res.on('end', () => {
      const parsed = JSON.parse(receivedData);
      expect(parsed.message).is('Received!');
      resolve();
    });
  };

  const url = new URL("https://codejamboree.com");

  const postData = JSON.stringify({ name: "Lewis Moten" });

  const options = {
    hostname: url.hostname,
    path: url.pathname,
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Content-Length': Buffer.byteLength(postData)
    }
  }
  const request = https.request(options, callback);
  request.write(postData);
  request.end();
});

About

A small testing framework

Resources

License

Stars

Watchers

Forks

Packages

No packages published