Skip to content

Commit

Permalink
Merge pull request #5 from chalu/add-unit-tests
Browse files Browse the repository at this point in the history
Add unit tests
  • Loading branch information
chalu authored Feb 16, 2024
2 parents f6241ef + c0646b8 commit ea42403
Show file tree
Hide file tree
Showing 16 changed files with 5,617 additions and 49 deletions.
12 changes: 12 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
root = true

[*]
indent_style = tab
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

[*.yml]
indent_style = space
indent_size = 2
49 changes: 49 additions & 0 deletions .github/workflows/CI.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
name: CI
on:
push:
branches:
- "!main"
- "*"
pull_request:
branches:
- "dev"
- "main"

jobs:
build-and-test:
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Install PNPM
uses: pnpm/action-setup@v3
with:
version: 8
run_install: false

- name: Install Node 20.x
uses: actions/setup-node@v4
with:
node-version: "20.x"
cache: pnpm

- name: Install Dependencies
run: pnpm install

- name: Check Code Style
run: pnpm lint

- name: Run Build
run: pnpm build

- name: Run Unit Tests
run: pnpm test:ci

- name: Summarise Tests
uses: test-summary/action@v2
with:
paths: "__tests__/summary.xml"
show: "fail, skip"
if: always()
1 change: 1 addition & 0 deletions .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pnpm test
6 changes: 6 additions & 0 deletions .xo-config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"rules": {
"import/extensions": "off",
"unicorn/prefer-module": "off"
}
}
47 changes: 47 additions & 0 deletions __tests__/data.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import {simpleFaker as Faker} from '@faker-js/faker';

type Config = {
howMany?: number;
};

type MaybeNumber = number | undefined | unknown;

const defaultDataSize = 100;

const factory = <T>(howMany: number, generator: () => T) => Faker.helpers.multiple<T>(generator, {count: howMany});

export const uuids = (arguments_: Config = {}) => {
const {howMany = defaultDataSize} = arguments_;
return factory(howMany, Faker.string.uuid);
};

export const dates = (arguments_: Config = {}) => {
const {howMany = defaultDataSize} = arguments_;
return factory(howMany, Faker.date.recent);
};

export const numbers = (arguments_: Config = {}) => {
const {howMany = defaultDataSize} = arguments_;
return factory(howMany, Faker.string.numeric).map(n => Number.parseInt(n, 10));
};

export const hexadecimals = (arguments_: Config = {}) => {
const {howMany = defaultDataSize} = arguments_;
return factory(howMany, Faker.string.hexadecimal);
};

const mulpleOf = (multiple: number, item: MaybeNumber) => {
if (
!item
|| typeof item !== 'number'
|| item % multiple !== 0
) {
return false;
}

return true;
};

export const isEven = (number_: MaybeNumber) => mulpleOf(2, number_);
export const isMultipleOfFive = (number_: MaybeNumber) => mulpleOf(5, number_);
export const isDate = (date: unknown) => date instanceof Date && !Number.isNaN(date.getTime());
97 changes: 97 additions & 0 deletions __tests__/features.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import * as _ from 'jest-extended';
import {tuplesFromArray} from '../src/index';
import {
uuids, dates, hexadecimals, numbers, isEven, isDate,
} from './data';

type ParameterDataset<D> = {data: D[]; size: number; type?: string};

describe('nTuple Array', () => {
it('should use default maxItems value', () => {
const tuples = tuplesFromArray({list: numbers()});
for (const tuple of tuples) {
expect(tuple).toBeArrayOfSize(2);
}
});

it('should use provided maxItems value', () => {
const tuples = tuplesFromArray({list: numbers(), maxItems: 5});
for (const tuple of tuples) {
expect(tuple).toBeArrayOfSize(5);
}
});

it('should work without a match function', () => {
const tuples = tuplesFromArray({list: uuids(), maxItems: 10});
for (const tuple of tuples) {
expect(tuple).toBeArrayOfSize(10);
}
});

it('should use match function when provided', () => {
const nums = Array.from({length: 100}, (_, i: number) => i + 1);
const tuples = tuplesFromArray({list: nums, maxItems: 5, match: isEven});
for (const tuple of tuples) {
expect(tuple).toBeArrayOfSize(5);
expect(tuple).toSatisfyAll(isEven);
}
});

it.each<ParameterDataset<string>>([
{data: uuids({howMany: 50}), size: 2},
{data: uuids({howMany: 55}), size: 5},
{data: uuids({howMany: 200}), size: 10},
])('should produce correct quantities when sub-arrays have $size items', ({data, size}) => {
const tuples = tuplesFromArray({list: data, maxItems: size});
const allItemsFlattened: Array<string | undefined > = [];
for (const tuple of tuples) {
expect(tuple).toBeArrayOfSize(size);
allItemsFlattened.push(...tuple);
}

expect(allItemsFlattened).toBeArrayOfSize(data.length);
});

it.each<ParameterDataset<string>>([
{data: hexadecimals({howMany: 5}), size: 3},
{data: hexadecimals({howMany: 59}), size: 5},
{data: hexadecimals({howMany: 108}), size: 10},
])('should produce correct remainder quantities when sub-arrays are not all maxItems size', ({data, size}) => {
const tuples = tuplesFromArray({list: data, maxItems: size});
const allItemsFlattened: Array<string | undefined > = [];
const localTuples: Array<Array<string | undefined >> = [];
for (const tuple of tuples) {
allItemsFlattened.push(...tuple);
localTuples.push(tuple);
}

expect(allItemsFlattened).toBeArrayOfSize(data.length);

for (const tuple of localTuples) {
expect(tuple).toBeArray();
expect(tuple.length).toBeLessThanOrEqual(size);
expect(tuple.length).toBeGreaterThanOrEqual(data.length % size);
}

const hasRemainder = localTuples.pop();
expect(hasRemainder).toBeArrayOfSize(data.length % size);
});

it.each<ParameterDataset<number | Date>>([
{data: dates({howMany: 50}), size: 10, type: 'date'},
{data: Array.from({length: 50}, (_, i: number) => i + 1), size: 5, type: 'number'},
])('should produce sub-arrays with the correct data types', ({data, size, type}) => {
const matcher = type === 'number' ? isEven : undefined;
const tuples = tuplesFromArray({list: data, maxItems: size, match: matcher});

for (const tuple of tuples) {
expect(tuple).toBeArrayOfSize(size);

const predicate = type === 'number'
? isEven
: (type === 'date' ? isDate : () => false);

expect(tuple).toSatisfyAll(predicate);
}
});
});
3 changes: 3 additions & 0 deletions __tests__/setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import {toBeArray, toBeArrayOfSize, toSatisfyAll} from 'jest-extended';

expect.extend({toBeArray, toBeArrayOfSize, toSatisfyAll});
60 changes: 60 additions & 0 deletions __tests__/smoke.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
const {tuplesFromArray, InvalidInvocationParameterError} = require('../dist/index.js');

describe('Smoke Tests', () => {
it('should throw if input array is not specified', () => {
function functionInClientCode() {
tuplesFromArray({});
}

expect(functionInClientCode).toThrow(InvalidInvocationParameterError);
});

it('should throw if input array is undefined', () => {
function functionInClientCode() {
tuplesFromArray({list: undefined});
}

expect(functionInClientCode).toThrow(InvalidInvocationParameterError);
});

it('should throw if input array is not an array', () => {
function functionInClientCode() {
tuplesFromArray({list: 'some iterable object'});
}

expect(functionInClientCode).toThrow(InvalidInvocationParameterError);
});

it('should throw if maxItems param not a number', () => {
function functionInClientCode() {
tuplesFromArray({list: [], maxItems: {}});
}

expect(functionInClientCode).toThrow(InvalidInvocationParameterError);
});

it('should throw if maxItems param is 0', () => {
function functionInClientCode() {
tuplesFromArray({list: [], maxItems: 0});
}

expect(functionInClientCode).toThrow(InvalidInvocationParameterError);
});

it('should throw if maxItems param is < 0', () => {
function functionInClientCode() {
tuplesFromArray({list: [], maxItems: -2});
}

expect(functionInClientCode).toThrow(InvalidInvocationParameterError);
});

it('should throw if the match param not a function', () => {
function functionInClientCode() {
tuplesFromArray({list: [], match: {}});
}

expect(functionInClientCode).toThrow(InvalidInvocationParameterError);
});
});

17 changes: 17 additions & 0 deletions dist/index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
type IterationResult<T> = {
done: boolean;
value: Array<T | undefined>;
};
type Matcher<T> = (item: T | undefined) => boolean;
export type TupleConfig<T> = {
list: T[];
maxItems?: number;
match?: Matcher<T>;
};
export declare class InvalidInvocationParameterError extends Error {
}
export declare const tuplesFromArray: <T>(config: TupleConfig<T>) => {
[Symbol.iterator](): any;
next(): IterationResult<T>;
};
export {};
59 changes: 59 additions & 0 deletions dist/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.tuplesFromArray = exports.InvalidInvocationParameterError = void 0;
class InvalidInvocationParameterError extends Error {
}
exports.InvalidInvocationParameterError = InvalidInvocationParameterError;
const validateParametersOrThrow = (list, maxItems, match) => {
if (!list || !Array.isArray(list)) {
throw new InvalidInvocationParameterError('expected list to be an array');
}
if (typeof maxItems !== 'number'
|| (typeof maxItems === 'number' && maxItems <= 0)) {
const message = 'expected maxItems (when provided) to be a positive integer (1 and above)';
throw new InvalidInvocationParameterError(message);
}
if (match !== undefined && typeof match !== 'function') {
const message = 'expected match (when provided) to be a function';
throw new InvalidInvocationParameterError(message);
}
};
const tuplesFromArray = (config) => {
const { list, match, maxItems = 2 } = config;
validateParametersOrThrow(list, maxItems, match);
let cursor = 0;
const maxItemSize = Number.parseInt(`${maxItems}`, 10);
const iterable = {
[Symbol.iterator]() {
return this;
},
next() {
if (cursor >= list.length) {
return { done: true, value: [] };
}
const items = [];
const endIndex = match === undefined
// A match funtion was provided. Okay to run to array end
// or until we've matched maxItemSize elements
? Math.min(cursor + maxItemSize, list.length)
// No match function was provided. We should run till we are
// out of items (list.length) or till we gotten the next set
// of maxItemSize items
: list.length;
while (cursor < endIndex) {
const item = list[cursor];
cursor += 1;
if (match && !match(item)) {
continue;
}
items.push(item);
if (match && items.length === maxItemSize) {
break;
}
}
return { done: false, value: items };
},
};
return iterable;
};
exports.tuplesFromArray = tuplesFromArray;
22 changes: 22 additions & 0 deletions jest.config.ci.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import type {JestConfigWithTsJest} from 'ts-jest';

const jestConfig: JestConfigWithTsJest = {
preset: 'ts-jest',
testEnvironment: 'node',
testMatch: [
'/**/*.test.*',
],
transformIgnorePatterns: [
'/node_modules/',
'\\.pnp\\.[^\\/]+$',
],
reporters: [
'default',
['jest-junit', {
outputFile: '__tests__/summary.xml',
}],
],
setupFilesAfterEnv: ['./__tests__/setup.ts'],
};

export default jestConfig;
Loading

0 comments on commit ea42403

Please sign in to comment.