beekai-oss / little-state-machine

📠 React custom hook for persist state management
https://lrz5wloklm.csb.app/
MIT License
1.48k stars 53 forks source link

Setting up testing patterns for LSM (React Testing Lib + Jest) - LSM returns empty object in JSDOM #142

Closed Dujota closed 1 year ago

Dujota commented 1 year ago

I finally re-visited the topic issue#111 in order to resolve this outstanding item. Originally, I had opted for a workaround by completely mocking out LSM and its return values. Although this "works" and is very usefull when you just want to programatically manipulate component ui without any simulated interactions, this still did not resolve the root problem (How do I test the behaviour of a component that relies on LSM).

for example:

// mockLSM.js
import * as LittleStateMachine from 'little-state-machine';

  jest.mock('little-state-machine', () => ({
    useStateMachine: jest.fn(),
  }));

export const stateMachineSpy = jest.spyOn(LittleStateMachine, 'useStateMachine');

// overload the hook and allow me to return any value or action 
export function useSpyStateMachine(state = {}, actions = {}) {
  return stateMachineSpy.mockReturnValue({
    state,
    actions,
  });
}

// then in a test we use it like so:

test('should do something', () => {
    useSpyStateMachine(
      { 
        notification: {  // state.notification
          status: 'success',
          message:'it works!'
        },
      },
      {
        updateNotification: jest.fn(),  //actions.updateNotification
      }
    );

     // at this point the mock data is injected by jest and we can test results 

    const { container } = render(<Comp />);
    // do some test stuff + assert
  });

This is fine, when doing unit testing or just trying to assert some branching logic that depends on the store's current state value . Unfortunately it was not enough since I could not properly render the app in the same way a user would experience it, which was the original goal (test behaviour based on user interaction with app) .

Again, the goal is provide a seamless, automated way to render components in the same way that the application would spin up; wrapped in LSM provider without having to add repetitive boilerplate code to every test (we have over 900+ Test Suites).

So the issue boild down to this: LSM, when rendered by RTL/Jest env=jsdom, returned an empty object on initialization since its running in nodejs first then loaded into jsdom.

Solution:

The setup:

// test-utils.js 

import React from 'react';
import { render as rtlRender } from '@testing-library/react';
import { createStore } from 'little-state-machine';
import Providers from 'components/Providers';

// wrapper should pass down props to 
const Wrapper = (props) => {
  const { component: Component } = props;
  createStore({ storeName: { some: 'value' } });

  return <Component {...props} />;
};

// allow providers to receive custom props if necessary
const render = (ui, { languageCode, ...options }) => {
  const AllTheProviders = ({ children }) => <Providers languageCode={languageCode}>{children}</Providers>;

  return rtlRender(ui, { wrapper: AllTheProviders, ...options });
};

// re-export everything
export * from '@testing-library/react';

// override render method
export { render, Wrapper }; 

Usage: act is required for handling state updates caused by LSM

import { render, Wrapper, screen, act } from 'lib/test-utils';
import MyTestComp from 'components/MyTestComp';

describe('<MyTestComp />', () => {
  test('renders Loader while fetching', async () => {

    await act(async () => {
      render(<Wrapper component={MyTestCompnent} handleClick={jest.fn()} />, { languageCode: 'en-ca' });
    });

    const loaderElement = screen.getByText('Loader');  
    expect(loaderElement).toBeInTheDocument();
  });  
})

In summary, yes all tests should be wrapped by their own providers (which was original intent anyway), but LSM needs to initialize the store within a wrapper component so that the lib can load by the time the test comp gets rendered by RTL.

I hope this helps save some time for others since there's literally 0 examples out there.

bluebill1049 commented 1 year ago

thanks a lot for sharing 🙏 I am used to doing the following

createStore({
  ....
})

<StateMachineProivder>

</StateMachineProivder>

in each of my test cases without the abstraction as you did. Maybe i should include an example in the read me.