Skip to content

Commit

Permalink
Merge pull request #143 from kakao-tech-campus-2nd-step3/test/#137
Browse files Browse the repository at this point in the history
Test/#137 ๋ฉ”์ธํŽ˜์ด์ง€, ๋กœ๊ทธ์ธ, ํšŒ์›๊ฐ€์ž… ํ…Œ์ŠคํŠธ์ฝ”๋“œ ๊ตฌํ˜„
  • Loading branch information
YIMSEBIN authored Nov 12, 2024
2 parents 3ebec96 + 4fdba06 commit da0db54
Show file tree
Hide file tree
Showing 12 changed files with 1,028 additions and 48 deletions.
718 changes: 711 additions & 7 deletions package-lock.json

Large diffs are not rendered by default.

9 changes: 8 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint --cache 'src/**/*.{ts,tsx}'",
"test": "vitest",
"tsc": "tsc --noEmit --project ./tsconfig.json",
"format": "prettier --write --cache 'src/**/*.{ts,tsx}'",
"lint-staged": "lint-staged",
Expand Down Expand Up @@ -58,6 +59,9 @@
"@storybook/react": "^8.3.0",
"@storybook/react-vite": "^8.3.0",
"@storybook/test": "^8.3.0",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.0.1",
"@testing-library/user-event": "^14.5.2",
"@types/loadable__component": "^5.13.9",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
Expand All @@ -72,6 +76,7 @@
"eslint-plugin-storybook": "^0.8.0",
"globals": "^15.9.0",
"husky": "^9.1.6",
"jsdom": "^25.0.1",
"lint-staged": "^15.2.10",
"msw": "^2.4.6",
"msw-storybook-addon": "^2.0.3",
Expand All @@ -82,7 +87,9 @@
"typescript": "5.5",
"typescript-eslint": "^8.0.1",
"vite": "^5.4.1",
"vite-plugin-svgr": "^4.2.0"
"vite-plugin-svgr": "^4.2.0",
"vitest": "^2.1.4",
"vitest-dom": "^0.1.1"
},
"eslintConfig": {
"extends": [
Expand Down
15 changes: 15 additions & 0 deletions src/__test__/setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import '@testing-library/jest-dom';

Object.defineProperty(window, 'matchMedia', {
writable: true,
value: (query: string) => ({
matches: false,
media: query,
onchange: null,
addListener: () => {},
removeListener: () => {},
addEventListener: () => {},
removeEventListener: () => {},
dispatchEvent: () => false,
}),
});
12 changes: 12 additions & 0 deletions src/__test__/test-utils.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { render } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
import AppProviders from '@/components/providers/index.provider';
import { ReactElement } from 'react';

export function renderWithProviders(ui: ReactElement) {
return render(
<MemoryRouter>
<AppProviders>{ui}</AppProviders>
</MemoryRouter>,
);
}
156 changes: 119 additions & 37 deletions src/apis/home/mocks/recruitmentsMockHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,46 +2,128 @@ import { getDynamicAPIPath } from '@/apis/apiPath';
import { http, HttpResponse } from 'msw';

export const recruitmentsMockHandler = [
http.get(getDynamicAPIPath.getRecruitments('all'), () => HttpResponse.json(RECRUITMENTS_RESPONSE_DATA)),
http.get(getDynamicAPIPath.getRecruitments(':filter'), async ({ params }) => {
const filter = params.filter as keyof typeof RECRUITMENTS_RESPONSE_DATA;
const data = RECRUITMENTS_RESPONSE_DATA[filter] || RECRUITMENTS_RESPONSE_DATA.all;
return HttpResponse.json(data);
}),
];

const RECRUITMENTS_RESPONSE_DATA = {
content: [
{
recruitmentId: 1,
imageUrl:
'https://img.freepik.com/free-photo/low-angle-view-of-skyscrapers_1359-1105.jpg?size=626&ext=jpg&ga=GA1.1.1297763733.1727740800&semt=ais_hybrid',
koreanTitle: '์ œ๋ชฉ1',
vietnameseTitle: 'Tiรชu ฤ‘แป1',
companyName: '์นด์นด์˜ค',
salary: 3000,
workHours: '3๊ฐœ์›”',
area: '๋Œ€์ „๊ด‘์—ญ์‹œ ์œ ์„ฑ๊ตฌ',
all: {
content: [
{
recruitmentId: 1,
imageUrl:
'https://img.freepik.com/free-photo/low-angle-view-of-skyscrapers_1359-1105.jpg?size=626&ext=jpg&ga=GA1.1.1297763733.1727740800&semt=ais_hybrid',
koreanTitle: '์ œ๋ชฉ1',
vietnameseTitle: 'Tiรชu ฤ‘แป1',
companyName: '์นด์นด์˜ค',
salary: 3000,
workHours: '3๊ฐœ์›”',
area: '๋Œ€์ „๊ด‘์—ญ์‹œ ์œ ์„ฑ๊ตฌ',
},
{
recruitmentId: 2,
imageUrl:
'https://img.freepik.com/free-photo/low-angle-view-of-skyscrapers_1359-1105.jpg?size=626&ext=jpg&ga=GA1.1.1297763733.1727740800&semt=ais_hybrid',
koreanTitle: '์ œ๋ชฉ2',
vietnameseTitle: 'Tiรชu ฤ‘แป2',
companyName: '๋„ค์ด๋ฒ„',
salary: 2000,
workHours: '6๊ฐœ์›”',
area: '์„œ์šธํŠน๋ณ„์‹œ ๊ฐ•๋‚จ๊ตฌ',
},
{
recruitmentId: 3,
imageUrl:
'https://img.freepik.com/free-photo/low-angle-view-of-skyscrapers_1359-1105.jpg?size=626&ext=jpg&ga=GA1.1.1297763733.1727740800&semt=ais_hybrid',
koreanTitle: '์ œ๋ชฉ3',
vietnameseTitle: 'Tiรชu ฤ‘แป3',
companyName: '๋ผ์ธ',
salary: 2500,
workHours: '12๊ฐœ์›”',
area: '๋ถ€์‚ฐ๊ด‘์—ญ์‹œ ํ•ด์šด๋Œ€๊ตฌ',
},
],
pageable: {
totalPage: 1,
},
{
recruitmentId: 2,
imageUrl:
'https://img.freepik.com/free-photo/low-angle-view-of-skyscrapers_1359-1105.jpg?size=626&ext=jpg&ga=GA1.1.1297763733.1727740800&semt=ais_hybrid',
koreanTitle: '์ œ๋ชฉ2',
vietnameseTitle: 'Tiรชu ฤ‘แป2',
companyName: '๋„ค์ด๋ฒ„',
salary: 3500,
workHours: '6๊ฐœ์›”',
area: '์„œ์šธํŠน๋ณ„์‹œ ๊ฐ•๋‚จ๊ตฌ',
},
{
recruitmentId: 3,
imageUrl:
'https://img.freepik.com/free-photo/low-angle-view-of-skyscrapers_1359-1105.jpg?size=626&ext=jpg&ga=GA1.1.1297763733.1727740800&semt=ais_hybrid',
koreanTitle: '์ œ๋ชฉ3',
vietnameseTitle: 'Tiรชu ฤ‘แป3',
companyName: '๋ผ์ธ',
salary: 4000,
workHours: '12๊ฐœ์›”',
area: '๋ถ€์‚ฐ๊ด‘์—ญ์‹œ ํ•ด์šด๋Œ€๊ตฌ',
},
],
pageable: {
totalPage: 1,
},
salary: {
content: [
{
recruitmentId: 1,
imageUrl:
'https://img.freepik.com/free-photo/low-angle-view-of-skyscrapers_1359-1105.jpg?size=626&ext=jpg&ga=GA1.1.1297763733.1727740800&semt=ais_hybrid',
koreanTitle: '์ œ๋ชฉ1',
vietnameseTitle: 'Tiรชu ฤ‘แป1',
companyName: '์นด์นด์˜ค',
salary: 3000,
workHours: '3๊ฐœ์›”',
area: '๋Œ€์ „๊ด‘์—ญ์‹œ ์œ ์„ฑ๊ตฌ',
},
{
recruitmentId: 3,
imageUrl:
'https://img.freepik.com/free-photo/low-angle-view-of-skyscrapers_1359-1105.jpg?size=626&ext=jpg&ga=GA1.1.1297763733.1727740800&semt=ais_hybrid',
koreanTitle: '์ œ๋ชฉ3',
vietnameseTitle: 'Tiรชu ฤ‘แป3',
companyName: '๋ผ์ธ',
salary: 2500,
workHours: '12๊ฐœ์›”',
area: '๋ถ€์‚ฐ๊ด‘์—ญ์‹œ ํ•ด์šด๋Œ€๊ตฌ',
},
{
recruitmentId: 2,
imageUrl:
'https://img.freepik.com/free-photo/low-angle-view-of-skyscrapers_1359-1105.jpg?size=626&ext=jpg&ga=GA1.1.1297763733.1727740800&semt=ais_hybrid',
koreanTitle: '์ œ๋ชฉ2',
vietnameseTitle: 'Tiรชu ฤ‘แป2',
companyName: '๋„ค์ด๋ฒ„',
salary: 2000,
workHours: '6๊ฐœ์›”',
area: '์„œ์šธํŠน๋ณ„์‹œ ๊ฐ•๋‚จ๊ตฌ',
},
],
pageable: { totalPage: 1 },
},
latestRegistration: {
content: [
{
recruitmentId: 2,
imageUrl:
'https://img.freepik.com/free-photo/low-angle-view-of-skyscrapers_1359-1105.jpg?size=626&ext=jpg&ga=GA1.1.1297763733.1727740800&semt=ais_hybrid',
koreanTitle: '์ œ๋ชฉ2',
vietnameseTitle: 'Tiรชu ฤ‘แป2',
companyName: '๋„ค์ด๋ฒ„',
salary: 2000,
workHours: '6๊ฐœ์›”',
area: '์„œ์šธํŠน๋ณ„์‹œ ๊ฐ•๋‚จ๊ตฌ',
},
{
recruitmentId: 1,
imageUrl:
'https://img.freepik.com/free-photo/low-angle-view-of-skyscrapers_1359-1105.jpg?size=626&ext=jpg&ga=GA1.1.1297763733.1727740800&semt=ais_hybrid',
koreanTitle: '์ œ๋ชฉ1',
vietnameseTitle: 'Tiรชu ฤ‘แป1',
companyName: '์นด์นด์˜ค',
salary: 3000,
workHours: '3๊ฐœ์›”',
area: '๋Œ€์ „๊ด‘์—ญ์‹œ ์œ ์„ฑ๊ตฌ',
},
{
recruitmentId: 3,
imageUrl:
'https://img.freepik.com/free-photo/low-angle-view-of-skyscrapers_1359-1105.jpg?size=626&ext=jpg&ga=GA1.1.1297763733.1727740800&semt=ais_hybrid',
koreanTitle: '์ œ๋ชฉ3',
vietnameseTitle: 'Tiรชu ฤ‘แป3',
companyName: '๋ผ์ธ',
salary: 2500,
workHours: '12๊ฐœ์›”',
area: '๋ถ€์‚ฐ๊ด‘์—ญ์‹œ ํ•ด์šด๋Œ€๊ตฌ',
},
],
pageable: { totalPage: 1 },
},
};
2 changes: 1 addition & 1 deletion src/features/auth/SignIn/components/SignInButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export function SignInButton() {
return (
<Button design="outlined" onClick={redirectToGoogleLogin}>
<Flex alignItems="center" gap={FLEX_GAP_CONFIG}>
<Icon.Social.Google />
<Icon.Social.Google data-testid="google-icon" />
<Typo size="14px" color="gray" element="span" style={BUTTON_STYLE}>
Sign up with Google
</Typo>
Expand Down
39 changes: 39 additions & 0 deletions src/features/auth/__test__/SignInButton.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { describe, it, expect, vi, beforeEach, MockedFunction } from 'vitest';
import { screen, fireEvent } from '@testing-library/react';
import { SignInButton } from '../SignIn/components/SignInButton';
import { useGoogleOAuth } from '../SignIn/hooks/useGoogleOAuth';
import { renderWithProviders } from '@/__test__/test-utils';

vi.mock('../SignIn/hooks/useGoogleOAuth');

describe('SignInButton ์ปดํฌ๋„ŒํŠธ', () => {
const mockRedirectToGoogleLogin = vi.fn();
const mockedUseGoogleOAuth = useGoogleOAuth as MockedFunction<typeof useGoogleOAuth>;

beforeEach(() => {
vi.clearAllMocks();
mockedUseGoogleOAuth.mockReturnValue({
isLoading: false,
redirectToGoogleLogin: mockRedirectToGoogleLogin,
});
});

it('๊ตฌ๊ธ€ ๋กœ๊ทธ์ธ ๋ฒ„ํŠผ์ด ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๋ Œ๋”๋ง๋œ๋‹ค', () => {
renderWithProviders(<SignInButton />);

const buttonElement = screen.getByRole('button', { name: /Sign up with Google/i });
expect(buttonElement).toBeInTheDocument();

const googleIcon = screen.getByTestId('google-icon');
expect(googleIcon).toBeInTheDocument();
});

it('๋ฒ„ํŠผ ํด๋ฆญ ์‹œ redirectToGoogleLogin ํ•จ์ˆ˜๊ฐ€ ํ˜ธ์ถœ๋œ๋‹ค', () => {
renderWithProviders(<SignInButton />);

const buttonElement = screen.getByRole('button', { name: /Sign up with Google/i });
fireEvent.click(buttonElement);

expect(mockRedirectToGoogleLogin).toHaveBeenCalledTimes(1);
});
});
43 changes: 43 additions & 0 deletions src/features/home/__test__/ConditionRenderer.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { screen } from '@testing-library/react';
import ConditionalRenderer from '../components/ConditionalRenderer';
import { userLocalStorage } from '@/utils/storage';
import { UserData } from '@/types';
import { renderWithProviders } from '@/__test__/test-utils';

const mockEmployeeUser = { type: 'employee', profileImage: 'userProfileImage', name: 'userName' } as UserData;
const mockEmployerUser = { type: 'employer', profileImage: 'employerProfileImage', name: 'employerName' } as UserData;

describe('ConditionalRenderer ์ปดํฌ๋„ŒํŠธ', () => {
beforeEach(() => {
vi.clearAllMocks();
});

it('userType์ด "employee"์ผ ๋•Œ Worker ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋ Œ๋”๋งํ•œ๋‹ค', async () => {
vi.spyOn(userLocalStorage, 'getUser').mockReturnValue(mockEmployeeUser);

renderWithProviders(<ConditionalRenderer />);

expect(screen.queryByText('home.greeting.heading')).not.toBeInTheDocument();
});

it('userType์ด "employer"์ผ ๋•Œ Employer ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋ Œ๋”๋งํ•œ๋‹ค', () => {
vi.spyOn(userLocalStorage, 'getUser').mockReturnValue(mockEmployerUser);

renderWithProviders(<ConditionalRenderer />);

const headingText = screen.getByText('home.greeting.heading');
const buttonText = screen.getByText('home.greeting.button');

expect(headingText).toBeInTheDocument();
expect(buttonText).toBeInTheDocument();
});

it('userType์ด undefined์ผ ๋•Œ Worker ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋ Œ๋”๋งํ•œ๋‹ค', () => {
vi.spyOn(userLocalStorage, 'getUser').mockReturnValue(undefined);

renderWithProviders(<ConditionalRenderer />);

expect(screen.queryByText('home.greeting.heading')).not.toBeInTheDocument();
});
});
65 changes: 65 additions & 0 deletions src/features/home/__test__/useRecruitmentData.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { describe, it, expect, vi, beforeEach, beforeAll, afterEach, afterAll } from 'vitest';
import { renderHook, act, waitFor } from '@testing-library/react';
import { useRecruitmentData } from '../hooks/useRecruitmentData';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { server } from '@mocks/server';

const queryClient = new QueryClient();

describe('useRecruitmentData ํ›… ํ…Œ์ŠคํŠธ', () => {
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());

beforeEach(() => {
vi.clearAllMocks();
});

const renderUseRecruitmentDataHook = (initialFilter = 'all') => {
return renderHook(() => useRecruitmentData(initialFilter), {
wrapper: ({ children }) => <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>,
});
};

it('๊ธฐ๋ณธ ํ•„ํ„ฐ๋กœ ๋ชจ๋“  ๋ชจ์ง‘ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜จ๋‹ค', async () => {
const { result } = renderUseRecruitmentDataHook('all');

await waitFor(() => {
expect(result.current.recruitmentList.content).toHaveLength(3);
expect(result.current.recruitmentList.content[0].companyName).toBe('์นด์นด์˜ค');
});
});

it('ํ•„ํ„ฐ๋ฅผ ๋ณ€๊ฒฝํ–ˆ์„ ๋•Œ ๋ชจ์ง‘ ๋ฐ์ดํ„ฐ๊ฐ€ ์—…๋ฐ์ดํŠธ ๋œ๋‹ค', async () => {
const { result } = renderUseRecruitmentDataHook('all');

await waitFor(() => {
expect(result.current.recruitmentList.content).toHaveLength(3);
});

act(() => {
result.current.handleFilterChange('salary');
});

await waitFor(() => {
expect(result.current.recruitmentList.content).toHaveLength(3);
expect(result.current.recruitmentList.content[0].companyName).toBe('์นด์นด์˜ค');
});
});

it('ํŽ˜์ด์ง€๋ฅผ ๋ณ€๊ฒฝํ–ˆ์„ ๋•Œ ํŽ˜์ด์ง€ ๋ฒˆํ˜ธ๊ฐ€ ์—…๋ฐ์ดํŠธ ๋œ๋‹ค', async () => {
const { result } = renderUseRecruitmentDataHook('all');

await waitFor(() => {
expect(result.current.page).toBe(0);
});

act(() => {
result.current.handlePageChange(1);
});

await waitFor(() => {
expect(result.current.page).toBe(1);
});
});
});
4 changes: 4 additions & 0 deletions src/mocks/server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { setupServer } from 'msw/node';
import { handlers } from './handlers';

export const server = setupServer(...handlers);
Loading

0 comments on commit da0db54

Please sign in to comment.