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:

Setup the Mock at top level, outside any function, directly

  // 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 the HTML to view it, inside test

// Print HTML during Test, with MAX buffer limit
 screen.debug(undefined, Infinity);
 // OR use System ENV var -> DEBUG_PRINT_LIMIT

Ref-1. Assert correct 'props' were passed to the mocked component, inside test

expect(mockPropsCaptorer_XXXMYCOMPONENTXXX).toHaveBeenCalledWith({'enter-anything-first-time-to-let-test-throw-expected-val':''});

Ref-2. Assert mocked component HTML was rendered, in test

// 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'

No need for TOP level mock declaration, if sometimes using SPIED IMPL and sometimes REAL IMPL

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
      }));
});

Spy reference in beforeAll and its typescript type

let mySpy: SpyInstance;
      beforeAll(() => {
        mySpy = jest.spyOn(mySpiedModule, 'namedExportName | default'); // Note 'default' is the word to be used for default export spy
      });

Parameterised Jest Test

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

    });

UI rendering AND interaction testing using React Testing library

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.

Mock @inmo module from node-modules, which is scoped package due to presence of @

... 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 : image

Inject pulled out functions as props using action handler:

image

Prop Type:

image

Action Handler:

image

Test code for above:

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();

image

Assertions and Test for above

      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();
      });

image


⚠️ **GitHub.com Fallback** ⚠️