# React Common Testing Patterns
## Stateful component
```jsx
test('my component updates state on button click', () => {
// Render the component
const { getByText } = render(<MyComponent />);
// Find the button element
const button = getByText('Click me');
// Use the act function to wrap the code that interacts with the component
act(() => {
// Click the button
fireEvent.click(button);
});
// Assert that the component's state has been updated as expected
expect(getByText('Button has been clicked')).toBeInTheDocument();
});
```
## Container component
```jsx
test('my container passes data to its child component', () => {
// Render the container with some data
const data = [{ id: 1, name: 'Item 1' }, { id: 2, name: 'Item 2' }];
const { getByText } = render(<MyContainer data={data} />);
// Assert that the child component receives the correct data
data.forEach(item => {
expect(getByText(item.name)).toBeInTheDocument();
});
});
```
## Higher-order component
```jsx!
import React from 'react';
import {
render, fireEvent, screen,
} from '@testing-library/react';
// This is the higher-order component that we want to test
function withLoadingIndicator(WrappedComponent: React.ComponentType<any>) {
return function WithLoadingIndicator(props: any) {
const { isLoading, ...otherProps } = props;
if (isLoading) {
return <div>Loading...</div>;
}
// eslint-disable-next-line react/jsx-props-no-spreading
return <WrappedComponent {...otherProps} />;
};
}
// This is the wrapped component that the higher-order component will be used with
function MyButton(props: any) {
const { name, onClick } = props;
return (
// eslint-disable-next-line react/button-has-type
<button onClick={onClick}>
{name}
</button>
);
}
describe('withName', () => {
let MyLoadingButton: React.ComponentType<any>;
beforeEach(() => {
MyLoadingButton = withLoadingIndicator(MyButton);
});
it('returns a valid React component', () => {
// First, we'll make sure that the higher-order component
// returns a valid React component when it's called
expect(MyLoadingButton).toBeInstanceOf(Function);
});
it('renders the wrapped component with the correct props', () => {
// Next, we'll render the returned component and make sure
// that it correctly renders the wrapped component with the correct props
const { getByText } = render(<MyLoadingButton name="Click me!" />);
const button = getByText('Click me!');
expect(button).toBeDefined();
});
it('handles events correctly', () => {
// Then, we'll test that the returned component correctly handles events
const handleClick = jest.fn();
const { getByText } = render(<MyLoadingButton name="Click me!" onClick={handleClick} />);
const button = getByText('Click me!');
fireEvent.click(button);
expect(handleClick).toHaveBeenCalled();
});
it('passes isLoading=true, test the isLoading effect', () => {
// Finally, we'll test that the returned component correctly
// passes additional props down to the wrapped component
const handleClick = jest.fn();
const { getByText, queryByText } = render(<MyLoadingButton isLoading name="Click me!" onClick={handleClick} />);
const div = getByText('Loading...');
expect(div).toBeDefined();
const button = queryByText('Click me!');
expect(button).toBeNull();
});
it('passes isLoading=false, test the isLoading effect', () => {
// Finally, we'll test that the returned component correctly
// passes additional props down to the wrapped component
const handleClick = jest.fn();
const { getByText, queryByText } = render(<MyLoadingButton isLoading={false} name="Click me!" onClick={handleClick} />);
const div = queryByText('Loading...');
expect(div).toBeNull();
const button = getByText('Click me!');
expect(button).toBeDefined();
});
});
```
## Custom hook
### Simple case: `useState`
- borrow the codes from https://kentcdodds.com/blog/fix-the-not-wrapped-in-act-warning#2-when-testing-custom-hooks
```jsx=
import React from 'react';
import {
act, renderHook
} from '@testing-library/react';
function useCount() {
const [count, setCount] = React.useState(0);
const increment = () => setCount((c) => c + 1);
const decrement = () => setCount((c) => c - 1);
return { count, increment, decrement };
}
describe('useCount', () => {
it('should increment and decrement updates the count', () => {
const { result } = renderHook(() => useCount());
expect(result.current.count).toBe(0);
act(() => result.current.increment());
expect(result.current.count).toBe(1);
act(() => result.current.decrement());
expect(result.current.count).toBe(0);
});
});
```
### Complex case: `useEffect` and `axios` mocking
- key insights come from https://cultivate.software/useeffect/
- Q: How do I test a useEffect hook?
- Indirectly!
- Always test from the user’s perspective.
- other good resources
- https://kentcdodds.com/blog/fix-the-not-wrapped-in-act-warning
- https://testing-library.com/docs/queries/about
```jsx=
import React from 'react';
import axios from 'axios'; // 因為在 test case 中直接 mock axios.get,所以需要在 test file 中 import
import {
render,
} from '@testing-library/react';
const useInitData = (url:string) => {
const [data, setData] = React.useState(null);
React.useEffect(() => {
axios.get(url)
.then((res) => setData(res.data));
}, [url]);
return [data];
};
// NOTE: 做一個 dummy component,只為了測試用
const DummyComponent = ({ url }: { url:string }) => {
const [data] = useInitData(url);
return (
<div>
hello:
{JSON.stringify(data, null, '')}
</div>
);
};
// jest.mock('axios');
// NOTE: 找到的文章都說要使用 `jest.mock` 。但發現這個 PoC 不需要。有關如何用最簡潔且正確的方式 mocking axios,需另外 survey。
describe('useInitData', () => {
it('should init the data', async () => {
const url = 'http://empty.domain';
axios.get = jest.fn().mockResolvedValue({ data: { foo: 'hahapoint' } });
const { findByText, queryByText } = render(<DummyComponent url={url} />);
const div = await findByText('hello', { exact: false });
expect(div).toBeDefined();
const div2 = queryByText('world', { exact: false });
expect(div2).toBeNull();
});
});
```