A pattern to match actual value with expected in tests.
npm install @selfage/test_matchernpm install @selfage/test_matcherA matcher means a function that returns MatchFn such as eq() andcontainStr(). It's best to be used together with assertThat() to populate
error messages, and reads more naturally.
``TypeScript
// 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 argumenttargetName
// is filled into the error message When matching ${targetName}:,actual
// 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 must be a string that contains the expected text.actual
assertThat(actual, containStr('expected text'), );
// actual can be anything and will be ignored or always succeed.actual
assertThat(actual, ignore(), );
// actual must be an array of only one type.actual
// 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 is undefined, it can be matched as the following.actual
assertThat(actual, isArray(), );
// actual must be a Set of only one type.actual
// 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 is undefined, it can be matched as the following.actual
assertThat(actual, isSet(), );
// actual must be a Map of one key type and one value type.actual
// 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 is undefined, it can be matched as the following.actual
assertThat(actual, isMap(), );`
Often we need to assert when a function throws an error, which can be helped by
assertReject(), assertThrow(), and eqError().
`TypeScript
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 expectede
// error name, and to contain (not equal to) the error message.
assertThat(e, eqError(new Error('has to')), );
// 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);`
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.
`TypeScript
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 .actual
assertThat(actual, eqUser(expected), );
// The T in MatchFn is the usually the type of the actual value you wantT
// to match. In this case, should be User. eqUser() is just an exampleMatchFn
// name which is really up to you. But following this naming convention makes
// the assertion statement reads more naturally.
function eqUser(expected?: User): MatchFn
// is just an alias of a function type.targetName
// type MatchFn
// 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 is used toWhen matching ${targetName}:
// construct and it needs to be descriptiveeqUser()
// enough for you to locate the failure within this matcher, because
// can be used inside other matchers, such as isArray().actual
if (expected === undefined) {
// Supports matching when we expect to be undefined.nullity
assertThat(actual, eq(undefined), );id field
}
assertThat(actual.id, eq(expected.id), );name field
assertThat(actual.name, eq(expected.name), );actual.channelIds
// Because we can expect to be undefined, the expectedexpected.channelIds
// array needs to be undefined in that case as well.
let channelIds: MatchFn
if (expected.channelIds) {
// Because isArray() takes an array of matchers, we need to convert
// into MatchFn.channelIds field
channelIds = expected.channelIds.map((channelId) => eq(channelId));
}
assertThat(actual.channelIds, isArray(channelIds), );expected.creditCards
// Similarly, let's convert intoMatchFn
// .creditCards field
let creditCards: MatchFn
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), );
};
}
function eqCreditCard(expected?: CreditCard): MatchFn
return (actual) => {
if (expected === undefined) {
assertThat(actual, eq(undefined), nullity);cardNumber field
}
assertThat(actual.cardNumber, eq(expected.cardNumber), );cvv field
assertThat(actual.cvv, eq(expected.cvv), );assert()
// If there are no exisitng matchers to help, you can fallback to use
// .cardNumber to be of numbers only
assert(
/^[0-9]$/.test(actual.cardNumber),
,Expect cardNumber to be of numbers only but it actually is
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(
+${actual.cardNumber}.
);
}
};
}
// The input to a matcher is also up to you, as long as it returns MatchFn.actual
// Hard-coded expected user.
function eqACertainUser(): MatchFn
return (actual) => {
// Match with a const User instance.actual
};
}
assertThat(actual, eqACertainUser(), );eqUser()
// Options to ignore certain fields.
function eqUserWithOptions(expected?: User, ignoreId: boolean): MatchFn
return (actual) => {
// Same as except don't assert on id field, if ignoreId isactual
// true.
};
}
assertThat(actual, eqUserWithOptions(expected, true), );`
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 which returns Promise, and assert with asyncAssertThat().
`TypeScript
function asyncEq(expected: number): AsyncMatcherFn
return async (actual) => {
await ...
}
}
await asyncAssertThat(actual, asyncEq(expected), actual);``