How to change the behaviour of a mocked import? - javascript

I am quite confused with mocking in Jest an how to unit test the implementations. The thing is i want to mock different expected behaviours.
Is there any way to achieve this? as imports can be only on the top of the file and to be able to mock something it must be declared before the import. I have also tried to pass a local function so I could overwrite the behaviour but jest complains you are not allowed to pass anything local.
jest.mock('the-package-to-mock', () => ({
methodToMock: jest.fn(() => console.log('Hello'))
}));
import * as theThingToTest from '../../../app/actions/toTest'
import * as types from '../../../app/actions/types'
it('test1', () => {
expect(theThingToTest.someAction().type).toBe(types.SOME_TYPE)
})
it('test2', () => {
//the-package-to-mock.methodToMock should behave like something else
expect(theThingToTest.someAction().type).toBe(types.SOME_TYPE)
})
internally as you can imagine theThingToTest.someAction() uses the-package-to-mock.methodToMock

You can mock with a spy and import the mocked module. In your test you set how the mock should behave using mockImplementation:
jest.mock('the-package-to-mock', () => ({
methodToMock: jest.fn()
}));
import { methodToMock } from 'the-package-to-mock'
it('test1', () => {
methodToMock.mockImplementation(() => 'someValue')
})
it('test2', () => {
methodToMock.mockImplementation(() => 'anotherValue')
})

I use the following pattern:
'use strict'
const packageToMock = require('../path')
jest.mock('../path')
jest.mock('../../../../../../lib/dmp.db')
beforeEach(() => {
packageToMock.methodToMock.mockReset()
})
describe('test suite', () => {
test('test1', () => {
packageToMock.methodToMock.mockResolvedValue('some value')
expect(theThingToTest.someAction().type).toBe(types.SOME_TYPE)
})
test('test2', () => {
packageToMock.methodToMock.mockResolvedValue('another value')
expect(theThingToTest.someAction().type).toBe(types.OTHER_TYPE)
})
})
Explanation:
You mock the class you are trying to use on test suite level, make sure the mock is reset before each test and for every test you use mockResolveValue to describe what will be return when mock is returned

Another way is to use jest.doMock(moduleName, factory, options).
E.g.
the-package-to-mock.ts:
export function methodToMock() {
return 'real type';
}
toTest.ts:
import { methodToMock } from './the-package-to-mock';
export function someAction() {
return {
type: methodToMock(),
};
}
toTest.spec.ts:
describe('45006254', () => {
beforeEach(() => {
jest.resetModules();
});
it('test1', () => {
jest.doMock('./the-package-to-mock', () => ({
methodToMock: jest.fn(() => 'type A'),
}));
const theThingToTest = require('./toTest');
expect(theThingToTest.someAction().type).toBe('type A');
});
it('test2', () => {
jest.doMock('./the-package-to-mock', () => ({
methodToMock: jest.fn(() => 'type B'),
}));
const theThingToTest = require('./toTest');
expect(theThingToTest.someAction().type).toBe('type B');
});
});
unit test result:
PASS examples/45006254/toTest.spec.ts
45006254
✓ test1 (2016 ms)
✓ test2 (1 ms)
-----------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
-----------|---------|----------|---------|---------|-------------------
All files | 100 | 100 | 100 | 100 |
toTest.ts | 100 | 100 | 100 | 100 |
-----------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
Snapshots: 0 total
Time: 3.443 s
source code: https://github.com/mrdulin/jest-v26-codelab/tree/main/examples/45006254

spyOn worked best for us. See previous answer:
https://stackoverflow.com/a/54361996/1708297

How to Change Mocked Functions For Different Test Scenarios
In my scenario I tried to define the mock function outside of the jest.mock which will return an error about trying to access the variable before it's defined. This is because modern Jest will hoist jest.mock so that it can occur before imports. Unfortunately this leaves you with const and let not functioning as one would expect since the code hoists above your variable definition. Some folks say to use var instead as it would become hoisted, but most linters will yell at you, so as to avoid that hack this is what I came up with:
Jest Deferred Mocked Import Instance Calls Example
This allows us to handle cases like new S3Client() so that all new instances are mocked, but also while mocking out the implementation. You could likely use something like jest-mock-extended here to fully mock out the implementation if you wanted, rather than explicitly define the mock.
The Problem
This example will return the following error:
eferenceError: Cannot access 'getSignedUrlMock' before initialization
Test File
const sendMock = jest.fn()
const getSignedUrlMock = jest.fn().mockResolvedValue('signedUrl')
jest.mock('#aws-sdk/client-s3', () => {
return {
S3Client: jest.fn().mockImplementation(() => ({
send: sendMock.mockResolvedValue('file'),
})),
GetObjectCommand: jest.fn().mockImplementation(() => ({})),
}
})
jest.mock('#aws-sdk/s3-request-presigner', () => {
return {
getSignedUrl: getSignedUrlMock,
}
})
The Answer
You must defer the call in a callback like so:
getSignedUrl: jest.fn().mockImplementation(() => getSignedUrlMock())
Full Example
I don't want to leave anything up to the imagination, although I phaked the some-s3-consumer from the actual project, but it's not too far off.
Test File
import { GetObjectCommand, S3Client } from '#aws-sdk/client-s3'
import { SomeS3Consumer } from './some-s3-consumer'
const sendMock = jest.fn()
const getSignedUrlMock = jest.fn().mockResolvedValue('signedUrl')
jest.mock('#aws-sdk/client-s3', () => {
return {
S3Client: jest.fn().mockImplementation(() => ({
send: sendMock.mockResolvedValue('file'),
})),
GetObjectCommand: jest.fn().mockImplementation(() => ({})),
}
})
jest.mock('#aws-sdk/s3-request-presigner', () => {
return {
// This is weird due to hoisting shenanigans
getSignedUrl: jest.fn().mockImplementation(() => getSignedUrlMock()),
}
})
describe('S3Service', () => {
const service = new SomeS3Consumer()
describe('S3 Client Configuration', () => {
it('creates a new S3Client with expected region and credentials', () => {
expect(S3Client).toHaveBeenCalledWith({
region: 'AWS_REGION',
credentials: {
accessKeyId: 'AWS_ACCESS_KEY_ID',
secretAccessKey: 'AWS_SECRET_ACCESS_KEY',
},
})
})
})
describe('#fileExists', () => {
describe('file exists', () => {
it('returns true', () => {
expect(service.fileExists('bucket', 'key')).resolves.toBe(true)
})
it('calls S3Client.send with GetObjectCommand', async () => {
await service.fileExists('bucket', 'key')
expect(GetObjectCommand).toHaveBeenCalledWith({
Bucket: 'bucket',
Key: 'key',
})
})
})
describe('file does not exist', () => {
beforeEach(() => {
sendMock.mockRejectedValue(new Error('file does not exist'))
})
afterAll(() => {
sendMock.mockResolvedValue('file')
})
it('returns false', async () => {
const response = await service.fileExists('bucket', 'key')
expect(response).toBe(false)
})
})
})
describe('#getSignedUrl', () => {
it('calls GetObjectCommand with correct bucket and key', async () => {
await service.getSignedUrl('bucket', 'key')
expect(GetObjectCommand).toHaveBeenCalledWith({
Bucket: 'bucket',
Key: 'key',
})
})
describe('file exists', () => {
it('returns the signed url', async () => {
const response = await service.getSignedUrl('bucket', 'key')
expect(response).toEqual(ok('signedUrl'))
})
})
describe('file does not exist', () => {
beforeEach(() => {
getSignedUrlMock.mockRejectedValue('file does not exist')
})
afterAll(() => {
sendMock.mockResolvedValue('file')
})
it('returns an S3ErrorGettingSignedUrl with expected error message', async () => {
const response = await service.getSignedUrl('bucket', 'key')
expect(response.val).toStrictEqual(new S3ErrorGettingSignedUrl('file does not exist'))
})
})
})
})

Andreas answer work well with functions, here is what I figured out using it:
// You don't need to put import line after the mock.
import {supportWebGL2} from '../utils/supportWebGL';
// functions inside will be auto-mocked
jest.mock('../utils/supportWebGL');
const mocked_supportWebGL2 = supportWebGL2 as jest.MockedFunction<typeof supportWebGL2>;
// Make sure it return to default between tests.
beforeEach(() => {
// set the default
supportWebGL2.mockImplementation(() => true);
});
it('display help message if no webGL2 support', () => {
// only for one test
supportWebGL2.mockImplementation(() => false);
// ...
});
It won't work if your mocked module is not a function. I haven't been able to change the mock of an exported boolean for only one test :/. My advice, refactor to a function, or make another test file.
export const supportWebGL2 = /* () => */ !!window.WebGL2RenderingContext;
// This would give you: TypeError: mockImplementation is not a function

Related

Vitest mock modules function in only one test and use the actual function in others

The following is an abstraction of my problem and thus does not make too much sense:
Given I have a simple utility callMethodIf that's returning the return of another imported method (blackbox).
~~/utils/call-method-if.js:
import { blackbox } from '~~/utils/blackbox';
export const callMethodIf = (condition) => {
return blackbox(condition);
};
~~/utils/blackbox.js:
export const blackbox = (condition) => {
return { called: condition };
};
How would I run one test case which calls the actual implementation of blackbox() and another one where I mock the return value of blackbox()?
I tried to do it that way:
import { describe, expect, it } from 'vitest';
import { callMethodIf } from '~~/utils/call-method-if';
describe('Call method if', () => {
it('returns "called: true" if condition is true', () => {
const result = callMethodIf(true);
expect(result).toEqual({ called: true });
});
it('returns mocked blackbox return object', () => {
vi.mock('~~/utils/blackbox', () => ({
blackbox: vi.fn().mockReturnValue({ mock: true })
}));
const result = callMethodIf(false);
expect(result).toEqual({ mock: true });
});
});
Both tests work if I run only one of them, but they don't work when combined.
Running vi.clearAllMocks() or vi.resetAllMocks() don't help.
Defining a global mock and overwriting it in my first test doesn't work either:
import { describe, expect, it } from 'vitest';
import { callMethodIf } from '~~/utils/call-method-if';
vi.mock('~~/utils/blackbox', () => ({
blackbox: vi.fn().mockReturnValue({ mock: true })
}));
describe('Call method if', () => {
it('returns "called: true" if condition is true', () => {
vi.mock('~~/utils/blackbox', async () => ({
blackbox: (await vi.importActual('~~/utils/blackbox')).blackbox
}));
const result = callMethodIf(true);
expect(result).toEqual({ called: true });
});
it('returns mocked blackbox return object', () => {
const result = callMethodIf(false);
expect(result).toEqual({ mock: true });
});
});
Okay, after lots of trial and error I finally got it to work. I can't really tell why my previous tries do not work tough.
Working solution:
import { describe, expect, it } from 'vitest';
import { callMethodIf } from '~~/utils/call-method-if';
vi.mock('~~/utils/blackbox');
describe('Call method if', () => {
it('returns "called: true" if condition is true', async () => {
const blackbox = await import('~~/utils/blackbox');
blackbox.blackbox = (await vi.importActual('~~/utils/blackbox')).blackbox;
const result = callMethodIf(true);
expect(result).toEqual({ called: true });
});
it('returns mocked blackbox return object', async () => {
const blackbox = await import('~~/utils/blackbox');
blackbox.blackbox = vi.fn().mockReturnValue({ mock: true });
const result = callMethodIf(false);
expect(result).toEqual({ mock: true });
});
});
When using TypeScript consider typing the importActual() return like that:
blackbox.blackbox = (await vi.importActual<typeof import('~~/utils/blackbox')>('~~/utils/blackbox')).blackbox;
I also ran in to this problem with using Vite. After a lot of trail and error I have managed to get the mocking of a module function working by using the following code:
vi.mock('#/models/generated/graphql')
describe('MyComponent works as expected', () => {
it('Shows loading when loading', async () => {
const graphql = await import('#/models/generated/graphql')
graphql.useGetAllQuery = vi.fn().mockReturnValue({ loading: true, error: null, data: null })
render(<MyComponent />)
expect(screen.findByTestId('my-component-loading')).toBeTruthy()
})
}
The function that I am mocking is this one (which is auto generated for my Graphql service):
export function useGetAllQuery(baseOptions?: Apollo.QueryHookOptions<GetAllQuery, GetAllQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<GetAllQuery, GetAllQueryVariables>(GetAllDocument, options);
}
I do not really understand why this works, but it does. I hope that this code snippet might help some.

Is it possible to clear the mock of a module in JEST?

I want to mock the function forgotPassword inside the module authenticationPlugin/App, So i am doing this
jest.mock("authenticationPlugin/App", () => ({
forgotPassword (email: string) {
const msg='success'
email='a'
return msg
}
}))
Now i want to clear the mock of authenticationPlugin/App and have a different implementation for the forgotPassword method
So i did this
jest.clearAllMocks();
jest.mock("authenticationPlugin/App", () => ({
forgotPassword (email: string) {
throw new Error(<any>{'err':{'message':'Network Error'}})
}
}))
Now i expect the forgotPassword method to have a different implementation after clearing the mocks in for the module authenticationPlugin/App but it doesn't change...
If you want to have a different implementation for the mock in each test, you can use jest.fn instead.
Expanding on your code, it could look like this:
it('returns success', () => {
authApp.forgotPassword = jest.fn((email: string) => {
const msg='success'
email='a'
return msg
});
// Your test code here.
});
test('returns error', () => {
authApp.forgotPassword = jest.fn((email: string) => {
throw new Error(<any>{'err':{'message':'Network Error'}})
});
// Your test code here.
});

Jest: Vue Component can't find mocked function

I'm mocking a ES6 class which is used inside my Vue Component:
export default class DataUploadApi {
// Get uploaded files
static async getUploadedFiles() : Promise<Object> {
return WebapiBase.getAsync({uri: DATA_UPLOAD_ENPOINTS.FILES});
}
}
I've been following along with this document, but I think my syntax is slightly off with my mock:
import { mount } from '#vue/test-utils';
import DataUploadApi from '../webapi/DataUploadService';
import FileDownloadList from '../components/file-download-list.vue';
const mockGetUploadedFiles = jest.fn().mockResolvedValue({json: JSON.stringify(uploadedFilesObj)});
jest.mock('../webapi/DataUploadService', () => jest.fn().mockImplementation(() => ({getUploadedFiles: mockGetUploadedFiles})));
describe('file-download-list component', () => {
beforeEach(() => {
// #ts-ignore
DataUploadApi.mockClear(); // https://stackoverflow.com/a/52707663/1695437 dont use # imports on mocks.
mockGetUploadedFiles.mockClear();
});
describe('renders correct markup:', () => {
it('without any uploaded files', () => {
const wrapper = mount(FileDownloadList, {});
expect(wrapper).toMatchSnapshot();
});
});
});
This test passes. However, in the snapshot I can see that the API called failed with this error message:
<p>
_DataUploadService.default.getUploadedFiles is not a function
</p>
What have I done wrong with the function mock? Thanks in advance!
There seemed to be a few issues with my code:
Mocking the API
Using an internal mockImplementation seems to cause issues, and is not required if you don't need the additional mock functionality.
jest.mock('#/apps/gb-data/webapi/DataUploadService', () => ({
getUploadedFiles() {
return Promise.resolve({ uploaded_files: {} });
},
}));
Changes to the test
Both flushPromises and nextTick are required.
it('with uploaded files', async () => {
const wrapper = mount(FileDownloadList, {
stubs: fileDownloadListStubs,
});
await flushPromises();
await wrapper.vm.$nextTick();
expect(wrapper).toMatchSnapshot();
});

How To Reset Manual Mocks In Jest

I have a manual mock of crypto that looks like this:
// __mocks__/crypto.js
const crypto = jest.genMockFromModule('crypto')
const toString: Function = jest.fn(() => {
return {}.toString()
})
const mockStringable = {toString}
const update: Function = jest.fn(() => mockStringable)
const deciper = {update}
crypto.createDecipheriv = jest.fn(() => deciper)
export default crypto
Which is basically tested like this:
const crypto = require('crypto')
jest.mock('crypto')
describe('cookie-parser', () => {
afterEach(() => {
jest.resetAllMocks()
})
describe('decryptCookieValue', () => {
it('should call the crypto library correctly', () => {
const result = decryptCookieValue('test-encryption-key', 'test-encrypted-value')
expect(crypto.pbkdf2Sync).toHaveBeenCalledTimes(2)
expect(crypto.createDecipheriv).toHaveBeenCalled()
// more tests, etc, etc, etc
expect(crypto.createDecipheriv('', '', '').update).toHaveBeenCalled()
expect(result).toEqual({}.toString())
})
})
...
This works however if in that same test file, I test another method that invokes decryptCookieValue from within crypto.createDecipheriv no longer returns my mock decipher. Instead it returns undefined. For instance:
describe('cookie-parser', () => {
afterEach(() => {
jest.resetAllMocks()
})
describe('decryptCookieValue', () => {
it('should call the crypto library correctly', () => {
const result = decryptCookieValue('test-encryption-key', 'test-encrypted-value')
expect(crypto.pbkdf2Sync).toHaveBeenCalledTimes(2)
expect(crypto.createDecipheriv).toHaveBeenCalled()
expect(crypto.createDecipheriv('', '', '').update).toHaveBeenCalled()
expect(result).toEqual({}.toString())
})
})
...
...
describe('parseAuthenticationCookie', () => {
it('should create the correct object', () => {
// parseAuthenticationCookie calls decryptCookieValue internally
const result = parseAuthenticationCookie('', '') // Fails because internal call to crypto.createDecipheriv stops returning mock decipher.
expect(result).toEqual({accessToken: null})
})
})
})
I think this is an issue with resetting the manual mock because if I take that later test and move it into a file all by itself with the same surrounding test harness it works just fine.
// new test file
import crypto from 'crypto'
import { parseAuthenticationCookie } from './index'
jest.mock('crypto')
describe('cookie-parser', () => {
afterEach(() => {
jest.resetAllMocks()
})
describe('parseAuthenticationCookie', () => {
it('should create the correct object', () => {
// Works just fine now
const result = parseAuthenticationCookie('', '')
expect(result).toEqual({accessToken: null})
})
})
})
Is my assessment here correct and, if so, how do I reset the state of the manual mock after each test?
From Jest docs:
Does everything that mockFn.mockClear() does, and also removes any mocked return values or implementations.
ref: https://jestjs.io/docs/en/mock-function-api#mockfnmockreset
In your example you are assuming that calling resetAllMocks will set your manual mock back and it's not.
The reason why your test works in a separate file is because jest runs each file isolated, which is nice since you can screw up only the specs living in the same file.
In your particular case something that might work is calling jest.clearAllMocks() (since this will keep the implementation and returned values).
clearMocks options is also available at the jest config object (false as default), if you want to clear all your mocks on every test, this might be handy.
Hope this help you or anyone else having having a similar issue.
Bonus tip (no quite related) If you are mocking a module that it's being used internally by other module and in some specific test you want to mock that module again with a different mock, make sure to require the module that it's using the mocked module internally again in that specific test, otherwise that module will still reference the mock you specified next to the imports statements.
Looks like the better way to test this is something on the lines of:
jest.mock('crypto')
describe('decrypt()', () => {
afterEach(() => {
jest.resetAllMocks()
})
it('returns value', () => {
const crypto = require('crypto')
const encryptedValue = 'encrypted-value'
const update = jest.fn()
const pbkdf2SyncResult = 'test result'
crypto.pbkdf2Sync = jest.fn().mockImplementation(() => {
return pbkdf2SyncResult
})
crypto.createDecipheriv = jest.fn().mockImplementation((format, key, iv) => {
expect(format).toEqual('aes-256-cbc')
expect(key).toEqual(pbkdf2SyncResult)
expect(iv).toEqual(pbkdf2SyncResult)
return {update}
})
decrypt(encryptedValue)
const inputBuffer = Buffer.from(encryptedValue, 'base64')
expect(update).toHaveBeenCalledWith(inputBuffer)
})
})
This way I don't even have to have the manual mock and I can use mockImplementationOnce if I need to have the mock reset.

Jest: Change output of manual mock for different tests within a test suite

Let's say I have the following two files:
// index.js
...
import { IS_IOS } from 'common/constants/platform';
...
export const myFunction = () => (IS_IOS ? 'foo' : 'bar');
// index.test.js
...
import { myFunction } from './index';
jest.mock('common/constants/platform', () => ({ IS_IOS: true }));
describe('My test', () => {
it('tests behavior on IOS', () => {
expect(myFunction()).toBe('foo');
});
// --> Here I want to change the value of IS_IOS to false
it('tests behavior if NOT IOS', () => {
expect(myFunction()).toBe('bar');
});
});
As you see my mocking function returns IS_IOS: true. I want it to return IS_IOS: false after my first test. How would I do that?
I also tried an adaptation of the solution here but I couldn't get it work, because there the mock returns a function:
module.exports = {
foo: jest.genMockFunction();
}
whereas my mock should return a boolean value which is not called inside the file I'm testing.
That's what I did here:
// common/constants/__mock__/platform
export const setIsIos = jest.fn(val => (IS_IOS = val));
export let IS_IOS;
// index.test.js
...
import { IS_IOS, setIsIos } from 'common/constants/platform';
jest.mock('common/constants/platform');
describe('My test', () => {
setIsIos('foo');
it('tests behavior on IOS', () => {
expect(myFunction()).toBe('foo');
});
setIsIos('bar');
it('tests behavior if NOT IOS', () => {
expect(myFunction()).toBe('bar');
});
});
Oddly when console-logging, i.e. console.log(IS_IOS); I get the expected values. The test however seems to use the original value, i.e. undefined.
Add jest.resetModules() to the beforeEach() call of that describe() test suite:
describe('EventManager', () => {
beforeEach(() => {
jest.resetModules();
});
...
Additionally I found A more complete example on how to mock modules with jest here

Categories

Resources