# SuperPort Real API 자동화 테스트 계획서 ## 📋 개요 본 문서는 SuperPort 애플리케이션의 모든 화면과 기능에 대한 Real API 기반 자동화 테스트 계획을 정의합니다. ### 핵심 원칙 - ✅ **Real API 사용**: Mock 데이터 사용 금지, 실제 서버와 통신 - ✅ **자동 오류 복구**: 에러 발생시 자동 진단 및 수정 - ✅ **전체 기능 커버리지**: 모든 화면의 모든 기능 테스트 - ✅ **재사용 가능한 프레임워크**: 확장 가능한 테스트 구조 ## 🏗️ 테스트 프레임워크 아키텍처 ### 1. 기본 구조 ``` test/ ├── integration/ │ ├── automated/ │ │ ├── framework/ │ │ │ ├── screen_test_framework.dart # 핵심 프레임워크 │ │ │ ├── api_error_diagnostics.dart # 에러 진단 시스템 │ │ │ ├── auto_fixer.dart # 자동 수정 시스템 │ │ │ └── test_data_generator.dart # 데이터 생성기 │ │ ├── screens/ │ │ │ ├── login_automated_test.dart │ │ │ ├── dashboard_automated_test.dart │ │ │ ├── equipment_automated_test.dart │ │ │ ├── company_automated_test.dart │ │ │ ├── user_automated_test.dart │ │ │ ├── warehouse_automated_test.dart │ │ │ └── license_automated_test.dart │ │ └── test_runner.dart # 통합 실행기 ``` ### 2. 핵심 컴포넌트 #### ScreenTestFramework ```dart class ScreenTestFramework { // 화면 분석 및 테스트 가능한 액션 추출 static Future> analyzeScreen(Widget screen); // 필수 필드 자동 감지 및 입력 static Future> generateRequiredData(String endpoint); // API 호출 및 응답 검증 static Future executeApiCall(ApiRequest request); // 에러 처리 및 재시도 static Future executeWithRetry( Future Function() action, {int maxRetries = 3} ); } ``` #### ApiErrorDiagnostics ```dart class ApiErrorDiagnostics { static ErrorDiagnosis diagnose(DioException error) { // 에러 타입 분류 // - 400: 검증 오류 (필수 필드, 타입 불일치) // - 401: 인증 오류 // - 403: 권한 오류 // - 404: 리소스 없음 // - 409: 중복 오류 // - 422: 비즈니스 로직 오류 // - 500: 서버 오류 } } ``` #### AutoFixer ```dart class AutoFixer { static Future> fix( ErrorDiagnosis diagnosis, Map originalData, ) { // 에러 타입별 자동 수정 로직 // - 필수 필드 누락: 기본값 추가 // - 타입 불일치: 타입 변환 // - 참조 무결성: 관련 데이터 생성 // - 중복 오류: 고유값 재생성 } } ``` ## 📱 화면별 테스트 계획 ### 1. 🔐 로그인 화면 (Login Screen) #### 테스트 시나리오 1. **정상 로그인** - 유효한 자격증명으로 로그인 - 액세스 토큰 저장 확인 - 대시보드 이동 확인 2. **로그인 실패 처리** - 잘못된 이메일/비밀번호 - 비활성 계정 - 네트워크 오류 3. **토큰 관리** - 토큰 만료시 자동 갱신 - 로그아웃 후 토큰 삭제 #### 자동화 테스트 코드 ```dart test('로그인 전체 프로세스 자동화 테스트', () async { // 1. 테스트 데이터 준비 final testCredentials = { 'email': 'admin@superport.kr', 'password': 'admin123!', }; // 2. 로그인 시도 try { final response = await authService.login(testCredentials); expect(response.accessToken, isNotEmpty); } catch (error) { // 3. 에러 진단 final diagnosis = ApiErrorDiagnostics.diagnose(error); // 4. 자동 수정 (예: 비밀번호 재설정 필요) if (diagnosis.errorType == ErrorType.invalidCredentials) { // 관리자 계정으로 비밀번호 재설정 API 호출 await resetTestUserPassword(); // 5. 재시도 final response = await authService.login(testCredentials); expect(response.accessToken, isNotEmpty); } } }); ``` ### 2. 📊 대시보드 화면 (Dashboard Screen) #### 테스트 시나리오 1. **통계 데이터 조회** - 전체 통계 로딩 - 각 카드별 데이터 검증 - 실시간 업데이트 확인 2. **최근 활동 표시** - 최근 활동 목록 조회 - 페이지네이션 - 필터링 3. **차트 및 그래프** - 장비 상태 분포 - 월별 입출고 현황 - 라이선스 만료 예정 #### 자동화 테스트 코드 ```dart test('대시보드 데이터 로딩 자동화 테스트', () async { // 1. 로그인 await RealApiTestHelper.loginAndGetToken(); // 2. 대시보드 데이터 조회 try { final stats = await dashboardService.getOverviewStats(); expect(stats.totalEquipment, greaterThanOrEqualTo(0)); expect(stats.totalUsers, greaterThanOrEqualTo(0)); } catch (error) { // 3. 에러 진단 final diagnosis = ApiErrorDiagnostics.diagnose(error); // 4. 자동 수정 (예: 초기 데이터 생성) if (diagnosis.errorType == ErrorType.noData) { await createInitialTestData(); // 5. 재시도 final stats = await dashboardService.getOverviewStats(); expect(stats, isNotNull); } } }); ``` ### 3. 🛠 장비 관리 화면 (Equipment Management) #### 테스트 시나리오 ##### 3.1 장비 입고 ```dart test('장비 입고 전체 프로세스 자동화 테스트', () async { // 1. 사전 데이터 준비 final company = await prepareTestCompany(); final warehouse = await prepareTestWarehouse(company.id); // 2. 장비 입고 데이터 생성 final equipmentData = TestDataGenerator.generateEquipmentInData( companyId: company.id, warehouseId: warehouse.id, ); // 3. 입고 실행 try { final response = await equipmentService.createEquipment(equipmentData); expect(response.id, isNotNull); expect(response.status, equals('I')); } catch (error) { // 4. 에러 진단 및 수정 final diagnosis = ApiErrorDiagnostics.diagnose(error); final fixedData = await AutoFixer.fix(diagnosis, equipmentData); // 5. 재시도 final response = await equipmentService.createEquipment(fixedData); expect(response.id, isNotNull); } }); ``` ##### 3.2 장비 출고 ```dart test('장비 출고 전체 프로세스 자동화 테스트', () async { // 1. 입고된 장비 준비 final equipment = await prepareInStockEquipment(); // 2. 출고 데이터 생성 final outData = { 'equipment_id': equipment.id, 'transaction_type': 'O', 'quantity': 1, 'out_company_id': await getOrCreateTestCompany().id, 'out_date': DateTime.now().toIso8601String(), }; // 3. 출고 실행 try { final response = await equipmentService.createEquipmentHistory(outData); expect(response.transactionType, equals('O')); } catch (error) { // 4. 에러 처리 final diagnosis = ApiErrorDiagnostics.diagnose(error); if (diagnosis.errorType == ErrorType.insufficientStock) { // 재고 부족시 입고 먼저 실행 await createAdditionalStock(equipment.id); // 재시도 final response = await equipmentService.createEquipmentHistory(outData); expect(response, isNotNull); } } }); ``` ##### 3.3 장비 목록 조회 ```dart test('장비 목록 필터링 및 검색 테스트', () async { // 1. 다양한 상태의 테스트 장비 생성 await createEquipmentsWithVariousStatuses(); // 2. 필터별 조회 final filters = [ {'status': 'I'}, // 입고 {'status': 'O'}, // 출고 {'status': 'R'}, // 대여 {'company_id': 1}, // 회사별 {'search': '노트북'}, // 검색어 ]; for (final filter in filters) { try { final equipments = await equipmentService.getEquipments(filter); expect(equipments, isNotEmpty); // 필터 조건 검증 if (filter['status'] != null) { expect(equipments.every((e) => e.status == filter['status']), isTrue); } } catch (error) { // 에러시 테스트 데이터 생성 후 재시도 await createTestDataForFilter(filter); final equipments = await equipmentService.getEquipments(filter); expect(equipments, isNotEmpty); } } }); ``` ### 4. 🏢 회사 관리 화면 (Company Management) #### 테스트 시나리오 ```dart test('회사 및 지점 관리 전체 프로세스', () async { // 1. 회사 생성 final companyData = TestDataGenerator.generateCompanyData(); try { final company = await companyService.createCompany(companyData); expect(company.id, isNotNull); // 2. 지점 추가 final branchData = TestDataGenerator.generateBranchData(company.id); final branch = await companyService.createBranch(branchData); expect(branch.companyId, equals(company.id)); // 3. 연락처 정보 추가 final contactData = { 'company_id': company.id, 'name': '담당자', 'phone': '010-1234-5678', 'email': 'contact@test.com', }; await companyService.addContact(contactData); // 4. 회사 정보 수정 company.name = '${company.name} (수정됨)'; final updated = await companyService.updateCompany(company); expect(updated.name, contains('수정됨')); } catch (error) { final diagnosis = ApiErrorDiagnostics.diagnose(error); // 중복 회사명 오류시 처리 if (diagnosis.errorType == ErrorType.duplicate) { companyData['name'] = '${companyData['name']}_${DateTime.now().millisecondsSinceEpoch}'; final company = await companyService.createCompany(companyData); expect(company.id, isNotNull); } } }); ``` ### 5. 👥 사용자 관리 화면 (User Management) #### 테스트 시나리오 ```dart test('사용자 CRUD 및 권한 관리 테스트', () async { // 1. 사용자 생성 final company = await prepareTestCompany(); final userData = TestDataGenerator.generateUserData( companyId: company.id, role: 'M', // Member ); try { final user = await userService.createUser(userData); expect(user.id, isNotNull); // 2. 권한 변경 (Member -> Admin) user.role = 'A'; await userService.updateUser(user); // 3. 비밀번호 변경 await userService.changePassword(user.id, { 'current_password': userData['password'], 'new_password': 'NewPassword123!', }); // 4. 계정 비활성화 await userService.changeUserStatus(user.id, false); // 5. 삭제 await userService.deleteUser(user.id); } catch (error) { final diagnosis = ApiErrorDiagnostics.diagnose(error); // 이메일 중복 오류시 if (diagnosis.errorType == ErrorType.duplicateEmail) { userData['email'] = TestDataGenerator.generateUniqueEmail(); final user = await userService.createUser(userData); expect(user.id, isNotNull); } } }); ``` ### 6. 📍 창고 위치 관리 (Warehouse Location) #### 테스트 시나리오 ```dart test('창고 위치 계층 구조 관리 테스트', () async { // 1. 메인 창고 생성 final mainWarehouse = await warehouseService.createLocation({ 'name': '메인 창고', 'code': 'MAIN', 'level': 1, }); // 2. 하위 구역 생성 final zones = ['A', 'B', 'C']; for (final zone in zones) { try { final subLocation = await warehouseService.createLocation({ 'name': '구역 $zone', 'code': 'MAIN-$zone', 'parent_id': mainWarehouse.id, 'level': 2, }); // 3. 선반 생성 for (int shelf = 1; shelf <= 5; shelf++) { await warehouseService.createLocation({ 'name': '선반 $shelf', 'code': 'MAIN-$zone-$shelf', 'parent_id': subLocation.id, 'level': 3, }); } } catch (error) { // 코드 중복시 자동 수정 final diagnosis = ApiErrorDiagnostics.diagnose(error); if (diagnosis.errorType == ErrorType.duplicateCode) { // 타임스탬프 추가하여 유니크하게 const newCode = 'MAIN-$zone-${DateTime.now().millisecondsSinceEpoch}'; await warehouseService.createLocation({ 'name': '구역 $zone', 'code': newCode, 'parent_id': mainWarehouse.id, 'level': 2, }); } } } }); ``` ### 7. 📜 라이선스 관리 (License Management) #### 테스트 시나리오 ```dart test('라이선스 생명주기 관리 테스트', () async { // 1. 라이선스 생성 final company = await prepareTestCompany(); final licenseData = TestDataGenerator.generateLicenseData( companyId: company.id, expiryDays: 30, // 30일 후 만료 ); try { final license = await licenseService.createLicense(licenseData); expect(license.id, isNotNull); // 2. 사용자에게 할당 final user = await prepareTestUser(company.id); await licenseService.assignLicense(license.id, user.id); // 3. 만료 예정 라이선스 조회 final expiringLicenses = await licenseService.getExpiringLicenses(days: 30); expect(expiringLicenses.any((l) => l.id == license.id), isTrue); // 4. 라이선스 갱신 license.expiryDate = DateTime.now().add(Duration(days: 365)); await licenseService.updateLicense(license); // 5. 할당 해제 await licenseService.unassignLicense(license.id); } catch (error) { final diagnosis = ApiErrorDiagnostics.diagnose(error); // 라이선스 키 중복시 if (diagnosis.errorType == ErrorType.duplicateLicenseKey) { licenseData['license_key'] = TestDataGenerator.generateUniqueLicenseKey(); final license = await licenseService.createLicense(licenseData); expect(license.id, isNotNull); } } }); ``` ## 🔧 에러 자동 진단 및 수정 시스템 ### 에러 타입별 자동 수정 전략 #### 1. 필수 필드 누락 (400 Bad Request) ```dart if (error.response?.data['missing_fields'] != null) { final missingFields = error.response.data['missing_fields'] as List; for (final field in missingFields) { switch (field) { case 'equipment_number': data['equipment_number'] = 'EQ-${DateTime.now().millisecondsSinceEpoch}'; break; case 'manufacturer': data['manufacturer'] = await getOrCreateManufacturer('기본제조사'); break; case 'warehouse_location_id': data['warehouse_location_id'] = await getOrCreateWarehouse(); break; } } } ``` #### 2. 타입 불일치 (422 Unprocessable Entity) ```dart if (error.response?.data['type_errors'] != null) { final typeErrors = error.response.data['type_errors'] as Map; typeErrors.forEach((field, expectedType) { switch (expectedType) { case 'integer': data[field] = int.tryParse(data[field].toString()) ?? 0; break; case 'datetime': data[field] = DateTime.parse(data[field].toString()).toIso8601String(); break; case 'boolean': data[field] = data[field].toString().toLowerCase() == 'true'; break; } }); } ``` #### 3. 참조 무결성 오류 (409 Conflict) ```dart if (error.response?.data['foreign_key_error'] != null) { final fkError = error.response.data['foreign_key_error']; switch (fkError['table']) { case 'companies': data[fkError['field']] = await createTestCompany().id; break; case 'warehouse_locations': data[fkError['field']] = await createTestWarehouse().id; break; case 'users': data[fkError['field']] = await createTestUser().id; break; } } ``` #### 4. 중복 데이터 (409 Conflict) ```dart if (error.response?.data['duplicate_field'] != null) { final duplicateField = error.response.data['duplicate_field']; switch (duplicateField) { case 'email': data['email'] = '${data['email'].split('@')[0]}_${DateTime.now().millisecondsSinceEpoch}@test.com'; break; case 'serial_number': data['serial_number'] = 'SN-${DateTime.now().millisecondsSinceEpoch}-${Random().nextInt(9999)}'; break; case 'license_key': data['license_key'] = UUID.v4(); break; } } ``` ## 📊 테스트 실행 및 리포팅 ### 통합 테스트 실행기 ```dart // test/integration/automated/test_runner.dart class AutomatedTestRunner { static Future runAllTests() async { final results = []; // 모든 화면 테스트 실행 results.add(await LoginAutomatedTest.run()); results.add(await DashboardAutomatedTest.run()); results.add(await EquipmentAutomatedTest.run()); results.add(await CompanyAutomatedTest.run()); results.add(await UserAutomatedTest.run()); results.add(await WarehouseAutomatedTest.run()); results.add(await LicenseAutomatedTest.run()); // 리포트 생성 return TestReport( totalTests: results.length, passed: results.where((r) => r.passed).length, failed: results.where((r) => !r.passed).length, autoFixed: results.expand((r) => r.autoFixedErrors).length, duration: results.fold(Duration.zero, (sum, r) => sum + r.duration), details: results, ); } } ``` ### 테스트 리포트 형식 ```json { "timestamp": "2025-01-21T10:30:00Z", "environment": "test", "summary": { "totalScreens": 7, "totalTests": 156, "passed": 148, "failed": 8, "autoFixed": 23, "duration": "5m 32s" }, "screens": [ { "name": "Equipment Management", "tests": 32, "passed": 29, "failed": 3, "autoFixed": 7, "errors": [ { "test": "장비 입고 - 필수 필드 누락", "error": "Missing required field: manufacturer", "fixed": true, "fixApplied": "Added default manufacturer" } ] } ] } ``` ## 🚀 CI/CD 통합 ### GitHub Actions 설정 ```yaml name: Automated API Tests on: push: branches: [main, develop] pull_request: branches: [main] schedule: - cron: '0 0 * * *' # 매일 자정 실행 jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Setup Flutter uses: subosito/flutter-action@v2 with: flutter-version: '3.x' - name: Install dependencies run: flutter pub get - name: Run automated tests run: flutter test test/integration/automated/test_runner.dart env: API_BASE_URL: ${{ secrets.TEST_API_URL }} TEST_USER_EMAIL: ${{ secrets.TEST_USER_EMAIL }} TEST_USER_PASSWORD: ${{ secrets.TEST_USER_PASSWORD }} - name: Upload test report uses: actions/upload-artifact@v3 with: name: test-report path: test-report.json - name: Notify on failure if: failure() uses: 8398a7/action-slack@v3 with: status: ${{ job.status }} text: 'Automated API tests failed!' ``` ## 📈 성공 지표 1. **테스트 커버리지**: 모든 화면의 95% 이상 기능 커버 2. **자동 복구율**: 발생한 에러의 80% 이상 자동 수정 3. **실행 시간**: 전체 테스트 10분 이내 완료 4. **안정성**: 연속 100회 실행시 95% 이상 성공률 ## 🔄 향후 확장 계획 1. **성능 테스트 추가** - 부하 테스트 - 응답 시간 측정 - 동시성 테스트 2. **보안 테스트 통합** - SQL Injection 테스트 - XSS 방어 테스트 - 권한 우회 시도 3. **UI 자동화 연동** - Widget 테스트와 통합 - 스크린샷 비교 - 시각적 회귀 테스트 4. **AI 기반 테스트 생성** - 사용 패턴 학습 - 엣지 케이스 자동 발견 - 테스트 시나리오 추천 --- 본 계획서는 SuperPort 애플리케이션의 품질 보증을 위한 포괄적인 자동화 테스트 전략을 제공합니다. 모든 테스트는 실제 API를 사용하며, 발생하는 오류를 자동으로 진단하고 수정하여 안정적인 테스트 환경을 보장합니다.