How to remove duplication from unit tests JEST? - javascript

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");
}

Related

Filter Object passed on Value Properties

I have an object like this:
let obj = {'machine1':{'err':'404', switch:false},
'gadget1':{'err':'404', switch:true}}
Would not pass validation. => return false
let obj2 = {'machine2':{'err':'404', switch:false},
'gadget2':{'err':null, switch:true}}
Would pass validation. => return true
I want to return true when every key in the object in which the switch is set to true does not have any errors (see obj2). (For machine2 the '404' error does not matter, because the switch is set to false.)
I tried something like that, but didn't get far.
function try() {
for (x in obj) {
obj[x].filter(x=>y.switch === true)
}
}
Thanks for reading!
you could do something like the follow:
const test = obj => Object.keys(obj).every(k => !obj[k].switch || obj[k].err === null)
So you check if every key in the object has switch set to false or the err equal to null.
You can do it by usign entries and every helpers of Array, like this:
let obj = {
'machine1': {
'err': '404',
switch: false
},
'gadget1': {
'err': '404',
switch: true
}
}
let obj2 = {
'machine2': {
'err': '404',
switch: false
},
'gadget2': {
'err': null,
switch: true
}
};
const validate = (data) => {
return Object.entries(data).every(([key, value]) => !value.switch || value.err === null)
}
console.log(validate(obj));
console.log(validate(obj2));
Did you mean something like that?
let obj={
1:{
'machine':{'err':'404', switch:false},
'gadget':{'err':'404', switch:true}
},
2:{
'machine':{'err':'404', switch:false},
'gadget':{'err':null, switch:true}
}
}
I changed the facility a bit to make it easier.
var n=1;
while (n<=2) {
if(obj[n].machine.switch==true){
if(obj[n].machine.err!='404')
console.log('SUKCES')
else console.log('ERROR 1')
}
else console.log('ERROR 0')
if(obj[n].gadget.switch==true){
if(obj[n].gadget.err!='404')
console.log('SUKCES')
else console.log('ERROR 1')
}
else console.log('ERROR 0')
n++;
}
Results:
ERROR 0
ERROR 1
ERROR 0
SUKCES

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...

TypeError: Failed to construct 'Touch': 1 argument required, but only 0 present

why i am getting this error?
i want to write a spec for one function, i wrote it but it shows above error
Here i added my code
describe("getcomponent funtion", function () {
it("if statement", function () {
let comp: any = { ej2_instances: [new Touch()] };
expect(getComponent(comp, 'touch') instanceof Touch).toBe(true);
});
it("else statement", function () {
let comp: any = { ej2_instances: [new Touch()] };
expect(getComponent(comp, 'button') instanceof Touch).not.toBe(true);
});
});
this is error->
* TypeError: Failed to construct 'Touch': 1 argument required, but only 0 present.
at UserContext.<anonymous> (D:/ej2-base-library/spec/base.spec.js:166:42)
HeadlessChrome 61.0.3163 (Windows 10.0.0) getcomponent funtion else statement FAILED
TypeError: Failed to construct 'Touch': 1 argument required, but only 0 present.
at UserContext.<anonymous> (D:/ej2-base-library/spec/base.spec.js:170:42)*
You have to give argument to "touch" function like following:
describe("getcomponent funtion", function () {
it("if statement", function () {
let elem:any =createElement('div', { id: 'test' });
let comp: any = { ej2_instances: [new Touch(elem)] };
expect(getComponent(comp, 'touch') instanceof Touch).toBe(true);
});
it("else statement", function () {
let elem:any =createElement('div', { id: 'test' });
let comp: any = { ej2_instances: [new Touch(elem)] };
expect(getComponent(comp, 'button') instanceof Touch).toBe(false);
});
});

How to add custom message to Jest expect?

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",
]

Categories

Resources