Why does jest mockResolvedValueOnce called multiple times returns the same value? - node.js

I have a class method where I trigger a #google-cloud/firestore multiple times. I would like to mock the call over the same .get() method multiple times.
Using a mockResolvedValueOnce multiple times with different values to return, the 2nd value is ignored.
jest.doMock('#google-cloud/firestore', () => class {
collection () {
return {
get: jest.fn().mockResolvedValue({
docs: []
}),
doc: () => {
return {
set: jest.fn(),
get: jest.fn().mockResolvedValueOnce({})
}
},
limit: () => {
return {
get: jest.fn().mockResolvedValue({ empty: true })
}
},
onSnapshot: jest.fn(),
select: () => {
return {
get: jest.fn() // <------------ MULTIPLE CALLS CHAINED BELOW
.mockResolvedValueOnce({
size: 1
}).mockResolvedValueOnce({
size: 2
})
}
}
}
}
})
When I console.log(snapshot.size) it returns me the same value "1" twice for both calls.
if (isEmptyModels || isStatsEmptyModels) {
// ...
console.log('📊 [STATS][MODELS] - Fulfilling the counters')
await Database.collection('models').select('id').get().then(snapshot => {
console.log(snapshot.size) // <--------- 1st call
this.fields.models.count = snapshot.size
this.fields.models.linked = snapshot.size
})
// ...
}
if (isEmptyProducts1P || isStatsEmptyProducts1P) {
// ...
console.log('📊 [STATS][PRODUCTS1P] - Fulfilling the counters')
await Database.collection('products1P').select('isMaintained').get().then(snapshot => {
console.log(snapshot.size) // <--------- 2nd call
snapshot.forEach(doc => {
if (doc.data().isMaintained) {
// ...
}
})
// ...
})
// ...
}
Why is that, and what is done wrong here ?
Error message is:
console.log
📊 [STATS][MODELS] - Fulfilling the counters
at Statistics.fulfillProductsCount (app/services/statistics/index.js:95:15)
console.log
1
at app/services/statistics/index.js:97:17
console.log
📊 [STATS][PRODUCTS1P] - Fulfilling the counters
at Statistics.fulfillProductsCount (app/services/statistics/index.js:106:15)
console.log
1
at app/services/statistics/index.js:108:17
TypeError: snapshot.forEach is not a function
117 | await Database.collection('products1P').select('isMaintained').get().then(snapshot => {
118 | console.log(snapshot.size)
> 119 | snapshot.forEach(doc => {
| ^
120 | if (doc.data().isMaintained) {
121 | this.fields.products1P.maintained += 1
122 | } else {
at app/services/statistics/index.js:119:18

This happens because each time you call Database.collection(), it creates a new object, and as a new object, this is the first time its properties are called. It is also valid for the others functions inside collection.
What I mean is that Database.collection is a function that returns an object that contains other functions that return object that contains properties mocked. By mocking this way, you will never be able to use mock...ValueOnce. But, I see two ways to "bypass" this problem :
1 - The short but conflicting way
You can use .mockReturnThis() to avoid entering into deep mock objects/functions, but in may be quickly conflicting when dealing with "fat" classes that have multiple times the same method names. In can also be helpful when mocking chainable methods (example: ORM queries with .find().filter().sort()...).
jest.doMock('#google-cloud/firestore', () => class {
collection = jest.fn().mockReturnThis();
select = jest.fn().mockReturnThis();
get = jest.fn().mockResolvedValueOnce({ size: 1 }).mockResolvedValueOnce({ size: 2 });
})
2 - The Long but working way
Mock the whole collection method Once instead of mocking only collection().select().get().
Database.collection.prototype.mockReturnValueOnce({
select: () => {
get: () => ({ size: 1 })
}
}).mockReturnValueOnce({
select: () => {
get: () => ({ size: 2 })
}
})
--> You will need access to the mocked Class and mock the method "collection" of the prototype (collection = jest.fn()).

Related

Why is the `message` for my Jest custom matcher not being displayed?

I've created a Jest custom matcher. It works (meaning, it passes/fails when it should), but I don't see the message anywhere in Jest's output.
What am I doing wrong? Do I have to do something to "enable" messages? Am I totally misunderstanding where the message is supposed to show up?
Environment: NestJS, Prisma
Execution command: jest --watch
Simplified code:
declare global {
namespace jest {
interface Matchers<R> {
toMatchHash(received: string, expected: string): R;
}
}
}
expect.extend({
toMatchJsonHash(received, expected) {
return {
pass: false,
message: () => `Why doesn't this work?!`,
};
},
});
expect(prisma.name.findMany).toHaveBeenCalledWith(expect.toMatchJsonHash('db0110285c148c77943f996a17cbaf27'));
Output:
● MyService › should pass a test using a custom matcher
expect(jest.fn()).toHaveBeenCalledWith(...expected)
Expected: toMatchJsonHash<db0110285c148c77943f996a17cbaf27>
Received: {<Big ol' object redacted for conciseness>}
Number of calls: 1
178 |
179 | // #ts-ignore
> 180 | expect(prisma.name.findMany).toHaveBeenCalledWith(expect.toMatchJsonHash('db0110285c148c77943f996a17cbaf27'));
| ^
181 | // expect(prisma.name.findMany).toHaveBeenCalledWith({
182 | // select: { type: true, name: true },
183 | // where: {
at Object.<anonymous> (my/my.service.spec.ts:180:32)
I'm expecting to see "Why doesn't this work?!" somewhere in the output, but I don't. What am I missing?
As suggested by #jonsharpe, the reason was that Jest was showing the message from the "outer" matcher, .toHaveBeenCalledWith().
To fix this, I found the source that defines the .toHaveBeenCalledWith() matcher and "merged" its code into my custom matcher.
This enabled my custom matcher to effectively "extend" the functionality of the .toHaveBeenCalledWith() matcher, including my own custom code and messages.
In case it helps someone, the code I ended up with for my specific use case was:
declare global {
namespace jest {
interface Matchers<R> {
toHaveBeenCalledWithObjectMatchingHash(expected: string): CustomMatcherResult;
}
}
}
expect.extend({toHaveBeenCalledWithObjectMatchingHash(received, expected) {
const isSpy = (received: any) =>
received != null &&
received.calls != null &&
typeof received.calls.all === 'function' &&
typeof received.calls.count === 'function';
const receivedIsSpy = isSpy(received);
const receivedName = receivedIsSpy ? 'spy' : received.getMockName();
const calls = receivedIsSpy
? received.calls.all().map((x: any) => x.args)
: received.mock.calls;
if(calls.length === 0) {
return {
pass: false,
message: () => `expected the function to be called with an object that hashes to '${expected}'. Instead, the function was not called.`,
};
}
if(calls[0].length === 0) {
return {
pass: false,
message: () => `expected the function to be called with an object that hashes to '${expected}'. Instead, the function was called, but not with any arguments.`,
};
}
const md5Hash = crypto.createHash('md5');
const receivedHash = md5Hash.update(JSON.stringify(calls[0][0])).digest('hex');
const pass = receivedHash === expected;
if(pass) {
return {
pass: true,
message: () => `expected the function to not be called with an object that hashes to '${expected}'. Instead, the passed object hashes to the same value.`,
};
} else {
return {
pass: false,
message: () => `expected the function to be called with an object that hashes to '${expected}'. Instead, the passed object hashes to '${receivedHash}'.`,
};
}
}});

Jest doMock the same method multiple times

I would like to test following part of the code:
// ... code above
const created = async payload => {
const model = await db.collection('models').doc(payload.model)
.get() // <--- 1st .get() occurence
if (!model.exists) {
// Add product to the orphans collection
await db.collection('orphans').doc(payload.sku).set(payload)
} else {
// Grab the categories field
const categories = model.get('categories') // <--- 2nd .get() occurence
// Product is either empty or does not exists at all
if (!categories || categories.length < 1) {
// Add product to the orphans collection
await db.collection('orphans').doc(payload.sku).set(payload)
} else {
// Otherwise remove from the orphans collection
await deleted(payload.sku)
}
}
}
I do not know how to properly mock the file twice in the same callback. Here is what I get:
test.only('it should react when an event "created" has been fired', async () => {
const spy = jest.fn()
jest.doMock('#google-cloud/firestore', () => class {
collection () {
return {
doc: () => {
return {
get: () => {
return {
exists: () => {
spy()
}
}
},
set: () => {
spy()
}
}
}
}
}
})
const observer = require('./product')
await observer('created', {})
await expect(spy.mock.calls.length).toBe(1)
})
I get this error:
● it should react when an event "created" has been fired
TypeError: model.get is not a function
25 | } else {
26 | // Grab the categories field
> 27 | const categories = model.get('categories')
| ^
28 |
29 | // Product is either empty or does not exists at all
30 | if (!categories || categories.length < 1) {
at created (app/observers/product.js:27:30)
at Object.<anonymous>.module.exports (app/observers/product.js:6:28)
at Object.<anonymous> (app/observers/product.spec.js:34:3)
Test Suites: 1 failed, 1 total
Tests: 1 failed, 1 skipped, 2 total
Snapshots: 0 total
Time: 0.147 s, estimated 1 s
Ran all test suites matching /app\/observers\/product.spec.js/i.
What is the working solution to test two scenarios of the same mocked get() method ?
In your code :
const model = await db.collection('models').doc(payload.model)
.get() // <--- 1st .get() occurence
If we look at your mock, the get method of doc returns :
{
exists: () => {
spy()
}
}
There are no property named get, so it is undefined (and not a function).
I guess you just have to change this part to :
{
exists: true, // can be false
get: spy,
}
And your problem should be solved.
Btw, you can also change the mock of set method to set: spy. Or you can keep it to set: () => { spy() }, but you should at least return the value if you want to mock it : set: () => { spy() }.
Now, about how to properly mock multiple times, here's what you can do :
const observer = require('./product')
const spyGet = jest.fn()
const spySet = jest.fn() // I like having different mocks, if one function use get & set, tests will be clever & more readable if you use different spies
describe('on event "created" fired', () => {
const categories = []
beforeEach(() => {
// I put mocks here to make test more readable
jest.doMock('#google-cloud/firestore', () => class {
collection () {
return {
doc: () => {
return {
get: () => {
return {
exists: true,
get: spyGet,
}
},
set: spySet
}
}
}
}
})
spyGet.mockResolvedValueOnce(categories) // you can also use mockResolvedValue, but mockResolvedValueOnce allow you to mock with different values on the same test & same mock
})
it.only('should get categories', async () => {
await observer('created', {})
// here's all the ways you can test it
expect(spyGet).toBeCalledTimes(1)
expect(spyGet.mock.calls.length).toBe(1)
expect(spyGet).toBeCalledWith('categories')
expect(spyGet).toHaveBeenNthCalledWith(1, 'categories')
})
})
Note : You should reset & clear your mocks between tests manually (in a afterEach or beforeEach) if you don't set it into jest config.

How to return a list of objects from Cypress Custom Commands in type script

I am using Cypress for my end to end Integration tests. I have a use case which involves returning a list of objects from Cypress Custom Commands and I have a difficulty in doing so. Here is my code pointer:
index.ts
declare global {
namespace Cypress {
interface Chainable<Subject> {
getTestDataFromElmoDynamoDB({locale, testType}): Cypress.Chainable<JQuery<expectedData[]>> // ??? not sure what return type should be given here.
}
}
}
Cypress.Commands.add('getTestDataFromDynamoDB', ({locale, testType}) => {
// expectedData is an interface declared. My use case is to return the list of this type.
let presetList: expectedData[]
cy.task('getTestDataFromDynamoDB', {
locale: locale,
testType: testType
}).then((presetData: any) => {
presetList = presetData;
// the whole idea here is to return presetList from cypress task
return cy.wrap(presetList) //??? not sure what should be written here
})
})
sampleSpec.ts
describe('The Sample Test', () => {
it.only('DemoTest', () => {
cy.getTestDataElmoDynamoDB({
locale: env_parameters.env.locale,
testType: "ChangePlan"
}).then((presetlist) => {
// not sure on how to access the list here. Tried wrap and alias but no luck.
presetList.forEach((preset: expectedData) => {
//blah blah blah
})
})
})
})
Did anyone work on similar use case before?
Thanks,
Saahith
Here My own command for doing exactly that.
Cypress.Commands.add("convertArrayOfAlliasedElementsToArrayOfInteractableElements", (arrayOfAlliases) => {
let arrayOfRecievedAlliasValues = []
for (let arrayElement of arrayOfAlliases) {
cy.get(arrayElement)
.then(aelement =>{
arrayOfRecievedAlliasValues.push(aelement)
})
}
return cy.wrap(arrayOfRecievedAlliasValues)
})
The way I do it is to pass it in an array and cy.wrap the array, Because it lets you chain the command with an interactable array.
The key point is - it has to be passed as array or object, because they are Reference types, and in cypress it is hard to work with let/var/const that are value types.
You can also allias the cy.wrapped object if you like.
The way to use it in code is:
cy.convertArrayOfAlliasedElementsToArrayOfInteractableElements(ArayOfElements)
What you asked for can be implemented as follows, but I do not know what type expectedData is, so let's assume that expectedData:string [], but you can replace string[] with your type.
plugins/index.ts
module.exports = (on: any, config: any) => {
on('task', {
getDataFromDB(arg: {locale: string, testType: string}){
// generate some data for an example
const list: string[] = [];
list.push('a', 'b');
return list;
},
});
};
commands.ts
declare global {
namespace Cypress {
interface Chainable<Subject> {
getTestDataElmoDynamoDB(arg: {locale: string, testType: string}): Cypress.Chainable<string[]>
}
}
}
Cypress.Commands.add('getTestDataElmoDynamoDB', (arg: {locale: string, testType: string}) => {
let presetList: string[] = [];
cy.task('getDataFromDB', arg)
.then((presetData?: string[]) => {
expect(presetData).not.be.undefined.and.not.be.empty;
// if the data is incorrect, the code will break earlier on expect, this line for typescript compiler
if (!presetData || !presetData.length) throw new Error('Present data are undefined or empty');
presetList = presetData;
return cy.wrap(presetList); // or you can return cy.wrap(presetData)
});
});
db.spec.ts
describe('Test database methods', () => {
it('When take some test data, expect that the data was received successfully ', () => {
cy.getTestDataElmoDynamoDB({ locale: 'someEnvVar', testType: 'ChangePlan' })
.then((list) => {
expect(list).not.empty.and.not.be.undefined;
cy.log(list); // [a,b]
// You can interact with list here as with a regular array, via forEach();
});
});
});
You can also access and receive data from cy.task directly in the spec file.
describe('Test database methods', () => {
it('When take some test data, expect that the data was received successfully ', () => {
cy.task('getDataFromDB', arg)
.then((list?: string[]) => {
expect(list).not.be.empty.and.not.be.undefined;
cy.log(list); // [a,b] — the same list as in the version above
});
});
});

Promise.all won't work for two mongodb queries

I am trying to merge three pieces of data at the backend into one array of objects. I am trying to figure out whether there are sufficient tickets available to proceed with the sale. The three pieces of data are:
A request from the front end to purchase a certain number of a certain type of ticket for an event. This is an array of objects.
The total number of tickets of that type for that event that have already been sold.
The number of tickets of that type that are available to sell from the the Event Document I created in MongoDB.
This is working using the following code. However, it is synchronous and I would like to do it asynchronous to speed up the site.
let numTicketsSought = req.body.numTicketsSought.map(e => { return(
Ticket.find({
userEvent: req.body.userEvent,
ticketTypeID: e.ticketTypeID
}).lean().countDocuments()
.then(number => {
e.ticketsSold = number
return e
}))
})
Promise.all(numTicketsSought).then(numTicketsSoughtdata => {
Event.findById(req.body.userEvent)
.lean()
.then(eventdata => {
numTicketsSoughtdata.map(e => {return (
eventdata.tickets.forEach(f => {
if (e.ticketTypeID === f.ticketTypeID){
e.numberOfTickets = f.numberOfTickets
e.ticketsAvailable = e.numberOfTickets - e.ticketsSold
return e
}
})
)})
})
However, when I try to run Ticket.find and Event.findbyID asynchronously (using Promise.all) the code won't run. Here is the code:
let numTicketsSought = req.body.numTicketsSought.map(e => { return(
Ticket.find({
userEvent: req.body.userEvent,
ticketTypeID: e.ticketTypeID
}).lean().countDocuments()
.then(number => {
e.ticketsSold = number
return e
}))
})
let eventInfo = Event.findById(req.body.userEvent).select('tickets.ticketTypeID tickets.numberOfTickets')
.then().catch(err => console.log(err))
Promise.all([numTicketsSought, eventInfo]).then(data => {
let numTicketsSought = data[0]
let eventInfo = data[1]
console.log('numTicketsSought', numTicketsSought)
console.log('eventInfo', eventInfo)
numTicketsSought.map(e => {return (
eventInfo.tickets.forEach(f => {
console.log('TRIGGERED')
console.log('e.ticketTypeID ', e.ticketTypeID )
console.log('f.ticketTypeID ', f.ticketTypeID )
console.log('----------------')
if (e.ticketTypeID === f.ticketTypeID){
console.log('MATCHED')
e.numberOfTickets = f.numberOfTickets
e.ticketsAvailable = e.numberOfTickets - e.ticketsSold
return e
}
})
)})
})
This is what the terminal is showing:
numTicketsSought [ Promise {
{ ticketType: 'Early Bird',
ticketTypeID: 1,
numTicketsSought: 1,
ticketsSold: 100 } },
Promise {
{ ticketType: 'Early Bird VIP',
ticketTypeID: 2,
numTicketsSought: 2,
ticketsSold: 15 } } ]
eventInfo { _id: 5de147e1ed01a505f1ee0011,
tickets:
[ { ticketTypeID: 1, numberOfTickets: 5 },
{ ticketTypeID: 2, numberOfTickets: 5 },
{ ticketTypeID: 3, numberOfTickets: 10 },
{ ticketTypeID: 4, numberOfTickets: 4 } ] }
TRIGGERED
e.ticketTypeID undefined
f.ticketTypeID 1
----------------
TRIGGERED
e.ticketTypeID undefined
f.ticketTypeID 2
----------------
TRIGGERED
e.ticketTypeID undefined
f.ticketTypeID 3
----------------
TRIGGERED
e.ticketTypeID undefined
f.ticketTypeID 4
----------------
TRIGGERED
e.ticketTypeID undefined
f.ticketTypeID 1
----------------
TRIGGERED
e.ticketTypeID undefined
f.ticketTypeID 2
----------------
TRIGGERED
e.ticketTypeID undefined
f.ticketTypeID 3
----------------
TRIGGERED
e.ticketTypeID undefined
f.ticketTypeID 4
----------------
What am I doing wrong that is prevent Promise.all from using the values from the first Promise?
I think it's because you have an array of Promises not a single Promise
let numTicketsSought = req.body.numTicketsSought.map(e => { return(
Ticket.find({
userEvent: req.body.userEvent,
ticketTypeID: e.ticketTypeID
}).lean().countDocuments()
.then(number => {
e.ticketsSold = number
return e
}))
})
The problem here is that Array.prototype.map will return an array.
Promise.All([]) in your case you are doing Promise.all([[],]) You will need to unwrap your promises and concatenate them all together
numTicketsSought.push(eventInfo);
Promise.all(numTicketsSought) // <-- rename numTicketsSought to something better like promisesToComplete

How to hook with useEffect/setState

I'm having trouble making the following test pass:
import { useEffect, useState } from "react";
export function useComponentResources(required) {
const [componentResources, setComponentResources] = useState(null);
useEffect(() => {
if (required) {
// api call
setTimeout(() => setComponentResources({}), 100);
}
}, [required]);
return componentResources;
}
import { renderHook } from "#testing-library/react-hooks";
import { useComponentResources } from "./component-resources.hook";
describe("component-resources.hook", () => {
it("fetches resources when required", () => {
//act
const { result } = renderHook(() => useComponentResources(true));
//assert
expect(result.current).toEqual({});
});
});
It keeps failing:
expect(received).toEqual(expected)
Expected value to equal:
{}
Received:
null
Difference:
Comparing two different types of values. Expected object but received null.
7 | const { result } = renderHook(() => useComponentResources(true));
9 | //assert
> 10 | expect(result.current).toEqual({});
11 | });
12 | });
I have created a repro case in codesandbox:
https://codesandbox.io/embed/priceless-browser-94ec2
renderHook doesn't wait for your setTimeout to fire; it can't know what 'side effects' your component has. So when your expect() runs, the current value is still its default - null.
We can force the test to wait until the hook updates again by using waitForNextUpdate, which is on the object renderHook returns. waitForNextUpdate is a function that returns a promise that resolves once the hook is updated again (e.g. when your setTimeout fires).
import { renderHook } from "#testing-library/react-hooks";
import { useComponentResources } from "./component-resources.hook";
describe("component-resources.hook", () => {
it("fetches resources when required", async () => {
const { result, waitForNextUpdate } = renderHook(() => useComponentResources(true));
await waitForNextUpdate();
expect(result.current).toEqual({});
});
});

Resources