2.1.1 • Published 11 months ago

@relmify/jest-fp-ts v2.1.1

Weekly downloads
1,307
License
MIT
Repository
github
Last release
11 months ago

Jest Matchers for the fp-ts ecosystem

Build

Jest matchers for projects using fp-ts and io-ts.

Problem

If your TypeScript project is written in a functional programming style using fp-ts and io-ts, many of the values you'll want to check in your unit tests will come wrapped inside container types like Either, Option, or These. Jest has no awareness of these container types and no built-in matchers to help you to compare wrapped values against un-wrapped values. This leaves you with two options:

  1. Extract the received value from the container type before using a jest matcher.
  2. Lift the expected value into a container of the expected type before using a jest matcher.

Both options work, but tend to make your tests somewhat verbose, adding unnecessary work when writing your tests, and making it harder to read and maintain them.

Solution

@relmify/jest-fp-ts adds additional matchers to Jest's default ones, making it easier to test code that makes use of fp-ts functional containers.

Installation

With npm:

npm install -D @relmify/jest-fp-ts

With yarn:

yarn add -D @relmify/jest-fp-ts

You also need both fp-ts and io-ts installed in your project. If you're here, presumably you're already using fp-ts. Not every fp-ts project uses io-ts though. If you aren't using io-ts in your project, simply install it as a development dependency.

With npm:

npm install -D io-ts

With yarn:

yarn add -D io-ts

Setup

To make all @relmify/jest-fp-ts matchers globally available in your test files, add @relmify/jest-fp-ts to your Jest setupFilesAfterEnv configuration.

See jest documentation for additional help.

Setup with package.json

In your package.json file add:

"jest": {
  "setupFilesAfterEnv": ["@relmify/jest-fp-ts"]
}

Setup with jest.config.js

// jest.config.js
module.exports = {
  setupFilesAfterEnv: ['@relmify/jest-fp-ts'],
};

Setup with vite.config.ts (for vitest)

// vite.config.ts
export default defineConfig({
  test: {
    // ...
    setupFiles: ['@relmify/jest-fp-ts'],
    // ...
  },
});

Typescript Editor Support

If your editor does not recognize the custom @relmify/jest-fp-ts matchers, add a global.d.ts file to your project with:

import '@relmify/jest-fp-ts';

If you've added a global.d.ts file and your editor still has problems recognizing these matchers, you may need to specifically include the global.d.ts file in your Typescript configuration using the "include" or "files" property. For example, in tsconfig.json:

{
  "compilerOptions": {
    "module": "commonjs",
    "strict": true,
    ...
  },
  "include": ["**/*", "global.d.ts"],
  "exclude": ["node_modules", "**/__tests__/*"]
}

Alternatively, you can resolve this issue by adding import '@relmify/jest-fp-ts'; to each test file that makes use of @relmify/jest-fp-ts matchers.


Matchers

Either Matchers

.toBeEither()

Use .toBeEither() to check if a value is consistent with an Either. In other words, this matcher confirms that the value is a Left or a Right.

Note that a Left or a Right value is also consistent with a These and would also pass a .toBeThese() test.

Either or These Matchers

The matchers below can be used for Left and Right values from an Either or a These.

.toBeLeft()

Use .toBeLeft() to check if a value is a Left.

.toBeLeftErrorMatching(string | RegExp)

Use .toBeLeftErrorMatching(string | RegExp) to check if a value is a Left whose value is an instance of Error with a message property that contains the supplied string or matches the supplied RegExp.

.toBeRight()

Use .toBeRight() to check if a value is a Right.

.toEqualLeft(value)

Use .toEqualLeft(value) to check if a value is a Left whose value equals an expected value. See Jest's toEqual(value) documentation for information about how the .toEqual() comparison works.

.toEqualRight(value)

Use .toEqualRight(value) to check if a value is a Right whose value equals an expected value. See Jest's toEqual(value) documentation for information about how the .toEqual() comparison works.

.toStrictEqualLeft(value)

Use .toStrictEqualLeft(value) to check if a value is a Left that contains a value that strictly equals an expected value. See Jest's toStrictEqual(value) documentation for information about how .toStrictEqual() differs from toEqual().

.toStrictEqualRight(value)

Use .toStrictEqualRight(value) to check if a value is a Right that contains a value that strictly equals an expected value. See Jest's toStrictEqual(value) documentation for information about how .toStrictEqual() differs from toEqual().

.toSubsetEqualLeft(value)

Use .toSubsetEqualLeft(value) to check if a value is a Left whose value equals or subset matches the expected value. A subset match passes when the received value is a Left whose value is an object with a subset of properties that match the expected object. The received value must contain all of the expected properties, and may contain more than the expected properties.

You can also pass an array of values to match against a received Left value that is an array of values. In this case, each value in the expected array is compared against the corresponding value in the array contained in the received Left. Both arrays must be the same length or the match will fail.

Note that an empty expected object will match against any received Left whose value is an object.

.toSubsetEqualRight(value)

Use .toSubsetEqualRight(value) to check if a value is a Right whose value equals or subset matches the expected value. A subset match passes when the received value is a Right whose value is an object with a subset of properties that match the expected object. The received value must contain all of the expected properties, and may contain more than the expected properties.

You can also pass an array of values to match against a received Right value that is an array of values. In this case, each value in the expected array is compared against the corresponding value in the array contained in the received Right. Both arrays must be the same length or the match will fail.

Note that an empty expected object will match against any received Right whose value is an object.

These Matchers

.toBeBoth()

Use .toBeBoth() to check if a value is a Both.

.toBeThese()

Use .toBeThese() to check if a value is consistent with a These. In other words, this matcher confirms that the value is a Left, a Right, or a Both.

Note that a Left or a Right value is also consistent with an Either and would also pass a .toBeEither() test.

.toEqualBoth(leftValue, rightValue)

Use .toEqualBoth(leftValue, rightValue) to check if a value is a Both that contains a left value that equals an expected value, and a right value that equals an expected value. See Jest's toEqual(value) documentationfor information about how the .toEqual() comparison works.

.toStrictEqualBoth(leftValue, rightValue)

Use .toStrictEqualBoth(leftValue, rightValue) to check if a value is a Both that contains a left value that strictly equals an expected value, and a right value that strictly equals an expected value. See Jest's toStrictEqual(value) documentation for information about how .toStrictEqual() differs from toEqual().

.toSubsetEqualBoth(leftValue, rightValue)

Use .toSubsetEqualBoth(leftValue, rightValue) to check if a value is a Both whose left and right values equal or subset match the expected leftValue and rightValue. A subset match passes when a received value is an object with a subset of properties that match the expected object. The received value must contain all of the expected properties, and may contain more than the expected properties.

You can also pass arrays of values to match against received values that contain arrays of values. In this case, each value in the expected array is compared against the corresponding value in the array contained in the received. Both arrays must be the same length or the match will fail.

Note that an empty expected object will match against any received object.

Option Matchers

.toBeNone()

Use .toBeNone() to check if a value is a None.

.toBeOption()

Use .toBeOption() to check if a value is an Option (either a Some or a None).

.toBeSome()

Use .toBeSome() to check if a value is a Some.

.toEqualSome(value)

Use .toEqualSome(value) to check if a value is a Some that contains a value that equals an expected value. See Jest's toEqual(value) documentationfor information about how the .toEqual() comparison works.

.toStrictEqualSome(value)

Use .toStrictEqualSome(value) to check if a value is a Some that contains a value that strictly equals an expected value. See Jest's toStrictEqual(value) documentation for information about how .toStrictEqual() differs from toEqual().

.toSubsetEqualSome(value)

Use .toSubsetEqualSome(value) to check if a value is a Some that contains an object with a subset of properties that match the expected object properties. The received value must contain all of the expected properties, and may contain more than the expected properties.

Decode Matchers

.toBeLeftWithErrorsMatching(Array<string | RegExp>)

Use .toBeLeftWithErrorsMatching(Array<string | RegExp>) when testing validation errors returned by io-ts decode() operations.

Note that a ValidationError is NOT a standard javascript Error object. See .toBeLeftErrorMatching(string | RegExp) for a matcher that works with standard Error objects.

An io-ts decode() method will return a Left with an array of ValidationError objects if the supplied value can not be successfully validated and decoded to the specified io-ts type. For codecs that are composed from multiple codecs, multiple errors may be returned as each sub-codec is applied to the values it is charged with validating.

This matcher provides an easy way to check if expected validation errors are present. To do this, it makes use of the io-ts PathReporter module.

To use this matcher, supply an array of strings that you expect to be present in the array of strings returned by PathReporter.report(). You can supply either regular expressions or substrings. The matcher will try to match each array entry against the array of Pathreporter.report() strings.

If the supplied object is not a Left that contains an array of ValidationError objects, or if any of the strings you supply cannot be matched to one of the ValidationError objects, the matcher will return false. If all of the strings you supply are matched, it will return true.

Example:

const Name = t.type({
  first: t.string,
  last: t.string,
});
type Name = t.TypeOf<typeof Name>;
const numberName = { first: 404, last: 401 };
test('if the received is a Left that contains errors matching the expected values', () => {
  expect(Name.decode(numberName)).toBeLeftWithErrorsMatching([/404/, '401']);
});

Note:

This matcher supports the current (stable) io-ts interface. There is a new experimental io-ts decoder interface that returns Either<DecodeError, A> results instead. This matcher does not support that interface.

Asymmetric Matchers

All of the provided matchers are asymmetric matchers, which means that they can be called from any other matcher that accepts asymmetric matchers like so:

test('works if called as an asymmetric matcher', () => {
  expect(left('Any sufficiently advanced technology is equivalent to magic.')).toEqual(
    expect.toEqualLeft('Any sufficiently advanced technology is equivalent to magic.'),
  );
});

The provided toEqual*(value), toStrictEqual*(value) and toSubsetEqual*(value)matchers also accept asymmetric matchers which means you can pass in any of the standard Jest asymmetric matchers, or any of the jest-extended matchers. This can be especially handy when you don't want to check against a literal value.

test('works if called with an asymmetric matcher', () => {
  expect(both(['error 1', 'error 2'], { first: 'Albert', last: 'Einstein' })).toEqualBoth(
    expect.anything(),
    { first: expect.any(String), last: expect.any(String) },
  );
});

The .toBeLeftWithErrorsMatching(Array<string | RegExp>) matcher does not accept asymmetric matchers. You can use standard jest matchers to achieve similar results like so:

import * as t from 'io-ts';
import { PathReporter } from 'io-ts/lib/PathReporter';
import { toBeLeftWithErrorsMatching } from '@relmify/jest-fp-ts';

expect.extend({ toBeLeftWithErrorsMatching });

const Name = t.type({
  first: t.string,
  last: t.string,
});
type Name = t.TypeOf<typeof Name>;

const receivedName = { first: 1, last: undefined };
const validation = Name.decode(receivedName);
const errorStrings = PathReporter.report(validation);

// Snapshots below use `@relmify/jest-serializer-strip-ansi` and `jest-snapshot-serializer-raw` to produce
// more readable snapshot output.
describe('Alternative ways to test validation errors', () => {
  test('Standard asymmetric matchers can be used to test for strings within pathReporter output', () => {
    expect(errorStrings).toEqual(
      expect.arrayContaining([expect.stringMatching('1'), expect.stringMatching(/undefined/)]),
    );
  });
  test('Standard snapshot tests can be used to test full pathReporter output', () => {
    expect(errorStrings).toMatchInlineSnapshot(`
      [
        Invalid value 1 supplied to : { first: string, last: string }/first: string,
        Invalid value undefined supplied to : { first: string, last: string }/last: string,
      ]
    `);
  });
  test('Standard snapshot tests can be used to test the raw array of validation errors (verbose!)', () => {
    expect(validation).toMatchInlineSnapshot(`
      {
        _tag: Left,
        left: [
          {
            context: [
              {
                actual: {
                  first: 1,
                  last: undefined,
                },
                key: ,
                type: InterfaceType {
                  _tag: InterfaceType,
                  decode: [Function],
                  encode: [Function],
                  is: [Function],
                  name: { first: string, last: string },
                  props: {
                    first: StringType {
                      _tag: StringType,
                      decode: [Function],
                      encode: [Function],
                      is: [Function],
                      name: string,
                      validate: [Function],
                    },
                    last: StringType {
                      _tag: StringType,
                      decode: [Function],
                      encode: [Function],
                      is: [Function],
                      name: string,
                      validate: [Function],
                    },
                  },
                  validate: [Function],
                },
              },
              {
                actual: 1,
                key: first,
                type: StringType {
                  _tag: StringType,
                  decode: [Function],
                  encode: [Function],
                  is: [Function],
                  name: string,
                  validate: [Function],
                },
              },
            ],
            message: undefined,
            value: 1,
          },
          {
            context: [
              {
                actual: {
                  first: 1,
                  last: undefined,
                },
                key: ,
                type: InterfaceType {
                  _tag: InterfaceType,
                  decode: [Function],
                  encode: [Function],
                  is: [Function],
                  name: { first: string, last: string },
                  props: {
                    first: StringType {
                      _tag: StringType,
                      decode: [Function],
                      encode: [Function],
                      is: [Function],
                      name: string,
                      validate: [Function],
                    },
                    last: StringType {
                      _tag: StringType,
                      decode: [Function],
                      encode: [Function],
                      is: [Function],
                      name: string,
                      validate: [Function],
                    },
                  },
                  validate: [Function],
                },
              },
              {
                actual: undefined,
                key: last,
                type: StringType {
                  _tag: StringType,
                  decode: [Function],
                  encode: [Function],
                  is: [Function],
                  name: string,
                  validate: [Function],
                },
              },
            ],
            message: undefined,
            value: undefined,
          },
        ],
      }
    `);
  });
});

LICENSE

MIT

Contributing

If you've come here to help contribute - Thanks! Take a look at CONTRIBUTING to see how to get started.

2.1.1

11 months ago

2.1.0

11 months ago

2.0.2

2 years ago

2.0.1

2 years ago

2.0.0

2 years ago

1.1.1

4 years ago

1.1.0

4 years ago

1.0.1

4 years ago

1.0.0

4 years ago