@selfage/test_matcher
v2.1.8
Published
A pattern to match actual value with expected in tests.
Downloads
169
Readme
@selfage/test_matcher
Install
npm install @selfage/test_matcher
Overview
Written in TypeScript and compiled to ES6 with inline source map & source. See @selfage/tsconfig for full compiler options. Provides basic assertions and matchers, which can be easily extended to custom data structures, promoting a framework or a pattern for matching actual value with expected in tests.
Basic matchers
A matcher means a function that returns MatchFn<T>
such as eq()
and
containStr()
. It's best to be used together with assertThat()
to populate
error messages, and reads more naturally.
// import other stuff...
import {
assertThat, eq, containStr, isArray, isSet, isMap
} from "@selfage/test_matcher";
// Usually called within a test case, if assertThat() fails, it throws an error
// which fails the rest of the test case.
// Equality is checked via `===`. Any type can be compared. The last argument
// `targetName` is filled into the error message `When matching ${targetName}:`,
// when assertion failed. In a test case, you can simply use the variable name,
// or anything you want to help you understand which assertion failed. You will
// see more why this is helpful in debugging when customizing matchers.
assertThat(actual, eq(expected), `actual`);
// `actual` must be a string that contains the expected text.
assertThat(actual, containStr('expected text'), `actual`);
// `actual` can be anything and will be ignored or always succeed.
assertThat(actual, ignore(), `actual`);
// `actual` must be an array of only one type.
// isArray() takes an array of matchers to match each element from `actual`.
assertThat(actual, isArray([eq(1), eq(2)]), `actual`);
// Therefore it can also be used like this.
assertThat(
actual,
isArray([containStr('expected1'), eq('expected2'), ignore()]),
`actual`);
// If `actual` is undefined, it can be matched as the following.
assertThat(actual, isArray(), `actual`);
// `actual` must be a Set of only one type.
// isSet() also takes an array of matchers just like isArray() to match Set in
// insertion order.
assertThat(actual, isSet([eq(1), eq(2)]), `actual`);
// If `actual` is undefined, it can be matched as the following.
assertThat(actual, isSet(), `actual`);
// `actual` must be a Map of one key type and one value type.
// isMap() takes an array of pairs of matchers, to match key and value in
// insertion order.
assertThat(
actual,
isMap([[eq('key'), eq('value')], [eq('key2'), eq('value2')]]),
`actual`);
// If `actual` is undefined, it can be matched as the following.
assertThat(actual, isMap(), `actual`);
Assert upon error
Often we need to assert when a function throws an error, which can be helped by
assertReject()
, assertThrow()
, and eqError()
.
import {
assertThat, assertReject, assertThrow, eqError
} from '@selfage/test_matcher';
// Suppose we are in an async function.
let e = await assertReject(() => {
return Promise.reject(new Error('It has to fail.'));
});
// `eqError()` expects the `actual` to be of `Error` type, to have the expected
// error name, and to contain (not equal to) the error message.
assertThat(e, eqError(new Error('has to')), `e`);
// If the function doesn't return a Promise.
let e = assertThrow(() => {
// Suppose we are calling some function that would throw an error.
throw an Error('It has to fail.');
});
assertThat(e, eqError(new Error('has to')), `e`);
Customized matcher
Once you have your own data class, it becomes a pain to match each field in each test. A customized matcher can help to ease the matching process.
import {
assert, assertThat, eq, isArray, MatchFn
} from '@selfage/test_matcher';
// Suppose we define the following interfaces as data classes.
interface User {
id?: number;
name?: string;
channelIds?: number[];
creditCard?: CreditCard[];
}
interface CreditCard {
cardNumber?: string;
cvv?: string;
}
// The eventual goal is to define a matcher that works like the following, where
// both `actual` and `expected` follow the interface definition, but are of
// different instances, i.e., they are not equal by `===`.
assertThat(actual, eqUser(expected), `actual`);
// The `T` in `MatchFn<T>` is the usually the type of the actual value you want
// to match. In this case, `T` should be `User`. eqUser() is just an example
// name which is really up to you. But following this naming convention makes
// the assertion statement reads more naturally.
function eqUser(expected?: User): MatchFn<User> {
// `MatchFn<T>` is just an alias of a function type.
// type MatchFn<T> = (actual: T) => void;
// You will need to compare the actual value with the expected value in the
// function body, and throw an error if anything is not matched, instead of
// returning a boolean if you were wondering.
return (actual) => {
// When using assertThat(), the last argument `targetName` is used to
// construct `When matching ${targetName}:` and it needs to be descriptive
// enough for you to locate the failure within this matcher, because
// `eqUser()` can be used inside other matchers, such as isArray().
if (expected === undefined) {
// Supports matching when we expect `actual` to be undefined.
assertThat(actual, eq(undefined), `nullity`);
}
assertThat(actual.id, eq(expected.id), `id field`);
assertThat(actual.name, eq(expected.name), `name field`);
// Because we can expect `actual.channelIds` to be undefined, the expected
// array needs to be undefined in that case as well.
let channelIds: MatchFn<number>[];
if (expected.channelIds) {
// Because isArray() takes an array of matchers, we need to convert
// `expected.channelIds` into `MatchFn<number>[]`.
channelIds = expected.channelIds.map((channelId) => eq(channelId));
}
assertThat(actual.channelIds, isArray(channelIds), `channelIds field`);
// Similarly, let's convert `expected.creditCards` into
// `MatchFn<CreditCard>[]`.
let creditCards: MatchFn<CreditCard>[];
if (expected.creditCards) {
// Well eqCreditCard() doesn't exist. Let's define it below.
creditCards = expected.creditCards.map(
(channelId) => eqCreditCard(channelId)
);
}
assertThat(actual.creditCards, isArray(creditCards), `creditCards field`);
};
}
function eqCreditCard(expected?: CreditCard): MatchFn<CreditCard> {
return (actual) => {
if (expected === undefined) {
assertThat(actual, eq(undefined), `nullity`);
}
assertThat(actual.cardNumber, eq(expected.cardNumber), `cardNumber field`);
assertThat(actual.cvv, eq(expected.cvv), `cvv field`);
// If there are no exisitng matchers to help, you can fallback to use
// `assert()`.
assert(
/^[0-9]$/.test(actual.cardNumber),
`cardNumber to be of numbers only`,
actual.cardNumber
);
// Or fallback to simply throw an error, if we re-write the above assertion
// as the following.
if (!(/^[0-9]$/.test(actual.cardNumber))) {
throw Error(
`Expect cardNumber to be of numbers only but it actually is ` +
`${actual.cardNumber}.`
);
}
};
}
// The input to a matcher is also up to you, as long as it returns `MatchFn<T>`.
// Hard-coded expected user.
function eqACertainUser(): MatchFn<User> {
return (actual) => {
// Match `actual` with a const User instance.
};
}
assertThat(actual, eqACertainUser(), `actual`);
// Options to ignore certain fields.
function eqUserWithOptions(expected?: User, ignoreId: boolean): MatchFn<User> {
return (actual) => {
// Same as `eqUser()` except don't assert on id field, if `ignoreId` is
// true.
};
}
assertThat(actual, eqUserWithOptions(expected, true), `actual`);
Async matcher
There are no out-of-the-box matchers that are async. But in case you need customized matchers to do async things, such as reading from files, you can implement AsyncMatcherFn<T>
which returns Promise<void>
, and assert with asyncAssertThat()
.
function asyncEq(expected: number): AsyncMatcherFn<number> {
return async (actual) => {
await ...
}
}
await asyncAssertThat(actual, asyncEq(expected), `actual`);