Jest basics: Testing function from component - javascript

I have a basic function:
components/FirstComponent:
sayMyName = (fruit) => {
alert("Hello, I'm " + fruit);
return fruit;
}
When I try to test it with Jest inside FirstComponent.test.js:
import FirstComponent from '../components/FirstComponent';
describe('<FirstComponent />', () => {
it('tests the only function', () => {
FirstComponent.sayMyName = jest.fn();
const value = FirstComponent.sayMyName('orange');
expect(value).toBe('orange');
});
});
Test says: Comparing two different types of values. Expected string but received undefined.
Apparently I'm not importing the function to test the right way?
I was not smart enough to understand the Jest documents how to test functions from components..
Is there some simple way to import function from component and test it?
Edit:
This works now using the 'react-test-renderer'
import FirstComponent from '../components/FirstComponent';
import renderer from 'react-test-renderer';
describe('<FirstComponent /> functions', () => {
it('test the only function', () => {
const wrapper = renderer.create(<FirstComponent />);
const inst = wrapper.getInstance();
expect(inst.sayMyName('orange')).toMatchSnapshot();
});
})

You have stubbed the function with one that does not return anything. FirstComponent.sayMyName = jest.fn();
To test the function, typically you can just do
// if static etc
import { sayMyName } from '../foo/bar';
describe('bar', () => {
it('should do what I like', () => {
expect(sayMyName('orange')).toMatchSnapshot();
});
})
This will store the output ("orange") and assert that every time you run this test, it should return orange. If your function stops doing that or returns something else, snapshot will differ and test will fail.
the direct comparison .toBe('orange') will still be possible but the really useful thing about jest is snapshot testing so you don't need to duplicate logic and serialise/deep compare structures or jsx.
if it's a component method, you need to render it first, getInstance() and then call it.

Related

How to only mock

jest unit test question here. If I am testing function A, and function A uses function B,
import { funcB } = require('./FileB');
const funcA = () => {
const someData = funcB();
}
Does this mean that, in my unit test file testing funcA, in order to mock funcB, do i HAVE to import the function to mock it? which means the file wouldn't use that function but simply there for mocking purposes or do I have to mock the entire module in which funcB lives on and mock the implementation of that single function?
my goal here is, I dont want every single test in my unit test file to have funcB mocked, I only want it mocked on some test but not all while testing funcA.
So if i have 4 test and 3 of them need it mocked but 2 need its actuall implementation, whats a best way to approach this?
One possible solution - use spyOn and mockRestore for following implementation:
export const funcB = () => {
console.log('original implementation of B')
return 42;
}
import { funcB } from './fileb';
export const funcA = () => {
const someData = funcB();
return someData;
}
import * as fileB from "../funcab/fileb";
import { funcA } from "../funcab/filea";
describe('testing 1...2...3...', () => {
const spy = jest.spyOn(fileB, 'funcB');
afterEach(() => {
spy.mockRestore();
})
it('will test with mocked funcB', () => {
spy.mockReturnValue(1);
expect(funcA()).toEqual(1);
});
it('will test without mocked funcB', () => {
expect(funcA()).toEqual(42);
})
})
Here is a post about Jest mock and spy — mockClear vs mockReset vs mockRestore. I hope you'll find it usefull.
Answer for comment - do you mean sth like this?
const mockFuncB = jest.fn();
jest.mock("../funcab/fileb", () => ({
funcB: () => mockFuncB(),
}));
then the test would look like:
it('will test with mocked funcB', () => {
mockFuncB.mockReturnValue(1);
expect(funcA()).toEqual(1);
});

Unit test a TypeScript void function with sinon

I am developing a VS Code extension and currently I am in the process of doing unit tests. I am complete new to unit testing and I am quite lost on how I should go about unit testing a void function.
export const getChecklistItems = async (id: number): Promise<any> => {
const checklistItems: ChecklistItem[] = [];
const items: any[] = await getItemsFromUrl(`path_to_url/{id}`);
items.map((item) => checklistItems.push(new ChecklistItem(item.checklist_items_id, item.checklist_items_content)));
return checklistItems;
};
export const onSelectInsertComment = async (id: number): Promise<void> => {
const editor: TextEditor | undefined = window.activeTextEditor;
const items: any[] = await getChecklistItems(id);
items.reverse(); // Workaround to output comments in correct numerical order inside the TextEditor
if (!editor) {
window.showWarningMessage('No editors are open in you workspace');
} else {
items.forEach((item) => {
editor?.insertSnippet(new SnippetString(`$LINE_COMMENT ${item.label}\n`));
});
window.showInformationMessage('Great. Checklist items have been inserted');
}
};
I am using mocha, chai and sinon for my unit tests. I am trying to use a spy to check whether getCheckListItems(id) is called when onSelectInsertComment(id) is called. One the the things I tried is the following:
describe('#onSelectInsertComment', () => {
it('should be resolved', () => {
const id = 1;
const spy = sinon.spy(getChecklistItems);
spy.withArgs(id);
onSelectInsertComment(id);
sinon.assert.calledOnce(spy);
});
});
The result is: AssertError: expected getChecklistItems to be called once but was called 0 times.
What am I doing wrong? I would appreciate it if I was pointed into the right direction. Thanks.
Notes:
I do not know if this works for VSCode extension, but this works for normal typescript project with sinon.
For the example: I assume that both getChecklistItems and onSelectInsertComment are in the same file: util.ts.
In your test, rather than use this import:
import { getChecklistItems, onSelectInsertComment } from './util';
Use this:
import * as util from './util';
And change your test code to:
import sinon from 'sinon';
import * as util from './util';
describe('#onSelectInsertComment', () => {
it('should be resolved', () => {
const id = 1;
const spy = sinon.spy(util, 'getChecklistItems');
spy.withArgs(id);
util.onSelectInsertComment(id);
sinon.assert.calledOnce(spy);
});
});
Why?
Because in your original code, you use sinon spy to wrap your function getChecklistItems, but function onSelectInsertComment has already defined and not use the spy defined, but use the original getChecklistItems. You can check references 1.
References
Sinon How to Stub Dependency
Sinon Spy Method Signature

How to import test from test1.spec file to test2.spec file

The test1.spec.js have multiple tests and multiple it block, I just to import only one it block in test2.spec.js
In your test2.spec you say:
module.exports.test2 = describe("Test 2", () => {
// Here you put your test code using beforeAll, AfterAll, It, etc.
});
In your test1.spec you say:
const test2 = require("./test2.spec");
Then within "describe o it" section you call test2 as a normal function, ex.:
text2();
Note: if you get an exception handle, just put the code where you call the function within a try/catch.
You need a function wrapper around the test.
Spec1
export const runTest = () => {
it('is a test that will run when runTest is called', () => expect(1).to.eq(1))
}
runTest() // if you want to run it here, but you can just define it here
Spec2
import { runTest } from './Spec1.js'
runTest()
Also quite useful if you want to parameterize the test
export const runTest = (expectedValue) => {
it('is a test that will run when runTest is called', () => {
expect(1).to.eq(expectedValue)
})
}
runTest(1) // passes
runTest(2) // fails

Mocking a function returned by a react hook

I'm building a pagination using the useQuery hook as part of the Apollo Client in React, which exposes a function called fetchMore seen here: https://www.apollographql.com/docs/react/data/pagination/
Everything works fine, but I'm trying to write a test one of the use cases, which is when the fetchMore function fails due to a network error. The code in my component looks like this.
const App = () => {
// Some other component logic
const {loading, data, error, fetchMore} = useQuery(QUERY)
const handleChange = () => {
fetchMore({
variables: {
offset: data.feed.length
},
updateQuery: (prev, { fetchMoreResult }) => {
if (!fetchMoreResult) return prev;
return Object.assign({}, prev, {
feed: [...prev.feed, ...fetchMoreResult.feed]
});
}
}).catch((e) => {
// handle the error
})
}
}
Basically I want to test the case where the fetchMore function function throws an Error. I DON'T want to mock the entire useQuery though, just the fetchMore function. What would be the best way to mock just the fetchMore function in my test?
One way to do it is to just mock the hook
In your spec file:
import { useQuery } from '#apollo/react-hooks'
jest.mock('#apollo/react-hooks',() => ({
__esModule:true
useQuery:jest.fn()
});
console.log(useQuery) // mock function - do whatever you want!
/*
e.g. useQuery.mockImplementation(() => ({
data:...
loading:...
fetchMore:jest.fn(() => throw new Error('bad'))
});
*/
You could also mock the stuff that goes on "behind the scenes" to simulate a network error, do whatever you need to to test your catch.
EDIT:
Search for __esModule: true on this page and you'll understand.
It's probably easier to just mock the whole function and return everything as mock data. But you can unmock it to use the real one so as not to conflict with other tests.

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.

Categories

Resources