반응형

react-testing-library 테스트 환경 설정

library 설치

yarn add --dev @testing-library/react @testing-library/jest-dom @testing-library/dom

setupTests.js 파일 생성

  • 경로 : <프로젝트>/src/setupTests.js
import "@testing-library/react";
import "@testing-library/jest-dom/extend-expect";

package.json 설정 수정

{
    ...
    "jest": {
        ...
        "setupFilesAfterEnv": [
            "<rootDir>/src/setupTests.js"
        ],
        ...
        // alias로 경로를 지정해 사용할 경우 아래같은 설정이 없으면 오류 발생        
        "moduleNameMapper": {
            "@components/(.*)": "<rootDir>/src/components/$1"
        },
        ...
    }
    ...
}

테스트 코드 작성

// index.js

import React from 'react';

const Test = () => {
    return <div>Hi</div>;
};

export default Test;
// index.test.js

import React from 'react';
import { render } from '@testing-library/react';
import Test from '@components/Test';

describe('<Test />', () => {
    it('matches snapshot', () => {
        const utils = render(<Test />);
        expect(utils.container).toMatchSnapshot();
    });
});

테스트 실행

yarn test

테스트 코드 예제

Snapshot 테스트

// index.js

import React from 'react';

const Test = () => {
    return <div>Hello World</div>;
};

export default Test;
// index.test.js

import React from 'react';
import { render } from '@testing-library/react';
import Test from '@components/Test';

describe('<Test />', () => {
    // 최초 테스트 실행시 스냅샷 파일을 만들고
    // 두 번째 테스트 실행시부터 생성된 스냅샷과 비교하여 일치하는지 테스트
    // 스냅샷은 상대경로에 index.test.js.snap 파일로 저장됨
    it('matches snapshot', () => {
        const utils = render(<Test />);
        expect(utils.container).toMatchSnapshot();
    });
});

Element 존재 확인 테스트

// index.js

import React from 'react';

const Test = () => {
    return <div>Hello World</div>;
};

export default Test;
// index.test.js

import React from 'react';
import { queryByText } from '@testing-library/dom';
import { render } from '@testing-library/react';
import Test from '@components/Test';

describe('<Test />', () => {
    it('Element 존재 확인 테스트', () => {
        const utils = render(<Test />);
        const element = queryByText(utils.container, 'Hello World');
        // const element = queryByText(
        //     utils.container,
        //     (content, element) => element.type === 'div' && content === 'Hello World'
        // );

        expect(element).toBeTruthy();
    });
});

Style 존재 확인 테스트

// index.js

import React from 'react';

const Test = () => {
    return <div style={{ textAlign: 'center' }}>Hello World</div>;
};

export default Test;
// index.test.js

import React from 'react';
import { queryByText } from '@testing-library/dom';
import { render } from '@testing-library/react';
import Test from '@components/Test';

describe('<Test />', () => {
    it('Style 존재 확인 테스트', () => {
        const utils = render(<Test />);
        const element = queryByText(utils.container, 'Hello World');

        expect(element).toHaveStyle('text-align: center;');
        expect(element).not.toHaveStyle('display: none;');
    });
});

Event trigger 테스트

// index.js

import React, { useState } from 'react';

const Test = () => {
    const [message, setMessage] = useState('Hello World');
    const [count, setCount] = useState(0);

    return (
        <div>
            <input
                type={'text'}
                value={message}
                onChange={e => setMessage(e.target.value)}
            />
            <span>{count}</span>
            <button onClick={() => setCount(count + 1)}>+1</button>
        </div>
    );
};

export default Test;
// index.test.js

import React from 'react';
import { fireEvent, render } from '@testing-library/react';
import Test from '@components/Test';

describe('<Test />', () => {
    it('Event trigger 테스트', () => {
        const utils = render(<Test />);
        const input = utils.getByDisplayValue('Hello World');
        const count = utils.getByText('0');
        const addButton = utils.getByText('+1');

        fireEvent.click(addButton);
        fireEvent.click(addButton);
        fireEvent.change(input, { target: { value: 'Bye' } });

        expect(count).toHaveTextContent('2');
        expect(input).toHaveValue('Bye');
        expect(input).toHaveAttribute('value', 'Bye');
    });
});

Callback Mock 테스트

// index.js

import React from 'react';

const Test = ({ onClick }) => {
    return (
        <div>
            <button onClick={() => onClick('Hello')}>Click</button>
        </div>
    );
};

export default Test;
// index.test.js

import React from 'react';
import { fireEvent, render } from '@testing-library/react';
import Test from '@components/Test';

describe('<Test />', () => {
    it('Callback Mocking 테스트', () => {
        const onClick = jest.fn();
        const utils = render(<Test onClick={onClick} />);
        const button = utils.getByText('Click');

        fireEvent.click(button);

        expect(onClick).toBeCalledWith('Hello');
    });
});

Module Mock 테스트

  • student.js
const student = {
    name: 'John',
    greet: () => 'Hello'
};

export default student;
  • index.js
import React from 'react';
import student from './student';

const Test = () => {
    return (
        <div>
            <div>{student.name}</div>
            <div>{student.greet()}</div>
        </div>
    );
};

export default Test;
  • index.test.js
import React from 'react';
import { queryByText, render } from '@testing-library/react';
import Test from '@components/Test';

jest.mock('./student', () => {
    return {
        name: 'Tom',
        greet: () => 'Hi'
    };
});

describe('<Test />', () => {
    it('module mock', () => {
        const { container } = render(<Test />);

        const name = queryByText(container, 'Tom');
        const greet = queryByText(container, 'Hi');

        expect(name).toBeInTheDocument();
        expect(greet).toBeInTheDocument();
    });
});

Async 테스트

// index.js

import React, { useState } from 'react';

const Test = () => {
    const [isOn, setIsOn] = useState(false);

    const handleClick = () => {
        setTimeout(() => {
            setIsOn(!isOn);
        }, 2000);
    };

    return (
        <div>
            <span>{isOn ? 'ON' : 'OFF'}</span>
            <button onClick={handleClick}>Switch</button>
        </div>
    );
};

export default Test;
// index.test.js

import React from 'react';
import { render, fireEvent, waitForElement } from '@testing-library/react';
import Test from '@components/Test';

describe('<Test />', () => {
    it('Async 테스트', async () => {
        const utils = render(<Test />);
        const switchButton = utils.getByText('Switch');

        fireEvent.click(switchButton);

        const statusText = await waitForElement(() => utils.getByText('ON'));

        expect(statusText).toHaveTextContent('ON');
    });
});

Axios Mock 테스트

# library 설치

yarn add --dev axios-mock-adapter
// index.js

import React, { useEffect, useState } from 'react';
import axios from 'axios';

const Test = () => {
    const [user, setUser] = useState(undefined);

    useEffect(() => {
        axios
            .get('https://jsonplaceholder.typicode.com/users/1')
            .then(({ data }) => setUser(data));
    }, []);

    return (
        <div>
            {user ? (
                <div>
                    <div className={'name'}>name : {user.name}</div>
                    <div className={'email'}>email : {user.email}</div>
                </div>
            ) : (
                <div>Loading</div>
            )}
        </div>
    );
};

export default Test;
// index.test.js

import React from 'react';
import { render, waitForElement } from '@testing-library/react';
import Test from '@components/Test';
import MockAdapter from 'axios-mock-adapter';
import axios from 'axios';

describe('<Test />', () => {
    const mock = new MockAdapter(axios, { delayResponse: 200 }); // 200ms 가짜 딜레이 설정

    // API 요청에 대하여 응답 미리 정하기
    mock.onGet('https://jsonplaceholder.typicode.com/users/1').reply(200, {
        id: 1,
        name: 'Leanne Graham',
        email: 'Sincere@april.biz'
    });

    it('Axios Mock 테스트', async () => {
        const utils = render(<Test />);

        const name = await waitForElement(() =>
            utils.container.querySelector('.name')
        );
        const email = await waitForElement(() =>
            utils.container.querySelector('.email')
        );

        expect(name).toHaveTextContent('name : Leanne Graham');
        expect(email).toHaveTextContent('email : Sincere@april.biz');
    });
});

Custom Hooks 테스트

# library 설치

yarn add --dev @testing-library/react-hooks react-test-renderer@^16.9.0
// useUserApi.js

import { useEffect, useState } from 'react';
import axios from 'axios';

const useUserApi = ({ id }) => {
    const [user, setUser] = useState(undefined);

    useEffect(() => {
        if (id) {
            axios
                .get(`https://jsonplaceholder.typicode.com/users/${id}`)
                .then(({ data }) => setUser(data));
        }
    }, [id]);

    return {
        user
    };
};

export default useUserApi;
// index.test.js

import React from 'react';
import MockAdapter from 'axios-mock-adapter';
import axios from 'axios';
import { renderHook } from '@testing-library/react-hooks';
import useUserApi from '@components/Test/hooks/useUserApi';

describe('<Test />', () => {
    const mock = new MockAdapter(axios, { delayResponse: 100 });

    mock.onGet('https://jsonplaceholder.typicode.com/users/1')
        .reply(200, {
            id: 1,
            name: 'Leanne Graham',
            email: 'Sincere@april.biz'
        })
        .onGet('https://jsonplaceholder.typicode.com/users/2')
        .reply(200, {
            id: 2,
            name: 'Ervin Howell',
            email: 'Shanna@melissa.tv'
        });

    const setup = (defaultProps) => {
        return renderHook((props) => useUserApi(props), {
            initialProps: defaultProps
        });
    };

    it('Custom Hooks 테스트', async () => {
        const { result, rerender, waitForNextUpdate } = setup({ id: 1 });

        expect(result.current.user).toBeUndefined();

        await waitForNextUpdate();

        expect(result.current.user.name).toEqual('Leanne Graham');

        rerender({ id: 2 });

        await waitForNextUpdate();

        expect(result.current.user.name).toEqual('Ervin Howell');
    });
});

Custom Hooks 테스트 - with context

// index.js

import React, { createContext, useState } from 'react';
import useUserApi from '@components/Test/hooks/useUserApi';

export const TestContext = createContext();

const Test = () => {
    const [user, setUser] = useState(undefined);

    const context = {
        user,
        setUser
    };

    return (
        <TestContext.Provider value={context}>
            <Child />
        </TestContext.Provider>
    );
};

const Child = () => {
    const { user } = useUserApi({ id: 1 });

    return (
        <div>
            {user ? (
                <div>
                    <div className={'name'}>name : {user.name}</div>
                    <div className={'email'}>email : {user.email}</div>
                </div>
            ) : (
                <div>Loading</div>
            )}
        </div>
    );
};

export default Test;
// useUserApi.js

import { useContext, useEffect } from 'react';
import axios from 'axios';
import { TestContext } from '@components/Test';

const useUserApi = ({ id }) => {
    const { user, setUser } = useContext(TestContext);

    useEffect(() => {
        if (id) {
            axios
                .get(`https://jsonplaceholder.typicode.com/users/${id}`)
                .then(({ data }) => setUser(data));
        }
    }, [id]);

    return {
        user
    };
};

export default useUserApi;
// index.test.js

import React, { useState } from 'react';
import { renderHook } from '@testing-library/react-hooks';
import useUserApi from '@components/Test/hooks/useUserApi';
import { TestContext } from '@components/Test/index';
import MockAdapter from 'axios-mock-adapter';
import axios from 'axios';

describe('<Test />', () => {
    const mock = new MockAdapter(axios);

    mock.onGet('https://jsonplaceholder.typicode.com/users/1')
        .reply(200, {
            id: 1,
            name: 'Leanne Graham',
            email: 'Sincere@april.biz'
        })
        .onGet('https://jsonplaceholder.typicode.com/users/2')
        .reply(200, {
            id: 2,
            name: 'Ervin Howell',
            email: 'Shanna@melissa.tv'
        });

    const setup = (defaultProps) => {
        return renderHook((props) => useUserApi(props), {
            initialProps: defaultProps,
            wrapper: ({ children }) => {
                const [user, setUser] = useState(undefined);
                const context = {
                    user,
                    setUser
                };
                return (
                    <TestContext.Provider value={context}>
                        {children}
                    </TestContext.Provider>
                );
            }
        });
    };

    it('Custom Hooks 테스트 - with context', async () => {
        const { result, rerender, waitForNextUpdate } = setup({ id: 1 });

        expect(result.current.user).toBeUndefined();

        await waitForNextUpdate();

        expect(result.current.user.name).toEqual('Leanne Graham');

        rerender({ id: 2 });

        await waitForNextUpdate();

        expect(result.current.user.name).toEqual('Ervin Howell');
    });
});

이슈

“require.context is not a function” 오류 발생

  • 내부적으로 require.context()를 사용하는 경우 오류 발생
  • require.context()는 webpack 함수이므로 jest 실행시 올바르게 동작하지 않음
  • 해결 방법

참고

반응형

'Development > React' 카테고리의 다른 글

[React] react-redux  (0) 2020.12.30
[React] Library  (0) 2020.12.30
[React] Enzyme  (0) 2019.12.28
[React] Setting  (0) 2019.09.08
[React] ETC  (0) 2019.08.29

+ Recent posts