@relmify/jest-fp-ts
v2.1.1
Published
Custom Jest matchers for projects using using fp-ts and io-ts
Downloads
83,781
Maintainers
Readme
Jest Matchers for the fp-ts ecosystem
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:
- Extract the received value from the container type before using a jest matcher.
- 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.
- Problem
- Solution
- Installation
- Setup
- Matchers
- Asymmetric matchers
- LICENSE
- Contributing
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
Contributing
If you've come here to help contribute - Thanks! Take a look at CONTRIBUTING to see how to get started.