Problem Explanation:
I want to unit test a redux-saga using Jest. I'm doing this the way it is described in the example provided within the redux-saga docs: https://redux-saga.js.org/docs/advanced/Testing.html
Within my Saga I'm calling a selector function selectSet that returns a specific object from the application store:
export const selectSet = state => state.setStore.set
In my saga I'm trying to yield this selector function:
import { put, select } from 'redux-saga/effects'
import { selectSet } from '../selectors'
export function* getSet() {
try {
const set = yield select(selectSet)
yield put({ type: 'SET_SUCCESS', payload: { set } })
} catch (error) {
yield put({ type: 'SET_ERROR', payload: { error } })
}
}
Within my test there is no valid application store so I'd have to mock the function to return the expected object:
import assert from 'assert'
import * as AppRoutines from './AppRoutines'
import { put, select } from 'redux-saga/effects'
describe('getSet()', () => {
it('should trigger an action type "SET_SUCCESS" with a payload containing a valid set', () => {
const generator = AppRoutines.getSet()
const set = {
id: 1,
slots: [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }, { id: 5 }],
}
const selectSet = jest.fn()
selectSet.mockReturnValue(set)
// Saga step 1
const actualFirst = generator.next().value
const expectedFirst = select(selectSet)
assert.deepEqual(
actualFirst,
expectedFirst,
'it should retreive a valid set from the store using the selectSet selector'
)
})
})
However - if I assert the saga to return a specific generator value using deepEqual and my mocked function, it expects my selector function to have the original selectSet constructor. But since I'm mocking the function with jest.fn() the constructor is actually equal to mockConstructor - which makes my test fail:
Expected value to deeply equal to:
{"##redux-saga/IO": true, "SELECT": {"args": Array [], "selector": [Function mockConstructor]}}
Received:
{"##redux-saga/IO": true, "SELECT": {"args": Array [], "selector": [Function selectSet]}}
Question: How can I make an assert.deepEqual containing a mock function without conflicting constructor types?
Alternative Question: Is there a way to make my assertion expect a mockConstructor instead of the actual selectSet constructor?
You should not need to mock the selector at all, as in a saga test of this nature, the selector is never actually called, instead you are testing the declarative instructions that are created for the redux saga middleware to act upon are as you expect
This is the instruction that the saga will create {"##redux-saga/IO": true, "SELECT": {"args": Array [], "selector": [Function selectSet]}}, but as the middleware is not running during this test scenario selectSelect will never actually get called
If you need to mock results that your selector returns for your action, then you do so by passing the mock data into the next step...
const set = {
id: 1,
slots: [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }, { id: 5 }],
}
// Saga step 1
const firstYield = generator.next().value
assertDeepEqual(firstYield, select(selectSet))
// Step 2 - successful so dispatch action
// mock data from the previous yield by passing into this step via `next(stubbedYieldedData)`
const secondYield = generator.next(set).value
assertDeepEqual(secondYield, put({type: 'SET_SUCCESS', payload: {set} }))
We can pass the mock store in a fake store as below. Below are the sample selector and generator function along with its test.
Selector
const authSelector = (state) => state.authReducer || initialState;
Saga Generator function
export function* getAuthToken(action) {
try {
const authToken = yield select(makeSelectAuthToken());
} catch (errObj) {}
}
Test Case
import { runSaga } from 'redux-saga'
const dispatchedActions = [];
const fakeStore = {
getState: () => ({ authReducer: { auth: 'test' } }),
dispatch: (action) => dispatchedActions.push(action)
}
await runSaga(fakeStore, getAuthToken, {
payload: {}
}).done;
expected case you can write here below this
Related
I'm trying to extend a mutation in Keystone 6, but having a lot of trouble just getting the standard DB update to work in a custom mutation resolver; Using the standard Keystone boilerplate and added a new collection/list.
Following the examples here, I've matched custom-schema.ts with the generated schema.graphql
schema.graphql (simplified):
type Dog {
id: ID!
name: String
}
input DogWhereUniqueInput {
id: ID
}
input DogUpdateInput {
name: String
}
type Mutation {
updateDog(
where: DogWhereUniqueInput!
data: DogUpdateInput!
): Dog
}
custom-schema.ts:
import { graphQLSchemaExtension } from '#keystone-6/core';
import { Context } from '.keystone/types';
export const extendGraphqlSchema = graphQLSchemaExtension<Context>({
typeDefs: `
type Mutation {
""" update a dog """
updateDog(
where: DogWhereUniqueInput!
data: DogUpdateInput!
): Dog
}
`,
resolvers: {
Mutation: {
updateDog: async (root, { where, id }, context) => {
try {
const response = await context.db.Dog.updateOne({
where: { id },
data: { name: 'updated name'}
});
return response;
} catch (updateError: any) {
throw updateError;
}
}}
}
},
);
keystone.ts:
import { extendGraphqlSchema } from './custom-schema';
// ...
export default withAuth(
config({
db: {
provider: 'sqlite',
url: 'file:./keystone.db',
},
ui: {
isAccessAllowed: (context) => !!context.session?.data,
},
lists,
session,
extendGraphqlSchema,
})
);
When I trigger an update from the (boilerplate) UI, I get this error repeatedly from the catch error handler. Same happens in graphQL playground. Really struggling to understand what's happening and why the resolver is getting spammed and generating this error.
RangeError: Maximum call stack size exceeded
at isLeafType (.../poc/node_modules/graphql/type/definition.js:247:20)
at coerceInputValueImpl (.../poc/node_modules/graphql/utilities/coerceInputValue.js:122:34)
Why is this happening, how to fix? Am I missing something obvious?
That's because both context.db and context.query internally still use the GraphQL API for CRUD. And since your custom mutation updateDog also has the same name as the generated mutation from schema updateDog, both the mutations are repeatedly invoking each other and hence the error RangeError: Maximum call stack size exceeded.
You can solve your problem in one of the two ways —
Change the name of your custom mutation to something else. Eg. updateDogCustom
or
(Practice caution) Instead of context.db.Dog.updateOne, use the prisma client to skip keystone's data layer and CRUD the database directly. Be warned, this means if you have hooks, access control or validation logic in place they won't be invoked.
export const extendGraphqlSchema = graphQLSchemaExtension<Context>({
typeDefs: `
type Mutation {
""" update a dog """
updateDog(
where: DogWhereUniqueInput!
data: DogUpdateInput!
): Dog
""" update a dog custom """
updateDogCustom(
where: DogWhereUniqueInput!
data: DogUpdateInput!
): Dog
}
`,
resolvers: {
Mutation: {
updateDog: async (root, { where: { id }, data: { name } }, context) => {
try {
const response = await context.prisma.dog.update({
where: { id },
data: { name },
});
return response;
} catch (updateError: any) {
throw updateError;
}
},
updateDogCustom: async (
root,
{ where: { id }, data: { name } },
context
) => {
try {
const response = await context.db.Dog.updateOne({
where: { id },
data: { name },
});
return response;
} catch (updateError: any) {
throw updateError;
}
},
},
},
});
Codesandbox here — https://codesandbox.io/s/winter-shadow-fz689e?file=/src/custom-schema.ts
You can run the graphql playground right from codesandbox from /api/graphql path. Eg. https://fz689e.sse.codesandbox.io/api/graphql
I need to use jest.mock together with async, await and import() so that I can use a function from another file inside the mocked module. Otherwise I must copy and paste a few hundreds of slocs or over 1000 slocs, or probably it is not even possible.
An example
This does work well:
jest.mock('./myLin.jsx', () => {
return {
abc: 967,
}
});
Everywhere I use abc later it has 967 as its value, which is different than the original one.
This does not work:
jest.mock('./myLin.jsx', async () => {
return {
abc: 967,
}
});
abc seems to not be available.
Actual issue
I need async to be able to do this:
jest.mock('~/config', async () => {
const { blockTagDeserializer } = await import(
'../editor/deserialize' // or 'volto-slate/editor/deserialize'
);
// … here return an object which contains a call to
// blockTagDeserializer declared above; if I can't do this
// I cannot use blockTagDeserializer since it is outside of
// the scope of this function
}
Actual results
I get errors like:
TypeError: Cannot destructure property 'slate' of '((cov_1viq84mfum.s[13]++) , _config.settings)' as it is undefined.
where _config, I think, is the ~/config module object and slate is a property that should be available on _config.settings.
Expected results
No error, blockTagDeserializer works in the mocked module and the unit test is passed.
The unit test code
The code below is a newer not-working code based on this file on GitHub.
import React from 'react';
import renderer from 'react-test-renderer';
import WysiwygWidget from './WysiwygWidget';
import configureStore from 'redux-mock-store';
import { Provider } from 'react-intl-redux';
const mockStore = configureStore();
global.__SERVER__ = true; // eslint-disable-line no-underscore-dangle
global.__CLIENT__ = false; // eslint-disable-line no-underscore-dangle
jest.mock('~/config', async () => {
const { blockTagDeserializer } = await import(
'../editor/deserialize' // or 'volto-slate/editor/deserialize'
);
const createEmptyParagraph = () => {
return {
type: 'p',
children: [{ text: '' }],
};
};
return {
settings: {
supportedLanguages: [],
slate: {
elements: {
default: ({ attributes, children }) => (
<p {...attributes}>{children}</p>
),
strong: ({ children }) => {
return <strong>{children}</strong>;
},
},
leafs: {},
defaultBlockType: 'p',
textblockExtensions: [],
extensions: [
(editor) => {
editor.htmlTagsToSlate = {
STRONG: blockTagDeserializer('strong'),
};
return editor;
},
],
defaultValue: () => {
return [createEmptyParagraph()];
},
},
},
};
});
window.getSelection = () => ({});
test('renders a WysiwygWidget component', () => {
const store = mockStore({
intl: {
locale: 'en',
messages: {},
},
});
const component = renderer.create(
<Provider store={store}>
<WysiwygWidget
id="qwertyu"
title="My Widget"
description="My little description."
required={true}
value={{ data: 'abc <strong>def</strong>' }}
onChange={(id, data) => {
// console.log('changed', data.data);
// setHtml(data.data);
}}
/>
</Provider>,
);
const json = component.toJSON();
expect(json).toMatchSnapshot();
});
What I've tried
The code snippets above show partially what I have tried.
I searched the web for 'jest mock async await import' and did not found something relevant.
The question
If jest.mock is not made to work with async, what else can I do to make my unit test work?
Update 1
In the last snippet of code above, the line
STRONG: blockTagDeserializer('strong'),
uses blockTagDeserializer defined here which uses deserializeChildren, createEmptyParagraph (which is imported from another module), normalizeBlockNodes (which is imported from another module) and jsx (which is imported from another module) functions, which use deserialize which uses isWhitespace which is imported from another module and typeDeserialize which uses jsx and deserializeChildren.
Without using await import(...) syntax how can I fully mock the module so that my unit test works?
If you want to dig into our code, please note that the volto-slate/ prefix in the import statements is for the src/ folder in the repo.
Thank you.
I'd advise not doing any "heavy" stuff (whatever that means) in a callback of jest.mock, – it is designed only for mocking values.
Given your specific example, I'd just put whatever the output of blockTagDeserializer('strong') right inside the config:
jest.mock('~/config', () => {
// ...
extensions: [
(editor) => {
editor.htmlTagsToSlate = {
STRONG: ({ children }) => <strong>{children}</strong>, // or whatever this function actually returns for 'strong'
};
return editor;
},
],
// ...
});
This doesn't require anything asynchronous to be done.
If you need this setup to be present in a lot of files, extracting it in a setup file seems to be the next best thing.
I found a solution. I have a ref callback that sets the htmlTagsToSlate property of the editor in the actual code of the module, conditioned by global.__JEST__ which is defined as true in Jest command line usage:
import { htmlTagsToSlate } from 'volto-slate/editor/config';
[...]
testingEditorRef={(val) => {
ref.current = val;
if (val && global.__JEST__) {
val.htmlTagsToSlate = { ...htmlTagsToSlate };
}
}}
Now the jest.mock call for ~/config is simple, there is no need to do an import in it.
I also use this function:
const handleEditorRef = (editor, ref) => {
if (typeof ref === 'function') {
ref(editor);
} else if (typeof ref === 'object') {
ref.current = editor;
}
return editor;
};
Consider the following:
// external file
export const someFunction = setState => {
setState({ value: "some new string" })
}
// component's file
import { someFunction } from "pathToFile"
interface TState {
value: string
}
const [state, setState] = useState<TState>({ value: "some string" })
useEffect(() => {
someFunction(setState)
}, [])
Is it possible to achieve that someFunction will accept only this particular setState function as а parameter? If so, how it could be done?
To be clear, I want:
someFunction(setState) // pass
someFunction(setAnotherState) // fail
someFunction(anotherFunctions) // fail
someFunction() // fail
someFunction(0) // fail
someFunction(true) // fail
someFunction({}) // fail
Basically, you are asking for a nominal type:
type NominalSetState = React.Dispatch<React.SetStateAction<TState>> &
{ readonly __brand_setState__: unique symbol }
const someFunction = (setState: NominalSetState) => {
setState({ value: "some new string" })
}
function useNominalState(init: TState) {
return useState<TState>(init) as [TState, NominalSetState]
}
const [state, setState] = useNominalState({ value: "some string" })
const [anotherState, setAnotherState] = useState<TState>({ value: "some string" })
Now, tests behave as desired:
someFunction(setState) // pass
someFunction(setAnotherState) // fail
someFunction() // fail
someFunction(0) // fail
someFunction(true) // fail
someFunction({}) // fail
someFunction((...arg: any[]) => any) // fail
Live code example
You can achieve your goal by implementing the callback methods.
Example:
this.readStudentFile(anyFile, this.callback);
callback(isPass) {
this.setState({ isPass: isPass });
}
readStudentFile(file: File, callback) {
if(file) {
callback(true);
} else callback(false);
}
You could define a very specific parameter type to someFunction, which will give you the same pass/fails that you want.
Only allowing one particular object isn't what Typescript is really for - you are meant to guard by only allowing some interfaces, and anything with that same interface will be allowed.
I have a utility file that uses the following implementation with a vuex store:
// example.js
import store from '#/store';
[...]
export default function exampleUtil(value) {
const user = store.state.user.current;
[...]
}
In my test, I found that I can successfully mock the value of user in the following two ways:
Manual mock
// store/__mocks__/index.js
export default {
state: {
user: {
current: {
roles: [],
isAdmin: false,
},
},
},
};
or
Mock function
// example.spec.js
jest.mock('#/store', () => ({
state: {
user: {
current: {
roles: [],
isAdmin: false,
},
},
},
}));
The issue that I'm running into is that I want to be able to change the value of current between tests, such as changing isAdmin to true or updating the array for roles.
What is the best way to do this using Jest mocks?
It turns out that the value of a mock can be changed inside a test directly after importing the mocked file.
Using the mock function example from above:
// example.spec.js
import store from '#/store'; // <-- add this
jest.mock('#/store', () => ({
state: {
user: {
current: {
roles: [],
isAdmin: false,
},
},
},
}));
it('should do things', () => {
store.state.user.current.roles = ['example', 'another']; // <-- change mock value here
[...]
});
I try to test that a dispatch to a redux store is happening inside an ES6 class method, but I'm failing miserably.
Somehow I'm unable to find out how to mock the store to get a response.
The method is quite simple:
class Foo {
…
bar() {
store.dispatch({ type: FOO_BAR_BAZ });
}
…
};
I just want to test that the dispatch happened.
I tried a couple of things, including redux-mock-store, but I get no feedback from the store.
it('should foo bar baz', () => {
const store = {
dispatch: jest.fn(),
};
const foobar = new Foo();
foobar.bar();
console.log(store.dispatch.mock);
//=> { calls: [], instances: [], invocationCallOrder: [], results: [] }
});
I would be deeply grateful if someone could point me in the right direction.
The jest store mock isn't invoked because the class doesn't have access to it. One way around this would be passing in the store to the constructor:
class Foo {
constructor(store) {
this.store = store
}
bar() {
this.store.dispatch({ type: FOO_BAR_BAZ });
}
}
-
it('should foo bar baz', () => {
const store = {
dispatch: jest.fn(),
};
const foobar = new Foo(store);
foobar.bar();
console.log(store.dispatch.mock);
});