import React from 'react'; import { render, screen, waitFor, fireEvent } from '@testing-library/react'; import { BrowserRouter } from 'react-router-dom'; import Dashboard from '../pages/Dashboard'; import { useDevices } from '../hooks/useDevices'; import { useSensorData } from '../hooks/useSensorData'; import { getDevices, getLatestData } from '../services/api'; // Mock the hooks and API services jest.mock('../hooks/useDevices'); jest.mock('../hooks/useSensorData'); jest.mock('../services/api'); const mockUseDevices = useDevices as jest.MockedFunction; const mockUseSensorData = useSensorData as jest.MockedFunction; const mockGetDevices = getDevices as jest.MockedFunction; const mockGetLatestData = getLatestData as jest.MockedFunction; // Mock Recharts components jest.mock('recharts', () => ({ LineChart: ({ children, data }: any) => (
{children}
), Line: ({ dataKey }: any) => (
Line: {dataKey}
), XAxis: () =>
XAxis
, YAxis: () =>
YAxis
, CartesianGrid: () =>
CartesianGrid
, Tooltip: () =>
Tooltip
, Legend: () =>
Legend
, ResponsiveContainer: ({ children }: any) => (
{children}
), })); const renderWithRouter = (component: React.ReactElement) => { return render( {component} ); }; describe('API 오류 시나리오 테스트', () => { beforeEach(() => { jest.clearAllMocks(); }); describe('네트워크 오류 시나리오', () => { it('네트워크 연결 오류 시 적절한 오류 메시지를 표시한다', () => { mockUseDevices.mockReturnValue({ devices: [], loading: false, error: '네트워크 연결을 확인해주세요.', refetch: jest.fn() }); mockUseSensorData.mockReturnValue({ latestData: [], loading: false, error: null, refetch: jest.fn() }); renderWithRouter(); expect(screen.getByText('오류가 발생했습니다')).toBeInTheDocument(); expect(screen.getByText('네트워크 연결을 확인해주세요.')).toBeInTheDocument(); expect(screen.getByText('다시 시도')).toBeInTheDocument(); }); it('서버 연결 타임아웃 시 적절한 오류 메시지를 표시한다', () => { mockUseDevices.mockReturnValue({ devices: [], loading: false, error: '서버 응답 시간이 초과되었습니다.', refetch: jest.fn() }); mockUseSensorData.mockReturnValue({ latestData: [], loading: false, error: null, refetch: jest.fn() }); renderWithRouter(); expect(screen.getByText('오류가 발생했습니다')).toBeInTheDocument(); expect(screen.getByText('서버 응답 시간이 초과되었습니다.')).toBeInTheDocument(); }); it('센서 데이터 로딩 실패 시 적절한 오류 메시지를 표시한다', () => { mockUseDevices.mockReturnValue({ devices: [{ id: 1, device_id: 'sensor-001', name: '테스트 센서', status: 'active', last_seen: '2025-08-01T10:00:00Z' }], loading: false, error: null, refetch: jest.fn() }); mockUseSensorData.mockReturnValue({ latestData: [], loading: false, error: '센서 데이터를 불러올 수 없습니다.', refetch: jest.fn() }); renderWithRouter(); expect(screen.getByText('오류가 발생했습니다')).toBeInTheDocument(); expect(screen.getByText('센서 데이터를 불러올 수 없습니다.')).toBeInTheDocument(); }); }); describe('서버 오류 시나리오', () => { it('500 서버 오류 시 적절한 오류 메시지를 표시한다', () => { mockUseDevices.mockReturnValue({ devices: [], loading: false, error: '서버 내부 오류가 발생했습니다. (500)', refetch: jest.fn() }); mockUseSensorData.mockReturnValue({ latestData: [], loading: false, error: null, refetch: jest.fn() }); renderWithRouter(); expect(screen.getByText('오류가 발생했습니다')).toBeInTheDocument(); expect(screen.getByText('서버 내부 오류가 발생했습니다. (500)')).toBeInTheDocument(); }); it('404 리소스 없음 오류 시 적절한 오류 메시지를 표시한다', () => { mockUseDevices.mockReturnValue({ devices: [], loading: false, error: '요청한 리소스를 찾을 수 없습니다. (404)', refetch: jest.fn() }); mockUseSensorData.mockReturnValue({ latestData: [], loading: false, error: null, refetch: jest.fn() }); renderWithRouter(); expect(screen.getByText('오류가 발생했습니다')).toBeInTheDocument(); expect(screen.getByText('요청한 리소스를 찾을 수 없습니다. (404)')).toBeInTheDocument(); }); it('403 권한 없음 오류 시 적절한 오류 메시지를 표시한다', () => { mockUseDevices.mockReturnValue({ devices: [], loading: false, error: '접근 권한이 없습니다. (403)', refetch: jest.fn() }); mockUseSensorData.mockReturnValue({ latestData: [], loading: false, error: null, refetch: jest.fn() }); renderWithRouter(); expect(screen.getByText('오류가 발생했습니다')).toBeInTheDocument(); expect(screen.getByText('접근 권한이 없습니다. (403)')).toBeInTheDocument(); }); }); describe('데이터 무결성 오류 시나리오', () => { it('잘못된 JSON 응답 시 적절한 오류 메시지를 표시한다', () => { mockUseDevices.mockReturnValue({ devices: [], loading: false, error: '서버에서 잘못된 데이터 형식을 반환했습니다.', refetch: jest.fn() }); mockUseSensorData.mockReturnValue({ latestData: [], loading: false, error: null, refetch: jest.fn() }); renderWithRouter(); expect(screen.getByText('오류가 발생했습니다')).toBeInTheDocument(); expect(screen.getByText('서버에서 잘못된 데이터 형식을 반환했습니다.')).toBeInTheDocument(); }); it('필수 필드 누락 시 적절한 오류 메시지를 표시한다', () => { mockUseDevices.mockReturnValue({ devices: [], loading: false, error: '응답 데이터에 필수 필드가 누락되었습니다.', refetch: jest.fn() }); mockUseSensorData.mockReturnValue({ latestData: [], loading: false, error: null, refetch: jest.fn() }); renderWithRouter(); expect(screen.getByText('오류가 발생했습니다')).toBeInTheDocument(); expect(screen.getByText('응답 데이터에 필수 필드가 누락되었습니다.')).toBeInTheDocument(); }); }); describe('재시도 기능 테스트', () => { it('다시 시도 버튼 클릭 시 refetch 함수가 호출된다', async () => { const mockRefetchDevices = jest.fn(); const mockRefetchData = jest.fn(); mockUseDevices.mockReturnValue({ devices: [], loading: false, error: '디바이스 목록을 불러올 수 없습니다.', refetch: mockRefetchDevices }); mockUseSensorData.mockReturnValue({ latestData: [], loading: false, error: '센서 데이터를 불러올 수 없습니다.', refetch: mockRefetchData }); renderWithRouter(); const retryButton = screen.getByText('다시 시도'); fireEvent.click(retryButton); expect(mockRefetchDevices).toHaveBeenCalled(); expect(mockRefetchData).toHaveBeenCalled(); }); it('재시도 후 로딩 상태가 표시된다', async () => { const mockRefetchDevices = jest.fn(); const mockRefetchData = jest.fn(); mockUseDevices.mockReturnValue({ devices: [], loading: false, error: '디바이스 목록을 불러올 수 없습니다.', refetch: mockRefetchDevices }); mockUseSensorData.mockReturnValue({ latestData: [], loading: false, error: '센서 데이터를 불러올 수 없습니다.', refetch: mockRefetchData }); renderWithRouter(); const retryButton = screen.getByText('다시 시도'); fireEvent.click(retryButton); // 재시도 후 로딩 상태로 변경 mockUseDevices.mockReturnValue({ devices: [], loading: true, error: null, refetch: mockRefetchDevices }); mockUseSensorData.mockReturnValue({ latestData: [], loading: true, error: null, refetch: mockRefetchData }); // 컴포넌트를 다시 렌더링하여 로딩 상태 확인 const { rerender } = renderWithRouter(); // 로딩 상태로 변경 mockUseDevices.mockReturnValue({ devices: [], loading: true, error: null, refetch: mockRefetchDevices }); mockUseSensorData.mockReturnValue({ latestData: [], loading: true, error: null, refetch: mockRefetchData }); rerender( ); expect(screen.getByText('데이터를 불러오는 중...')).toBeInTheDocument(); }); }); describe('부분적 오류 시나리오', () => { it('디바이스 목록만 오류가 발생한 경우 센서 데이터는 정상 표시된다', () => { mockUseDevices.mockReturnValue({ devices: [], loading: false, error: '디바이스 목록을 불러올 수 없습니다.', refetch: jest.fn() }); mockUseSensorData.mockReturnValue({ latestData: [ { device_id: 'sensor-001', temperature: 25.5, humidity: 60.0, pm10: 15.2, pm25: 8.1, pressure: 1013.25, illumination: 500, tvoc: 120, co2: 450, o2: 20.9, co: 0.5, recorded_time: '2025-08-01T10:00:00Z' } ], loading: false, error: null, refetch: jest.fn() }); renderWithRouter(); // 오류 메시지가 표시되지만 센서 데이터도 함께 표시됨 expect(screen.getByText('오류가 발생했습니다')).toBeInTheDocument(); expect(screen.getByText('25.5°C')).toBeInTheDocument(); expect(screen.getByText('60.0%')).toBeInTheDocument(); }); it('센서 데이터만 오류가 발생한 경우 디바이스 목록은 정상 표시된다', () => { mockUseDevices.mockReturnValue({ devices: [ { id: 1, device_id: 'sensor-001', name: '테스트 센서', status: 'active', last_seen: '2025-08-01T10:00:00Z' } ], loading: false, error: null, refetch: jest.fn() }); mockUseSensorData.mockReturnValue({ latestData: [], loading: false, error: '센서 데이터를 불러올 수 없습니다.', refetch: jest.fn() }); renderWithRouter(); // 오류 메시지가 표시되지만 디바이스 정보도 함께 표시됨 expect(screen.getByText('오류가 발생했습니다')).toBeInTheDocument(); expect(screen.getByText('센서 데이터를 불러올 수 없습니다.')).toBeInTheDocument(); expect(screen.getByText('디바이스 상태')).toBeInTheDocument(); }); }); describe('오류 복구 시나리오', () => { it('오류 후 정상 데이터가 로드되면 오류 메시지가 사라진다', async () => { // 초기 오류 상태 mockUseDevices.mockReturnValue({ devices: [], loading: false, error: '디바이스 목록을 불러올 수 없습니다.', refetch: jest.fn() }); mockUseSensorData.mockReturnValue({ latestData: [], loading: false, error: null, refetch: jest.fn() }); const { rerender } = renderWithRouter(); expect(screen.getByText('오류가 발생했습니다')).toBeInTheDocument(); // 정상 데이터로 복구 mockUseDevices.mockReturnValue({ devices: [ { id: 1, device_id: 'sensor-001', name: '테스트 센서', status: 'active', last_seen: '2025-08-01T10:00:00Z' } ], loading: false, error: null, refetch: jest.fn() }); mockUseSensorData.mockReturnValue({ latestData: [ { device_id: 'sensor-001', temperature: 25.5, humidity: 60.0, pm10: 15.2, pm25: 8.1, pressure: 1013.25, illumination: 500, tvoc: 120, co2: 450, o2: 20.9, co: 0.5, recorded_time: '2025-08-01T10:00:00Z' } ], loading: false, error: null, refetch: jest.fn() }); rerender( ); // 오류 메시지가 사라지고 정상 데이터가 표시됨 await waitFor(() => { expect(screen.queryByText('오류가 발생했습니다')).not.toBeInTheDocument(); expect(screen.getByText('25.5°C')).toBeInTheDocument(); expect(screen.getByText('60.0%')).toBeInTheDocument(); }); }); }); });