How to add custom message to Jest expect? - javascript

Image following test case:
it('valid emails checks', () => {
['abc#y.com', 'a#b.nz'/*, ...*/].map(mail => {
expect(isValid(mail)).toBe(true);
});
});
I would like to add auto-generated message for each email like Email 'f#f.com' should be valid so that it's easy to find failing test cases.
Something like:
// .map(email =>
expect(isValid(email), `Email ${email} should be valid`).toBe(true);
Is it possible in Jest ?
In Chai it was possible to do with second parameter like expect(value, 'custom fail message').to.be... and in Jasmine seems like it's done with .because clause. But cannot find solution in Jest.

You try this lib that extends jest: https://github.com/mattphillips/jest-expect-message
test('returns 2 when adding 1 and 1', () => {
expect(1 + 1, 'Woah this should be 2!').toBe(3);
});

I don't think it's possible to provide a message like that. But you could define your own matcher.
For example you could create a toBeValid(validator) matcher:
expect.extend({
toBeValid(received, validator) {
if (validator(received)) {
return {
message: () => `Email ${received} should NOT be valid`,
pass: true
};
} else {
return {
message: () => `Email ${received} should be valid`,
pass: false
};
}
}
});
And then you use it like this:
expect(mail).toBeValid(isValid);
Note: toBeValid returns a message for both cases (success and failure), because it allows you to use .not. The test will fail with the corresponding message depending on whether you want it to pass the validation.
expect(mail).toBeValid(isValid);
// pass === true: Test passes
// pass === false: Failure: Email ... should be valid
expect(mail).not.toBeValid(isValid);
// pass === true: Failure: Email ... should NOT be valid
// pass === false: Test passes

Although it's not a general solution, for the common case of wanting a custom exception message to distinguish items in a loop, you can instead use Jest's test.each.
For example, your sample code:
it('valid emails checks', () => {
['abc#y.com', 'a#b.nz'/*, ...*/].map(mail => {
expect(isValid(mail)).toBe(true);
});
});
Could instead become
test.each(['abc#y.com', 'a#b.nz'/*, ...*/])(
'checks that email %s is valid',
mail => {
expect(isValid(mail)).toBe(true);
}
);

2021 answer
I did this in some code I was writing for Mintbean by putting my it blocks inside forEach.
By doing this, I was able to achieve a very good approximation of what you're describing.
Pros:
Excellent "native" error reports
Counts the assertion as its own test
No plugins needed.
Here's what your code would look like with my method:
// you can't nest "it" blocks within each other,
// so this needs to be inside a describe block.
describe('valid emails checks', () => {
['abc#y.com', 'a#b.nz'/*, ...*/].forEach(mail => {
// here is where the magic happens
it(`accepts ${mail} as a valid email`, () => {
expect(isValid(mail)).toBe(true);
})
});
});
Errors then show up like this.
Notice how nice these are!
FAIL path/to/your.test.js
● valid emails checks › accepts abc#y.com as a valid email
expect(received).toBe(expected)
Expected: "abc#y.com"
Received: "xyz#y.com"
19 | // here is where the magic happens
20 | it(`accepts ${mail} as a valid email`, () => {
> 21 | expect(isValid(mail)).toBe(true);
^
22 | })

You can use try-catch:
try {
expect(methodThatReturnsBoolean(inputValue)).toBeTruthy();
}
catch (e) {
throw new Error(`Something went wrong with value ${JSON.stringify(inputValue)}`, e);
}

Another way to add a custom error message is by using the fail() method:
it('valid emails checks', (done) => {
['abc#y.com', 'a#b.nz'/*, ...*/].map(mail => {
if (!isValid(mail)) {
done.fail(`Email '${mail}' should be valid`)
} else {
done()
}
})
})

Just had to deal with this myself I think I'll make a PR to it possibly: But this could work with whatever you'd like. Basically, you make a custom method that allows the curried function to have a custom message as a third parameter.
It's important to remember that expect will set your first parameter (the one that goes into expect(akaThisThing) as the first parameter of your custom function.
For a generic Jest Message extender which can fit whatever Jest matching you'd already be able to use and then add a little bit of flourish:
expect.extend({
toEqualMessage(received, expected, custom) {
let pass = true;
let message = '';
try {
// use the method from Jest that you want to extend
// in a try block
expect(received).toEqual(expected);
} catch (e) {
pass = false;
message = `${e}\nCustom Message: ${custom}`;
}
return {
pass,
message: () => message,
expected,
received
};
}
});
declare global {
// eslint-disable-next-line #typescript-eslint/no-namespace
namespace jest {
// eslint-disable-next-line #typescript-eslint/naming-convention
interface Matchers<R> {
toEqualMessage(a: unknown, b: string): R;
}
}
}
Will show up like:
Error: expect(received).toEqual(expected) // deep equality
Expected: 26
Received: 13
Custom Message: Sad Message Indicating failure :(
For specific look inside the expect(actualObject).toBe() in case that helps your use case:
import diff from 'jest-diff'
expect.extend({
toBeMessage (received, expected, msg) {
const pass = expected === received
const message = pass
? () => `${this.utils.matcherHint('.not.toBe')}\n\n` +
`Expected value to not be (using ===):\n` +
` ${this.utils.printExpected(expected)}\n` +
`Received:\n` +
` ${this.utils.printReceived(received)}`
: () => {
const diffString = diff(expected, received, {
expand: this.expand
})
return `${this.utils.matcherHint('.toBe')}\n\n` +
`Expected value to be (using ===):\n` +
` ${this.utils.printExpected(expected)}\n` +
`Received:\n` +
` ${this.utils.printReceived(received)}` +
`${(diffString ? `\n\nDifference:\n\n${diffString}` : '')}\n` +
`${(msg ? `Custom:\n ${msg}` : '')}`
}
return { actual: received, message, pass }
}
})
// usage:
expect(myThing).toBeMessage(expectedArray, ' was not actually the expected array :(')

you can use this: (you can define it inside the test)
expect.extend({
ToBeMatch(expect, toBe, Msg) { //Msg is the message you pass as parameter
const pass = expect === toBe;
if(pass){//pass = true its ok
return {
pass: pass,
message: () => 'No ERRORS ',
};
}else{//not pass
return {
pass: pass,
message: () => 'Error in Field '+Msg + ' expect ' + ' ('+expect+') ' + 'recived '+'('+toBe+')',
};
}
}, });
and use it like this
let z = 'TheMassageYouWantWhenErrror';
expect(first.name).ToBeMatch(second.name,z);

I end up just testing the condition with logic and then using the fail() with a string template.
i.e.
it('key should not be found in object', () => {
for (const key in object) {
if (Object.prototype.hasOwnProperty.call(object, key)) {
const element = object[key];
if (element["someKeyName"] === false) {
if (someCheckerSet.includes(key) === false) {
fail(`${key} was not found in someCheckerSet.`)
}
}

To expand on #Zargold's answer:
For more options like the comment below, see MatcherHintOptions doc
// custom matcher - omit expected
expect.extend({
toBeAccessible(received) {
if (pass) return { pass };
return {
pass,
message: () =>
`${this.utils.matcherHint('toBeAccessible', 'received', '', {
comment: 'visible to screen readers',
})}\n
Expected: ${this.utils.printExpected(true)}
Received: ${this.utils.printReceived(false)}`,
};
}
// custom matcher - include expected
expect.extend({
toBeAccessible(received) {
if (pass) return { pass };
return {
pass,
message: () =>
`${this.utils.matcherHint('toBeAccessible', 'received', 'expected', { // <--
comment: 'visible to screen readers',
})}\n
Expected: ${this.utils.printExpected(true)}
Received: ${this.utils.printReceived(false)}`,
};
}

You can rewrite the expect assertion to use toThrow() or not.toThrow(). Then throw an Error with your custom text. jest will include the custom text in the output.
// Closure which returns function which may throw
function isValid (email) {
return () => {
// replace with a real test!
if (email !== 'some#example.com') {
throw new Error(`Email ${email} not valid`)
}
}
}
expect(isValid(email)).not.toThrow()

I'm usually using something like
it('all numbers should be in the 0-60 or 180-360 range', async () => {
const numbers = [0, 30, 180, 120];
for (const number of numbers) {
if ((number >= 0 && number <= 60) || (number >= 180 && number <= 360)) {
console.log('All good');
} else {
expect(number).toBe('number between 0-60 or 180-360');
}
}
});
Generates:

Instead of using the value, I pass in a tuple with a descriptive label. For example, when asserting form validation state, I iterate over the labels I want to be marked as invalid like so:
errorFields.forEach((label) => {
const field = getByLabelText(label);
expect(field.getAttribute('aria-invalid')).toStrictEqual('true');
});
Which gives the following error message:
expect(received).toStrictEqual(expected) // deep equality
- Expected - 1
+ Received + 1
Array [
"Day",
- "false",
+ "true",
]

Related

How can I update a global variable in Cypress tests?

I want to use a Global variable inside my Cypress test file but its value isn't changing as expected despite adding waits.
const builder = {
stepsArr: []
};
describe('my test', () => {
beforeEach(() => {
cy.intercept('/graphql', (req) => {
req.continue((res) => {
if (res.body.data?.steps) {
builder.stepsArr = res.body.data.steps.steps;
console.log({ stepsArr: builder.stepsArr }); // logs correctly!
}
});
}).as('graphqlRequest');
});
it.only('should check global var', () => {
cy.waitFor('steps');
cy.wrap({ getStepByTitle: pathwayBuilder.getStepByTitle })
.invoke('getStepByTitle',
'some title',
builder.stepsArr // always empty array!
)
.then((stepObj) => {
cy.log(stepObj);
});
});
});
The order of execution is correct but the value of Global variable isn't updating. Its showing empty array when I invoke my function despite retrying for like 100 times. What could be wrong?
cy.waitFor('steps'); is from a command in support/commands.js file
Cypress.Commands.add('waitFor', operationName => {
cy.wait('#graphqlRequest').then(({ request }) => {
if (request.body.operationName !== operationName) {
cy.log('Waiting for:', operationName)
return cy.waitFor(operationName)
}
return null
})
})
The function just logs the parameters on console
exports.pathwayBuilder = {
getStepByTitle: (title, steps) => {
console.log("Search", title);
console.log("Steps", steps); // empty!
}
}
I think the .invoke() args are wrong, see this example invoke() - functions with arguments
const fn = (a, b, c) => {
return a + b + c
}
cy.wrap({ sum: fn })
.invoke('sum', 2, 4, 6)
.should('be.gt', 10) // true
.and('be.lt', 20) // true
...the function takes three arguments and they are passed in comma-separated.
You getStepByTitle accepts two arguments, but you pass in one - an object containing the second argument
so
.invoke('getStepByTitle', {
steps: builder.stepsArr // always empty array!
})
should be
.invoke('getStepByTitle', 'aTitle', builder.stepsArr )
Some more things I found in running the test
getStepByTitle() needs to return something, otherwise stepObj (the result of the .invoke()) will be undefined
the cy.wait() does not succeed (for me) inside the custom command, but it does work if I inline the code of the custom command in the test (for ref Cypress 7.1.0)
the cy.wrap({ getStepByTitle }).invoke(... is evaluating before the cy.wait() finishes. Looks like some sort of optimization, Cypress is invoking when the command is added to the queue.
Substitute
.then(obj => obj.getStepByTitle('some title', builder.stepsArr))`
for
.invoke('getStepByTitle', 'some title', builder.stepsArr )

Assignment of Nuxt Axios response to variable changes response data content

async fetch() {
try {
console.log(await this.$api.events.all(-1, false)); // <-- First log statement
const res = await this.$api.events.all(-1, false); // <-- Assignment
console.log(res); // <-- Second log statement
if (!this.events) {
this.events = []
}
res.data.forEach((event, index) => {
const id = event.hashid;
const existingIndex = this.events.findIndex((other) => {
return other.hashid = id;
});
if (existingIndex == -1) {
this.events.push(events);
} else {
this.events[existingIndex] = event;
}
});
for (var i = this.events.length - 1; i >= 0; --i) {
const id = this.events[i].hashid
const wasRemoved =
res.data.findIndex((event) => {
return event.hashid == id
}) == -1
if (wasRemoved) {
this.events.splice(i, 1)
}
}
this.$store.commit('cache/updateEventData', {
updated_at: new Date(Date.now()),
data: this.events
});
} catch (err) {
console.log(err)
}
}
// The other functions, maybe this somehow helps
async function refreshTokenFirstThen(adminApi, func) {
await adminApi.refreshAsync();
return func();
}
all(count = -1, description = true) {
const func = () => {
return $axios.get(`${baseURL}/admin/event`, {
'params': {
'count': count,
'description': description ? 1 : 0
},
'headers': {
'Authorization': `Bearer ${store.state.admin.token}`
}
});
}
if (store.getters["admin/isTokenExpired"]) {
return refreshTokenFirstThen(adminApi, func);
}
return func();
},
Both log statements are giving slightly different results even though the same result is expected. But this only happens when is use the function in this specific component. When using the same function in other components, everything works as expected.
First data output:
[
{
"name": "First Name",
"hashid": "VQW9xg7j",
// some more correct attributes
},
{
"name": "Second name",
"hashid": "zlWvEgxQ",
// some more correct attributes
}
]
While the second console.log gives the following output:
[
{
"name": "First Name",
"hashid": "zlWvEgxQ",
// some more correct attributes, but this time with reactiveGetter and reactiveSetter
<get hashid()>: reactiveGetter()
​​ length: 0
​​​​ name: "reactiveGetter"
​​​​ prototype: Object { … }
​​​​<prototype>: function ()
​​​<set hashid()>: reactiveSetter(newVal)
​​​​length: 1
​​​​name: "reactiveSetter"
​​​​prototype: Object { … }
​​​​<prototype>: function ()
},
{
"name": "Second name",
"hashid": "zlWvEgxQ",
// some more correct attributes and still without reactiveGetter and reactiveSetter
}
]
As it can be seen, somehow the value of my hashid attribute changes, when assigning the response of the function call.
The next weird behavior happening here, is that the first object where the hashid field changes also gets reactiveGetter and reactiveSetter (but the second object in the array does not get these).
So it looks to me like something is happening with the assignment that I don't know about. Another guess would be that this has something to do with the Vuex store, because I do not change the Vuex tore in the other place where I use the same function.
It is verified that the backend always sends the correct data, as this is dummy data, consisting of an array with two objects with some attributes. So no other data except this two objects is expected.
Can someone explain to me why this behavior occurs?
There are few problems...
Do not use console.log with objects. Browsers tend to show "live view" of object - reference
this.events.findIndex((other) => { return other.hashid = id; }); is wrong, you are using assignment operator (=) instead of identity operator (===). That's why the hashid of the first element changes...

How to remove duplication from unit tests JEST?

Yesterday, I wrote a unit test with Jest and Today I figure out that I made some duplicated code that does the same test.
I have a file: null.spec.js that contain these tests :
import ArrayNull from "../../../../../src/1.x.x/scripts/array/has/null";
describe("Array has any null value", () => {
.......
it("should throw error if the parameter is not an array", () => {
function passNumber() {
ArrayNull.hasAnyNull(0);
}
function passString() {
ArrayNull.hasAnyNull("item");
}
expect(passNumber).toThrowError("The parameter should be an array");
expect(passString).toThrowError("The parameter should be an array");
});
it("should throw error if the parameter is undefined or null", () => {
function passUndefinedOrNull() {
ArrayNull.hasAnyNull(undefined || null);
}
expect(passUndefinedOrNull).toThrowError("The parameter is null or undefined");
});
it("should throw error if the array is empty", () => {
function pasEmptyArray() {
ArrayNull.hasAnyNull([]);
}
expect(pasEmptyArray).toThrowError("The array is empty");
});
});
describe("Array has at least a null value", () => {
...........
it("should throw error if the parameter is not an array", () => {
function passNumber() {
ArrayNull.hasAtLeastNull(0);
}
function passString() {
ArrayNull.hasAtLeastNull("item");
}
expect(passNumber).toThrowError("The parameter should be an array");
expect(passString).toThrowError("The parameter should be an array");
});
it("should throw error if the array is empty", () => {
function pasEmptyArray() {
ArrayNull.hasAtLeastNull([]);
}
expect(pasEmptyArray).toThrowError("The array is empty");
});
it("should throw error when the parameter is undefined or null", () => {
function passUndefinedOrNull() {
ArrayNull.hasAtLeastNull(undefined || null);
}
expect(passUndefinedOrNull).toThrowError("The parameter is null or undefined");
});
});
Look how I wrote redundantly it("should throw an error if the parameter is not an array", callback) in each test even it does the same thing: Throw an error but with Different Functions
The first funtion is: hasAnyNull()
The second function is: hasAtLeastNull()
How can I remove this duplication between describe("Array has any null value", callback) and
describe("Array has at least a null value", callback)`?
Multiple test blocks can be generated by iterating over values:
[
['has any null', hasAnyNull],
['has at least a null', hasAtLeastNull]
].forEach(([description, fn]) => {
describe(`Array ${description} value`, () => {
it(...);
});
});
This can be done in-place or extracted to a helper function.
When it comes to repeated describe blocks, this is exactly what describe.each does, with the addition of description formatting:
describe.each([
['has any null', hasAnyNull],
['has at least a null', hasAtLeastNull]
])('Array %s value', (_description, fn) => {
it(...);
});
Deduplication isn't an end in itself. If DRY code becomes less readable and error-prone than WET code this may be a problem in application code but it is certainly a problem in tests. If application code fails the expectations because of complexity, this is expected to be detected in tests; this doesn't apply to tests themselves.
I solved the DRY problem by :
Make all errors throwing in one it("should throw an error") without details.
implementing top-level function _throwError(func) .
Pass a callback func inside it.
Previous Code :
it("should throw error if the parameter is not an array", () => {
function passNumber() {
ArrayNull.hasAnyNull(0);
}
function passString() {
ArrayNull.hasAnyNull("item");
}
expect(passNumber).toThrowError("The parameter should be an array");
expect(passString).toThrowError("The parameter should be an array");
});
it("should throw error if the parameter is undefined or null", () => {
function passUndefinedOrNull() {
ArrayNull.hasAnyNull(undefined || null);
}
expect(passUndefinedOrNull).toThrowError("The parameter is null or undefined");
});
it("should throw error if the array is empty", () => {
function pasEmptyArray() {
ArrayNull.hasAnyNull([]);
}
expect(pasEmptyArray).toThrowError("The array is empty");
});
Current Code :
it("should throw an error", () => {
__throwError(ArrayNull.hasAnyNull);
});
__throwError Function:
function __throwError(func) {
function passNumber() {
func.length === 1 ? func(0) : func(0, 1)
}
function passString() {
func.length === 1 ? func("item") : func("item", 1);
}
function passEmptyArray() {
func.length === 1 ? func([]) : func([], 1);
}
function passUndefinedOrNull() {
func.length === 1 ? func(undefined || null) : func(undefined || null, 1);
}
expect(passNumber).toThrowError("The parameter should be an array");
expect(passString).toThrowError("The parameter should be an array");
expect(passEmptyArray).toThrowError("The array is empty");
expect(passUndefinedOrNull).toThrowError("The parameter is null or undefined");
}

Converting Async Response to Standard Javascript Object

Imagine your React app gets a response like this:
email_address
first_name
last_name
What's a best practice way to convert these fields to something more common in Javascript:
emailAddress
firstName
lastName
Also keeping mind that there could be nested structures.
I've typically done this immediately when the response is received.
My colleagues seem to think it's fine to keep the snake_case syntax persist through the app.
There may be some edge cases that fail, I could not find anything on github that would do the trick but if you have any errors then please let me know.
It is assuming you only pass object literals to it, maybe you can write some tests and tell me if anything fails:
const snakeToCamel = snakeCased => {
// Use a regular expression to find the underscores + the next letter
return snakeCased.replace(/(_\w)/g, function(match) {
// Convert to upper case and ignore the first char (=the underscore)
return match.toUpperCase().substr(1);
});
};
const toCamel = object => {
if (Array.isArray(object)) {
return object.map(toCamel);
}
if (typeof object === 'object' && object !== null) {
return Object.entries(object).reduce(
(result, [key, value]) => {
result[snakeToCamel(key)] = toCamel(value);
return result;
},
{}
);
}
return object;
};
console.log(
toCamel({
arra_of_things: [
{ thing_one: null },
{ two: { sub_item: 22 } },
],
sub_one: {
sub_two: {
sub_three: {
sub_four: {
sub_four_value: 22,
},
},
},
},
})
);

test case failing due to .map is not a function error

Hi i have a react component expenses-total.js and a corresponding test case expenses-total.test.js as shown below.
expenses-total.js
export default (expenses=[]) => {
if (expenses.length === 0) {
return 0;
} else {
return expenses
.map(expense => expense.amount)
.reduce((sum, val) => sum + val, 0);
}
};
expenses-total.test.js
import selectExpensesTotal from '../../selectors/expenses-total';
const expenses = [
{
id: "1",
description: "gum",
amount: 321,
createdAt: 1000,
note: ""
},
{
id: "2",
description: "rent",
amount: 3212,
createdAt: 4000,
note: ""
},
{
id: "3",
description: "Coffee",
amount: 3214,
createdAt: 5000,
note: ""
}
];
test('Should return 0 if no expenses', ()=>{
const res = selectExpensesTotal([]);
expect(res).toBe(0);
});
test('Should correctly add up a single expense', ()=>{
const res = selectExpensesTotal(expenses[0]);
expect(res).toBe(321);
});
test('Should correctly add up multiple expenses',()=>{
const res = selectExpensesTotal(expenses);
expect(res).toBe(6747);
});
when i run the test case, its getting failed by giving an error
TypeError: expenses.map is not a function
I know the test case is correct but dont know what is wrong with thecomponent.
Could anyone please help me in fixing this error?
The problem is with if (expenses.length === 0) and the test case that uses selectExpensesTotal(expenses[0]):
expenses[0] passes an object, which has no length property, so in the function being tested, expenses.length returns undefined. However, undefined === 0 evaluates to false so your code goes into the else block tries to use .map on the object, which doesn't have that function, thus it throws an error.
In a brief: you can't map over an object.
expenses is an array of objects, so expenses[0] is an object.
Condition expenses.length === 0 evaluates to false, since obviously .length property does not exist on Object.prototype, so the else condition takes place - your function tries to map over an object.
The problem is that expenses[0] is an object (you probably expected it to be an array) and an object does not have a map function. A quick hack would be to add another ifs into the loop to check if expenses is actually an object. So that:
export default (expenses=[]) => {
if (expenses.length === 0) {
return 0;
} else {
if (typeof expenses === 'object') {
return expenses.amount
} else {
return expenses
.map(expense => expense.amount)
.reduce((sum, val) => sum + val, 0);
}
}
};
I hope this help.
To fix this error, you can pass in an array of object into
selectExpensesTotal([expenses[0]])
rather than just an object
selectExpensesTotal(expenses[0])
So your code show look like this:
test('Should correctly add up a single expense', ()=>{
const res = selectExpensesTotal([expenses[0]]);
expect(res).toBe(321);
});
.map function will now work on expenses. Because, this is now an array of object ( works with map function ) and not an object(This does not work with map function)

Categories

Resources