I've got a Vue 3 app which makes an API call and processes the response for adjusting some data fields. The API call is made after the child component emits a custom event, which the parent is listening on. Its working fine, but now I want to test (jest 26, vue-test-utils) it and I'm failing all the time. I'm not able to mock the response, its always undefined. Therefore, i cannot expect the changed data fields.
I'm new to jest, but I've think this is the desired way to mock a response. Or am I wrong?
This is my abstracted test code. Have a look at the open()-method and how i try to mock its API response.
Structure:
-
|- Parent.vue
|- Child.vue
|- api.js
Parent.vue
<template>
<Child
v-for="price in prices"
:key="price.type"
:price="price"
#open="open"
></Child>
</template>
<script>
// imports
export default {
name: "Parent",
components: {
Child
},
props: ["id"],
async mounted() {
// on creation load initial prices
const pricesDto = await loadPricesFor(this.id);
this.prices = toPrices(pricesDto); // external dto to array mapper
},
data() {
return {
prices: []
};
},
methods: {
async open() {
try {
const response = await makeApiCall(this.id);
console.log("foo", response); // response is undefined!
// make something with response data
} catch (error) {
// do nothing
}
}
}
};
</script>
parent.test.js:
import {flushPromises, mount} from "#vue/test-utils";
import * as apiModule from "./api.js";
import Child from "./Child";
jest.mock("./api.js");
it("test api call", async () => {
// given
const wrapper = async () => {
return mount(Child, {
props: {
id: "123",
},
});
};
await flushPromises();
// when
wrapper.findComponent(Child).vm.$emit("open");
// then
const mockedResponse = {
foo: "bar",
};
const response = Promise.resolve({
status: 200,
ok: true,
json: () => Promise.resolve(mockedResponse),
});
const spy = jest.spyOn(apiModule, "makeApiCall").mockReturnValueOnce(response);
expect(spy).toHaveBeenCalledWith("123");
// expect changed data fields regarding to response. This is not working!
});
I hope you might solve it without a interactive example, if not tell me and I'm gonna code some.
Solution: Okay I was dumb. I need to mock my response before firing the custom event not afterwards (makes sense...). So the code works as expected if I'm moving
const spy = jest.spyOn(apiModule, "makeApiCall").mockReturnValueOnce(response);
before //when block.
Related
How do I write a test for this function? I first tried to include an optional param in the function such as mockData and go from there, but that seems too tedious. I have a function, something like this:
export async function getBreadcrumbs(service, id) {
const { folderId, label } = await getFileDetailsById(id);
const { breadcrumbs } = await getFoldersService(service, folderId);
return [...breadcrumbs, { label, id: '' }];
}
I'm thinking jest.spyOn is what I need to write the test with but I'm not sure how to approach it. I started with:
import * as crumbs from '.....data'
describe('getCrumbs', () => {
it('should return breadcrumbs name and id', async () => {
const data = jest.spyOn(crumbs, 'getBreadcrumbs')
});
});
But I'm not sure where to go from here. How can I mock a return of this function? This function returns:
[{ displayName: 'Hey', id: '' }]
getFileDetailsById and getFoldersService are API calls.
TLDR: I have a function and I'm unsure how to mock its return.
I have this JavaScript data file(src/test/test.js):
module.exports = {
"title": "...",
"Number": "number1",
"Number2": ({ number1 }) => number1 / 2,
}
I want to pass this file verbatim(functions preserved) to a page, so that the page can use that data to build itself. I already have the page template and everything else sorted out, I just need to find a way to pass this into the page.
The first approach I tried is requireing this file in gatsby-node.js and then passing it as pageContext.
gatsby-node.js
const path = require('path');
exports.createPages = ({actions, graphql}) => {
const { createPage } = actions;
return graphql(`
query loadQuery {
allFile(filter: {sourceInstanceName: {eq: "test"}}) {
edges {
node {
relativePath
absolutePath
}
}
}
}
`).then(result => {
if (result.errors) {
throw result.errors;
}
for (const node of result.data.allFile.edges.map(e => e.node)) {
const data = require(node.absolutePath);
createPage({
path: node.relativePath,
component: path.resolve('./src/templates/test.js'),
context: data,
});
}
});
};
gatsby-config.js
module.exports = {
plugins: [
{
resolve: `gatsby-source-filesystem`,
options: {
name: `test`,
path: `${__dirname}/src/test/`,
},
},
],
}
src/templates/test.js
import React from 'react';
const index = ({ pageContext }) => (
<p>{pageContext.Number2()}</p>
);
export default index;
However, I get this warning when running the dev server:
warn Error persisting state: ({ number1 }) => number1 / 2 could not be cloned.
If I ignore it and try to use the function anyway, Gatsby crashes with this error:
WebpackError: TypeError: pageContext.Number2 is not a function
After searching for a while, I found this:
The pageContext was always serialized so it never worked to pass a function and hence this isn't a bug. We might have not failed before though.
- Gatsby#23675
which told me this approach wouldn't work.
How could I pass this data into a page? I've considered JSON instead, however, JSON can't contain functions.
I've also tried finding a way to register a JSX object directly, however I couldn't find a way.
Regarding the main topic, as you spotted, can't be done that way because the data is serialized.
How could I pass this data into a page? I've considered JSON instead,
however, JSON can't contain functions.
Well, this is partially true. You can always do something like:
{"function":{"arguments":"a,b,c","body":"return a*b+c;"}}
And then:
let func = new Function(function.arguments, function.body);
In this case, you are (de)serializing a JSON function, creating and casting a function based on JSON parameters. This approach may work in your scenario.
Regarding the JSX, I guess you can try something like:
for (const node of result.data.allFile.edges.map(e => e.node)) {
const data = require(node.absolutePath);
createPage({
path: node.relativePath,
component: path.resolve('./src/templates/test.js'),
context:{
someComponent: () => <h1>Hi!</h1>
},
});
}
And then:
import React from 'react';
const Index = ({ pageContext: { someComponent: SomeComponent} }) => (
return <div><SomeComponent /></div>
);
export default index;
Note: I don't know if it's a typo from the question but index should be capitalized as Index
In this case, you are aliasing the someComponent as SomeComponent, which is a valid React component.
I don't know if I can present my problem clearly and legibly.
Let me start by introducing...
...the essence
I have created a Mobx store, where I store information about dialog boxes. It is very simple, so let me quote it in full here.
import { makeObservable, action, computed, observable } from "mobx";
class WindowsStore {
windowsList = [];
constructor() {
makeObservable(this, {
windowsList: observable,
addWindow: action,
removeWindowById: action,
windows: computed,
});
}
addWindow(uniqueId, component, props) {
this.windowsList.push({
id: uniqueId,
Win: [component],
props
});
}
removeWindowById(wndId) {
const wndIndex = this.windowsList.findIndex((wnd) => wnd.id === wndId);
if (wndIndex > -1) this.windowsList.splice(wndIndex, 1);
}
get windows() {
return this.windowsList; <-- is't sick, but I don't like mobx strict-mode warnings
}
}
const windowsStore = (window.windowsStore = new WindowsStore());
export default windowsStore;
I wanted such a construction, because it needs to place all dialogs as children in one component (I will present it below)
const WindowsList = observer(({ windowsStore }) => {
return windowsStore.windows.map((wnd) => {
const Win = wnd.Win[0]; <-- it's weird (I know it) but that's the only way it wants to work
return (
<Win
{...wnd.props}
key={wnd.id}
onClose={() => {
WindowsStore.removeWindowById(wnd.id);
}}
/>
);
});
});
then, calls the component <WindowsList windowsStore = {WindowsStore} /> where it needs and voilà.
Now, anywhere in the application, it calls store addWindow (uniqueId, component_class, props) to call the dialog.
What happens next. The variable props (generally as an object) is passed as properties to component_class (as seen in the body of the <WindowsList /> component above)
Indeed, it works.
My problem
However, I have a problem with passing a function in props, e.g .:
// ...somewhere in dialog component...
doDelete(e) {
console.log("Deleting...");
}
openDeleteConfirm = (item) => {
const filepath = combinePathName(item.path, item.name);
WindowsStore.addWindow("delete-" + filepath, DeleteConfirmation, {
item: filepath,
onConfirm: this.doDelete, <-- this is my problem :(
});
};
What I noticed is that, there is no onConfirm property passed in the dialog component, even though it is visible in store.windowsList. Here is the console output:
> windowsStore.windowsList[0].props
Proxy {Symbol(mobx administration): ObservableObjectAdministration, onConfirm: ƒ}
[[Handler]]: Object
[[Target]]: Object
item: (...)
onConfirm: ƒ res() <-- Here!
Symbol(mobx administration): ObservableObjectAdministration {target_: {…}, values_: Map(1), name_: "WindowsStore#206.windowsList[..].props", keysAtom_: Atom, defaultEnhancer_: ƒ, …}
get item: ƒ ()
set item: ƒ (v)
__proto__: Object
[[IsRevoked]]: false
How can I pass a function via store to a component as its property?
Is it even possible?
Help, because I got lost a bit ;)
It's not a solution
In part, I managed to fix my problem, however it is imprecise and I am afraid it may cause other problems (in the future)
The trick
I decided to put a pointer to an action in the actions array like this:
// ...somewhere in dialog component...
doDelete({ path, name }) {
console.log(`Delete entry ${name} in ${path}...`);
}
openDeleteConfirm = (item) => {
const filepath = combinePathName(item.path, item.name);
WindowsStore.addWindow("delete-" + filepath, DeleteConfirmation, {
item: filepath,
actions: [
() => {
this.doDelete(item);
},
],
});
};
In the dialog code, referenced by index in the actions array:
this.props.actions[0]();
It works, but I don't like it: /
I'm trying to mock a function and not sure what i'm doing wrong here. I have this function "getGroups"
getGroups:
export const getGroups = async () => {
try {
const groupApiUrl = getDashboardPath(GROUPS_TAB_INDEX);
const data = await fetch(groupApiUrl, { cache: 'force-cache' });
const userData = await data.json();
return userData;
} catch (error) {
throw Error(error);
}
};
___mocks___/getGroups.js:
export default async () => {
return {
groups: [
{ id: 1, name: 'Data1' },
{ id: 2, name: 'Data2' }
]
};
};
getGroups.test.js:
jest.mock('./getGroups.js');
// eslint-disable-next-line import/first
import { getGroups } from './getGroups';
const fakeRespose = {
groups: [
{ id: 1, name: 'Data1' },
{ id: 2, name: 'Data2' }
]
};
describe('getGroups', () => {
it('returns data', async () => {
const data = await getGroups();
console.log('DATA', data); <---- UNDEFINED?
expect(data).toBeDefined();
expect(data).toMatchObject(fakeRespose);
});
it('handles error', async () => {
// const data = await getGroups();
await getGroups().toThrow('Failed');
});
});
What are you doing wrong here?
Default export in your mock instead of named as in the implementation
In your implementation you're using named export and you're importing { getGroups } so to make it work you need to change your mock like this
__mocks__\getGroups.js
export const getGroups = async () => {
return {
groups: [
{ id: 1, name: 'Data1' },
{ id: 2, name: 'Data2' }
]
};
};
working example
TL;DR
Testing mock
There is no point at all to test mock function. This does not proves your implementation is working. Even if you change your implementation your tests will still pass.
Use mocks only for the dependencies of your implementation
Use jest.genMockFromModule
It will create jest.fn() for each of the module's exported methods and will preserve the constants, allowing you to change the return value/implementation for some test cases and will also be able to write assertions if the function have been called
__mocks__\getGroups.js
const mock = jest.genMockFromModule('../getGroups');
mock.getGroups.mockResolvedValue({
groups: [
{ id: 1, name: 'Data1' },
{ id: 2, name: 'Data2' }
]
})
module.exports = mock;
Jest will automatically hoist jest.mock calls (read more...)
So you can safely leave the import statements first and then call jest.mock
From Jest Docs, here's an example of a Mock.
jest.mock('../moduleName', () => {
return jest.fn(() => 42);
});
// This runs the function specified as second argument to `jest.mock`.
const moduleName = require('../moduleName');
moduleName(); // Will return '42';
In your case data is undefined, because you haven't actually supplied a mocked implementation for the function or the mock hasn't worked and you're still calling the original function.
Example Reference: https://jestjs.io/docs/en/jest-object#jestmockmodulename-factory-options
However, in your simple case you could also solve this with a spy, either jest.spyOn or jest.fn(). Here are two solutions to what you're trying to achieve. You can look at the code and run it here: https://repl.it/repls/FairUnsungMice
UPDATE after comment:
Manual mocks are defined by writing a module in a __mocks__/
subdirectory immediately adjacent to the module. For example, to mock
a module called user in the models directory, create a file called
user.js and put it in the models/__mocks__ directory. Note that the
__mocks__ folder is case-sensitive, so naming the directory __MOCKS__ will break on some systems.
Double check the naming, directory structure & type of exports you've setup - they should match. Also, it's worth checking this out: https://github.com/facebook/jest/issues/6127 - looks like an open issue with jest. If you need a solution, look at using a different approach as I mentioned.
Reference: https://jestjs.io/docs/en/manual-mocks
newbie testing here and I ran into this error
module.js
import reducer from './reducer';
export default function getGModalModule({ domain }) {
return {
id: `GModal-module-${domain}`,
reducerMap: {
[domain]: reducer({
domain,
}),
},
};
}
module.test.js
import getGModalModule from '../module';
import reducer from '../reducer';
describe('getGModalModule', () => {
it('returns the correct module', () => {
const reducers = reducer({
domain: 'demo'
})
const expectVal = getGModalModule({
domain: 'demo'
});
const received = {
id: `GModal-module-demo`,
reducerMap: {
demo: reducers,
},
}
console.log("expectVal", expectVal)
console.log("received", received)
expect(expectVal).toEqual(received);
});
});
The error says something like this:
Expected: {"id": "GModal-module-demo", "reducerMap": {"demo": [Function gModalReducer]}}
Received: serializes to the same string
I've tried to log out the result and they look exactly the same, any idea why this doesn't pass the test ?
expectVal { id: 'GModal-module-demo', reducerMap: { demo: [Function: gModalReducer] } }
received { id: 'GModal-module-demo', reducerMap: { demo: [Function: gModalReducer] } }
Thanks in advance,
Jest cannot compare functions. Even if they are serialized into the same string, even if their source code is the same, function may refer to different variables through closures or bound to different this so we cannot say if functions are equal.
expect(function aaa() {}).toEqual(function aaa() {}); // fails
You may want to exclude methods before comparison explicitly or create custom matcher that will do that for you. But much better if you test what functions does instead. In your case you probably have separate test for ./reducer.js, right? You can just join both test: that one testing reducer in isolation and this one that checks object key names.