React Unit Testing - mosinn/DOCS-n-Snippets-n-Steps GitHub Wiki
Mocking - To WHOLE-ISTICALLY Mock a child module i.e. every function exported from it, while testing a Parent module:
// import ...
// Soon after 'imports' within the Test file, do below mock declaration at top level, ie. NOT inside any func(), including any beforeAll():-
// WARNING: below mock function name MUST begin with prefix mock...else it doesn't seem to work
const mockPropsCaptorer_XXXMYCOMPONENTXXX = jest.fn();
// @ts-ignore // To resolve lint error on 'props' missing a 'type' definition
// WARNING: below jest.mock invocation MUST happen at TOP level, NOT inside any HELPER function. Else mocking doesn't seem to work
jest.mock('./<<relative-to-test-file>>/XXXMYCOMPONENTXXX', () => (props) => {
// Ref-1. Capture 'props' into an EXTERNAL mock func, is ONE thing we do here, as that is later used in test
mockPropsCaptorer_XXXMYCOMPONENTXXX(props);
// Ref-2. PRINT simplified html is SECOND thing we can do here, so that mocked component prints some HTML.
// We can assert later that Parent document contains mocked child
return <div>--html--XXXMYCOMPONENTXXX</div>
});
// Print HTML during Test, with MAX buffer limit
screen.debug(undefined, Infinity);
// OR use System ENV var -> DEBUG_PRINT_LIMIT
expect(mockPropsCaptorer_XXXMYCOMPONENTXXX).toHaveBeenCalledWith({'enter-anything-first-time-to-let-test-throw-expected-val':''});
// Assert component was rendered inside dome >>
expect(screen.getByText('--html--XXXMYCOMPONENTXXX')).toBeInTheDocument();
Spying - Spy on a Parent (under test) module's, specific import-ed dependency child module's NAMED EXPORT FUNCTION (export funcXYZ = () =>{..}) (not {object), so that I control what that spied function returns during testA on Parent, but during testB on Parent, uses ACTUAL IMPL of that spied function
Import EVERY EXPORT available in module-having-function-to-be-spied dependency, inside TEST file using syntax below:
import * as mySpiedModule from './hooks/myParentsDependencyModuleWithSomeNamedExportsToBeSpied'
In describe(... test case, where spy-impl is needed, we specify SPY implementation in beforeEach(), then in afterEach we RESTORE REAL implementation, so that OTHER describes() DONT get impacted or dont see modified implementation
describe('testing needing SPY implementation', () => {
// @ts-ignore
let spyOn_mySpiedModule_funcXYZ;
beforeAll( () => {
// Spy ref handle is created at this point, AND, NOW implementation is jests' dummy MOCK, so even after no more override, we still need to RESTORE once done with test
spyOn_mySpiedModule_funcXYZ = jest.spyOn(mySpiedModule, "funcXYZ");
});
afterAll(()=>{
// @ts-ignore
// This mockRestore() on spy ref, is what removes any mock implementations overriding original impl
spyOn_mySpiedModule_funcXYZ.mockRestore();
});
it ('should use modified mock implementation returning value1 on some specific func of spied module', async () => {
// PROVIDE MOCK IMPLEMENTATION and USE subsequently the effects inside parent, while running a test
spyOn_mySpiedModule_funcXYZ.mockImplementation(() => {return {someProp: true}});
// invoke Parent under test rendering as usual
// assert Parent behaviour with value1
}));
it ('should use modified mock implementation returning value2 on some specific func of spied module', async () => {
// PROVIDE MOCK IMPLEMENTATION and USE...
spyOn_mySpiedModule_funcXYZ.mockImplementation(() => {return {someProp: false}});
// invoke Parent under test rendering as usual
// assert Parent behaviour with value2
}));
});
let mySpy: SpyInstance;
beforeAll(() => {
mySpy = jest.spyOn(mySpiedModule, 'namedExportName | default'); // Note 'default' is the word to be used for default export spy
});
describe('my button handling test', () => {
it.each
`
arg1Name | arg1Name | expected | scenarioDescToAppearAsTestTitleDuringTheRun
${false} | ${{a: 'abc'}} | ${11} | ${'should test blah blah 1'}
${true} | ${{a: 'def'}} | ${11} | ${'should test blah blah 2'}
`
('my button handling test report title when "$scenarioDescToAppearAsTestTitleDuringTheRun" , arg1Name is "{$arg1Name}" ', ({arg1Name , arg2Name , expected}) : void => {
// Test Body
});
describe('MyCompoUnderTest', () => {
describe('ENTRY mode', () => {
beforeEach(() => {
inputProps = { ...inputProps, mode: 'ENTRY' };
});
it('renders all UI elements', () => {
render(<MyCompoUnderTest {...inputProps} />);
// Cross Button
expect(screen.getByRole('button', { name: 'Close Dialog' })).toBeInTheDocument();
// Dialog
expect(screen.getByRole('dialog', { name: 'Person Details' })).toBeInTheDocument();
expect(screen.getByRole('heading', { name: 'Person Details' })).toBeInTheDocument();
// Input Fields
expect(screen.getByRole('textbox', { name: 'Email Address' })).toBeInTheDocument();
// Buttons
expect(screen.getByRole('button', { name: 'Cancel' })).toBeInTheDocument();
// Note Text
expect(screen.getByText('ipsum dorem')).toBeInTheDocument();
// User typing
let fieldRef = screen.getByRole('textbox', { name: 'Person Details' });
userEvent.clear(fieldRef);
userEvent.type(fieldRef, 'new xyz name');
expect(fieldRef.getAttribute('value')).toEqual('new xyz name');
});
});
// Note:
// name - 'Close Dialog' comes from Actual UI dsisplayed label, seen using screen.debug(undefined, Infinity);
// role name- button: is advised by delibrately trying to do screen.get... then letting the error message of failure ProcessingInstruction.
// This lists ALL available elements and ignores noise.
... after import statements inside the test case file
const mockPropsCaptorer_AsyncAutoCompxxxField = jest.fn();
const mockPropsCaptorer_StyledAutoCompxxxField = jest.fn();
.... others if needed (see actual mock body)
// @ts-ignore
jest.mock('@inmo/autocompxxx', () => {
return {
__esModule: true,
// @ts-ignore
AsyncAutoCompxxxField: (props) => {
mockPropsCaptorer_AsyncAutoCompxxxField(props);
return (<div>--html--AsyncAutoCompxxxField</div>)
},
// @ts-ignore
StyledAutoCompxxxField: (props) => {
mockPropsCaptorer_StyledAutoCompxxxField(props);
return (<div>--html--StyledAutoCompxxxField</div>)
}
, ...all [ONLY if used in class under test] or nothing from the package
}
});
Mocking Scoped Module, which needs to be mocked IFF, it does not allow proper rendering or interaction in screen.debug > DOM output
Eg. challenge is :
Inject pulled out functions as props using action handler:
Prop Type:
Action Handler:
import {XxxOptionRenderer} from "./XxxOptionRenderer";
// This "props-captorer" COMPONENT FIELD represents mock " Component" which actually receives "props"
// Useful to call expect().toHaveBeenCalledWith(..args)
const mockPropsCaptorer_AsyncAutoCompleteField = jest.fn();
// This "props-captorer" ONLY stores received "props"
// Useful for testing "Function" props, by allowing to actually call the function, then assert the mock ..toHaveBeenCalled() is true.
// No arg checking is needed here else may land in again func arg issue if passed from within
// @ts-ignore
let mockPropsCaptorer_AsyncAutoCompleteField_Props: {
// NOTE: IDE Generates this using "infer" option near RedBulb :)
[x: string]: any; id: any; hideLabel: any; labelPosition: any; label: any; mandatory: any; mode: any; showDropdownArrow: any;
loadOptions: any; multi: any; clearable: any; cache: any; optionRenderer: any; defaultOptions: any; value: any; placeholder: any; loadingPlaceholder: any; noResultsText: any;
};
// We mock the module here and provide above "props-captorer"
// @ts-ignore
jest.mock('@xxx/autocomplete', () => {
return {
__esModule: true,
// @ts-ignore
AsyncAutoCompleteField: (props) => {
mockPropsCaptorer_AsyncAutoCompleteField_Props = props;
mockPropsCaptorer_AsyncAutoCompleteField(props);
return (<div>--html--AsyncAutoCompleteField</div>)
},
// @ts-ignore
XXXAutoCompleteField: .... // others from same module, if used in class under test
}
});
// Mocks for actionHandler injected functions, to be set while rendering component during it() test
const mockXxxAsyncAutoCompleteLoadOptions = jest.fn();
const mockOnXxxAsyncAutocompleteChange = jest.fn().mockImplementation( (id: string, value: any, setLocalState: ()=> void) => {
setLocalState();
});
const mockOnXxxAsyncAutocompleteSetLoaclState = jest.fn();
const mockXxxAsyncAutocompleteNoResults = jest.fn();
it('XXXwhen address book xxx is selected while opening the dialog', () => {
render(<XxxDetailsDialog {
...{
...inputProps,
actionHandlers: {
...inputProps.actionHandlers,
selectedXxxMetaLineItem: xxxGridDataWhenPreSelectedFixture
}
}
} />);
screen.debug(undefined, Infinity);
expect(screen.getByText('--html--AsyncAutoCompleteField')).toBeInTheDocument();
expect(mockPropsCaptorer_AsyncAutoCompleteField_Props.id).toEqual('xxxOption');
expect(mockPropsCaptorer_AsyncAutoCompleteField_Props.optionRenderer).toEqual(XxxOptionRenderer);
// Mock func based assertions - moved to here
mockPropsCaptorer_AsyncAutoCompleteField_Props.onChange('id', {key: 'val'});
expect(mockOnXxxAsyncAutocompleteChange).toHaveBeenCalled();
//expect(mockOnXxxAsyncAutocompleteChange).toHaveBeenLastCalledWith('id', {}, mockOnXxxAsyncAutocompleteSetLoaclState);
// Below wont work as setLocalState is func I cannot equate, so in mockImpl of mockOnXxxAsyncAutocompleteChange, receive
// all 3 args ie id:string, value:any, setLocalState:()=>void, then just in body call the 3rd arg
// No need to try and assert 1st mock is called with args..just assert 1st mock is just called
// Then, because of additional mock impl body, assert 2nd mock is also called
//expect(mockOnXxxAsyncAutocompleteChange).toHaveBeenLastCalledWith('id', {key: 'val'}, <<<setLocalState????>>);
expect(mockOnXxxAsyncAutocompleteChange).toHaveBeenLastCalledWith('id', {key: 'val'}, expect.anything());
expect(mockOnXxxAsyncAutocompleteChange).toHaveBeenCalled();
mockPropsCaptorer_AsyncAutoCompleteField_Props.noResultsText();
expect(mockXxxAsyncAutocompleteNoResults).toHaveBeenCalled();
});