Accessing useState value from jest test - reactjs

In a class based component I can easily access state by doing:
const control = shallow(<Test />);
control.state()
For a hooks based component like the one below, how can I access count from my test?
const Test = () => {
const [count, setCount] = useState(0);
return (
<>
<button onClick={() => setCount(count + 1)}>Count</button>
<div>{count}</div>
</>
)
}

It is not possible, and it shouldn't be, given that the state is an implementation detail. If you rename the state to myCount, the component still works, but the test would fail.
Alternative 1: Test the DOM
However, since you render the count, you can simply expect that the correct count is rendered.
Example using react-testing-library:
import { render, fireEvent } from 'react-testing-library'
const rendered = render(<Test />)
rendered.getByText(0) // Test that 0 is present in the DOM
fireEvent.click(rendered.getByText('Count'))
rendered.getByText(0) // Test that 1 is present in the DOM
Alternative 2: Extract the state logic in a reducer, and test the reducer
If you really want to test the state updates in isolation, you can useReducer, and test the reducer easily with jest, since it is a pure JavaScript function.

Related

Why change of Child state causes Parent rerender in React

I made a small experiment while learning React
https://codepen.io/bullet03/pen/abGoGvL?editors=0010:
React 17.0 with Fiber
2 components: Agregator(Parent) and Counter(Child)
Counter(Child) uses useState hook to change it's state
NB!!! Agregator(Parent) doesn't return JSX component, but calls Counter(Child) function. I know it's not casual way of using React, but this is the essence of the experiment
when click the button of Counter(Child) component we expect only Counter(Child) to be rerendered, but somehow Agregator(Parent) rerenders as well according to the console.log
Here comes the question: Why change of Counter(Child) component state causes Agregator(Parent) component rerender?
function Agregator() {
console.log("render Agregator");
return Counter();
}
function Counter() {
const [count, setCount] = React.useState(0);
const incCount = () => setCount((prev) => prev + 1);
console.log("render Counter", count);
return <button onClick={incCount}>{count}</button>;
}
ReactDOM.render(<Agregator />, document.getElementById("root"));
As far as I know, it shouldn't happen, because rerender of the component is caused by several cases:
change of state
change of props
change of context
forceUpdate
rerender of ancestor component
None of this is applicable to our case
NB!!! No need to rewrite this experiment to correct one, but it would be greate to get the explanation why in my case it works like this, so change of Child component state causes Parent component to rerender.
NB!!! Agregator(Parent) doesn't return JSX component, but calls Counter(Child) function. I know it's not casual way of using React, but this is the essence of the experiment
Then there's only one component at all, and no parent/child relationship, which renders (ha!) your experiment invalid.
Consider what would happen if you used your IDE's Inline Function feature on the Counter() invocation (which is essentially what the JS interpreter does, but with call stack and all that):
function Agregator() {
console.log("render Agregator");
// inlined...
const [count, setCount] = React.useState(0);
const incCount = () => setCount((prev) => prev + 1);
console.log("render Counter", count);
return <button onClick={incCount}>{count}</button>;
// ... end of inline.
}
ReactDOM.render(<Agregator />, document.getElementById("root"));

What is the pattern for ensuring a user action triggers a Recoil state update in a Jest test

Suppose I have a (rather contrived) controller and view
const useController = () => {
const setId = useSetRecoilState(idState)
return {
setId
}
}
const MyComponent = () => {
const { setId } = useController()
return (
<button onClick={() => setId(1)}>Click me!</button>
)
}
What is the correct pattern for testing
describe('my component', () => {
test.todo('when click button, state updated to 1')
})
This pattern comes up a lot for me when I have a component that sets state and I want to verify that the component is actually updating the state (say, via an <input> and that I haven't forgotten to add the recoil setter into the onChange event handler.
I can imagine rendering that component under test inside a custom wrapper that sets the whole state as the value of an INPUT or something and then in my test I get that INPUT's value and I have the state, but is there a better way?
I'm essentially trying to spy on the atom's setter I guess.
Edit To clarify, I'm following what even the recoil testing library says: if you're testing a hook that is just for one component, you should test them together. So my "unit tests" treat the unit as "controller-as-hook" and component (the view).
You don't need to spy on anything. Just act on the component or hook via react-test-library and trigger an action with that. Then you can use recoils snapshot_UNSTABLE() to generate a new snapshot of the recoil state which then allows you to test the value of an atom, selector, etc. against a expected value, like so:
const initialSnapshot = snapshot_UNSTABLE();
expect(initialSnapshot.getLoadable(idState).valueOrThrow()).toBe(0);
act(() => {/* act on your component or hook to mutate the state */});
const nextSnapshot = snapshot_UNSTABLE();
expect(nextSnapshot.getLoadable(idState).valueOrThrow()).toBe(1);

How to convert React Class Component fields into hooks

I know the gist of converting React Class components to functional components, but I found an instance where I nor my searching on the internet know the answer.
export default class Counter extends Component<Props, State> {
count = 0
updateCount = ()=> this.count +=1
render() {
return <div onClick={this.updateCount}>{this.count}</div>
}
}
Ignore the ugliness of the class, but how would i carry the count over into a functional component, with useRef?
The reason I ask is because in another class i am trying to convert, I have an async PromisePool running, that updates the downloaded variable each time a promise finishes, and when I tried to put downloaded into the state, it would always rerender the component and lose the data.
With useRef, you can create a variable which is not initialized on each re-render. The above component would look like
export default () => {
const count = useRef(0);
const updateCount = ()=> count.current +=1
render() {
return <div onClick={updateCount}>{count}</div>
}
}
However, you must know that updating a ref doesn't cause a re-render and hence updated value won't reflect in render
If you wish to trigger a re-render, make use of useState instead
export default () => {
const [count, setCount] = useState(0);
const updateCount = ()=> setCount(prevCount => prevCount + 1);
render() {
return <div onClick={updateCount}>{count}</div>
}
}
This is a direct example of moving the component over to a functional component. The count will remain in use as long as the component doesn't fully remount. It will remain even after re-renders.
export default function Counter(){
const [count,setCount] = useState(0);
return <div onClick={()=>setCount(count=>count+1)}>{count}</div>
}
Unless you are remounting this component by using a different key or changing the dom above it (i.e. adding a wrapping div after your promise finishes), then this should work fine (but in the instance of remounting, the class component would also reset it's counter).
You cannot carry the updated value of count with useRef since no rerender occurs when count is updated. useRef is for persisting an object in a component over multiple renders.
A possible solution for your PromisePools issue: Instead of converting the parent into a functional component, make use of the shouldComponentUpdate() lifecycle method in your class component to prevent a rerender of the child component when the state is changed.
Take a look at the lifecycle docs for more info:
https://reactjs.org/docs/react-component.html#shouldcomponentupdate

Testing onChange event with Jest

According to my code coverage, I need to test the function called in my onChange event. This is actually where I update my functional component's state using the useState hook.
Here is my component :
const Component:React.FC<{}> = () => {
const {value, setState} = useState('');
return(
<View>
<CustomComponent
onChange={(value) => setState(value)}
/>
</View>
)
}
The customComponent is a component derived from the React Input component.
When text is changed inside of it, it calls the onChange function passed as its prop with the text. This is how it comes up to the parent Component which sets the value of the text input in its state as displayed above.
My code coverage returns this analysis :
onChange={//(value) => setState(value)//}
Where the code between the // has to be covered. I don't really understand how I can cover this. First thought was use mock functions, but I can't seem to find how to mock it to the onChange event since I don't pass anything as prop to the main Component.
After a few tests, I finally understood that the coverage wasn't asking for the actual test of the onChange function but actually the value that is evaluated. Therefore, here is what I am doing:
Fetching the TextInput Child component
Changing its Text
Evaluating what it renders
I am using #testing-library/react-native here because it makes selecting tree components easier with the use of accessibilityLabel for example (It actually made me understand the importance of that prop).
Here is what a test looks like:
describe('Testing useState functions', () => {
test('test', () => {
//Rendering the component and its tree
const { container, getByLabelText } = render(<SignupView />);
//Extracting the child, username_input component with his accessibilityLabel
const username_input = getByLabelText('username_input');
const email_input = getByLabelText('email_input');
//Fire a native changeText event with a specific value
fireEvent.changeText(username_input, 'doe');
fireEvent.changeText(email_input, 'doe#joe.com');
//Checking the rendered value
expect(username_input.props.value).toEqual('doe');
expect(email_input.props.value).toEqual('doe#joe.com');
});
});

Set state when testing functional component with useState() hook

When I tested class component with enzyme I could do wrapper.setState({}) to set state. How can I do the same now, when I am testing function component with useState() hook?
For example in my component I have:
const [mode, setMode] = useState("my value");
And I want to change mode inside my test
When using state from hooks, your test must ignore implementation details like state in order to properly test it.
You can still make sure the component passes the correct state into its children.
You can find a great example in this blog post written by Kent C. Dodds.
Here's an excerpt from it with a code example.
Test that relies on state implementation details -
test('setOpenIndex sets the open index state properly', () => {
const wrapper = mount(<Accordion items={[]} />)
expect(wrapper.state('openIndex')).toBe(0)
wrapper.instance().setOpenIndex(1)
expect(wrapper.state('openIndex')).toBe(1)
})
Test that does not rely on state implementation details -
test('counter increments the count', () => {
const {container} = render(<Counter />)
const button = container.firstChild
expect(button.textContent).toBe('0')
fireEvent.click(button)
expect(button.textContent).toBe('1')
})
This is the way that I found to do it, I'm not saying this is right or wrong. In my case, a block of code was dependent on state being set to a particular value. I will keep my opinions about testing in React to myself.
In your test file:
Adjust your import for the react library
import * as React from 'react'
Then in your test spy on useState and mock its implementation
const stateSetter = jest.fn()
jest
.spyOn(React, 'useState')
//Simulate that mode state value was set to 'new mode value'
.mockImplementation(stateValue => [stateValue='new mode value', stateSetter])
Please be aware that mocking useState this will way apply to all instances where useState is called for your test, so if you have more than one state value that you are looking at, they will all be set to 'new mode value'. Someone else may be able to help you sort that out. Hope it helps.
At top of test file, can be defined first as:
import { useState } from 'react';
jest.mock('react', () => ({
...jest.requireActual('react'),
useState: jest.fn()
}));
const useStateMock: jest.Mock<typeof useState> = useState as never;
After that at each test can be used with different value which is wanted to be tested:
const setValue = jest.fn();
useStateMock
.mockImplementation(() => ['value', setValue]);

Resources