0
votes

Can we skip a react hook ("getderivedstatefromprops") while unit test of a component using Enzyme and jest.

I am writing a Counter component, whose initial state is set as zero and new state is received from parent component, while simulate the increment or decrement function in unit test, changed state is override by parents initial state due to "getderivedstatefromprops" hook. In component it is called only once but in unit test when we update the component instance after simulating increment function it receive the initial value and state is changed to initial state. Is this any way while creating instance of component skip this hook?

export default class Counter extends React.Component<CounterProps, 
   CounterState> {
     state = {
      value: 0,
     }

   static getDerivedStateFromProps(props, state)   {
      return { ...state, value: props.initialValue }     
   }

   incrementCounter = () => {
     const { value } = this.state

     const { id, maxValue, countChangeHandler, isDisabled } = this.props

       const newValue = value + 1

       if (newValue <= maxValue) {
         countChangeHandler(id, newValue) 
       }

      this.setState({ value: newValue })
  }

  render() {   
    
     const { value } = this.state
     const { maxValue, isDisabled } = this.props

    return (
      <StyledCounter>
        <Count disabled={isDisabled}>{value}</Count>
        <Action onClick={this.incrementCounter}>
          <Icon shape="plusGray" size="20px" />
        </Action>
        <Label>{'max ' + maxValue}</Label>
      </StyledCounter>
    )}

}

unit test:

describe('Counter components', () => {
  function renderCounter(customProperties = {}) {
    const properties = { 

      id: 124355,

      initialValue: 3,

      maxValue: 5,

      isDisabled: false,

      countChangeHandler: jest.fn(),
      ...customProperties,
    }
    const { output } = renderComponent(<Counter {...properties} />)

    return output
  }

  afterEach(() => {
    jest.resetAllMocks()
  })

  it('On multiple click on Increment button, counter value should not exceed max value.', () => {
    const component = renderCounter()

    component.find('div#increment').simulate('click')
    component.find('div#increment').simulate('click')
    component.find('div#increment').simulate('click')
    component.find('Counter').update()
    expect(component.find(Counter).state('value')).toEqual(5)
  })  

})

renderComponent is using Mount. unit test fails expecting 5, but received 3.

1
does it work in real project code? I believe it should reset state because of gDSFP as well as it's going in your test.skyboyer
correct, gDSFP reset the value but countChangeHandler, sends the updated value to parent and it passes, again to child component. countChangeHandler have other responsibility. But unit test, I have to check incrementCounter in isolation.Rupesh Agrawal
I see your point. I just pretty sure you don't need to isolate part of component but test it in a whole for more reliable tests in result.skyboyer

1 Answers

0
votes

So your component is something like controlled input pattern in React when component itself does not update some of its state and should rely on parent for that.

I believe you don't need any special trick to test it. What I'd test here:

  1. updating props.value affects result of render
  2. clicking on button calls props.countChangeHandler with props.value + 1 while it less than props.maxValue
  3. clicking on buttons does not call props.countChangeHandler at all if it's more than props.maxValue
const countChangeHandlerMocked = jest.fn();
function createCounter({ initialValue, maxValue }) {
  return shallow(<Counter 
    initialValue={initialValue} 
    maxValue={maxValue} 
    id="mock_id" 
    countChangeHandler={countChangeHandlerMocked} 
  />);
}

it('renders props.value as a Count`, () => {
  const wrapper = createCounter({initialValue: 42, maxValue: 100});
  expect(wrapper.find(Count).props().children).toEqual(42);
  expect(countChangeHandlerMocked).not.toHaveBeenCalled();
});

it('keeps Count in sync with props.value on update', () => {
  const wrapper = createCounter({initialValue: 42, maxValue: 100});
  wrapper.setProps({ initialValue: 43 });
  expect(wrapper.find(Count).props().children).toEqual(43);
  expect(countChangeHandlerMocked).not.toHaveBeenCalled();
});

it('reacts on button click if value< maxValue', () => { 
  const wrapper = createCounter({ initialValue: 1, maxValue: 10 });
  wrapper.find(Action).props().onClick();
  expect(countChangeHandlerMocked).toHaveBeenCalledWith('mocked_id', 2);
});

it('does not react on button click if value == maxValue', () => {
  const wrapper = createCount({ initialValue: 10, maxValue: 10 });
  wrapper.find(Action).props().onClick();
  expect(countChangeHandlerMocked).not.toHaveBeenCalled();
})

See, the more you are relying on implementation details like state or lifecycle methods the more fragile tests become. Say having tests as above it's easy to refactor component to be functional one or composition of several(just maybe we would need to use mount() instead of shallow) and both component and tests would be ok. In contrary relying on implementation details too much will make your tests failing even after legit changes(say removing gDSFP and using props.initialValue instead of state.value in the render()).

Does it make sense?