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