- 모든 서비스 메서드 시그니처를 실제 구현에 맞게 수정 - TestDataGenerator 제거하고 직접 객체 생성으로 변경 - 모델 필드명 및 타입 불일치 수정 - 불필요한 Either 패턴 사용 제거 - null safety 관련 이슈 해결 수정된 파일: - test/integration/screens/company_integration_test.dart - test/integration/screens/equipment_integration_test.dart - test/integration/screens/user_integration_test.dart - test/integration/screens/login_integration_test.dart
20 KiB
20 KiB
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
class ScreenTestFramework {
// 화면 분석 및 테스트 가능한 액션 추출
static Future<List<TestableAction>> analyzeScreen(Widget screen);
// 필수 필드 자동 감지 및 입력
static Future<Map<String, dynamic>> generateRequiredData(String endpoint);
// API 호출 및 응답 검증
static Future<TestResult> executeApiCall(ApiRequest request);
// 에러 처리 및 재시도
static Future<TestResult> executeWithRetry(
Future<dynamic> Function() action,
{int maxRetries = 3}
);
}
ApiErrorDiagnostics
class ApiErrorDiagnostics {
static ErrorDiagnosis diagnose(DioException error) {
// 에러 타입 분류
// - 400: 검증 오류 (필수 필드, 타입 불일치)
// - 401: 인증 오류
// - 403: 권한 오류
// - 404: 리소스 없음
// - 409: 중복 오류
// - 422: 비즈니스 로직 오류
// - 500: 서버 오류
}
}
AutoFixer
class AutoFixer {
static Future<Map<String, dynamic>> fix(
ErrorDiagnosis diagnosis,
Map<String, dynamic> originalData,
) {
// 에러 타입별 자동 수정 로직
// - 필수 필드 누락: 기본값 추가
// - 타입 불일치: 타입 변환
// - 참조 무결성: 관련 데이터 생성
// - 중복 오류: 고유값 재생성
}
}
📱 화면별 테스트 계획
1. 🔐 로그인 화면 (Login Screen)
테스트 시나리오
-
정상 로그인
- 유효한 자격증명으로 로그인
- 액세스 토큰 저장 확인
- 대시보드 이동 확인
-
로그인 실패 처리
- 잘못된 이메일/비밀번호
- 비활성 계정
- 네트워크 오류
-
토큰 관리
- 토큰 만료시 자동 갱신
- 로그아웃 후 토큰 삭제
자동화 테스트 코드
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)
테스트 시나리오
-
통계 데이터 조회
- 전체 통계 로딩
- 각 카드별 데이터 검증
- 실시간 업데이트 확인
-
최근 활동 표시
- 최근 활동 목록 조회
- 페이지네이션
- 필터링
-
차트 및 그래프
- 장비 상태 분포
- 월별 입출고 현황
- 라이선스 만료 예정
자동화 테스트 코드
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 장비 입고
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 장비 출고
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 장비 목록 조회
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)
테스트 시나리오
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)
테스트 시나리오
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)
테스트 시나리오
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)
테스트 시나리오
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)
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)
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)
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)
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;
}
}
📊 테스트 실행 및 리포팅
통합 테스트 실행기
// test/integration/automated/test_runner.dart
class AutomatedTestRunner {
static Future<TestReport> runAllTests() async {
final results = <ScreenTestResult>[];
// 모든 화면 테스트 실행
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,
);
}
}
테스트 리포트 형식
{
"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 설정
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!'
📈 성공 지표
- 테스트 커버리지: 모든 화면의 95% 이상 기능 커버
- 자동 복구율: 발생한 에러의 80% 이상 자동 수정
- 실행 시간: 전체 테스트 10분 이내 완료
- 안정성: 연속 100회 실행시 95% 이상 성공률
🔄 향후 확장 계획
-
성능 테스트 추가
- 부하 테스트
- 응답 시간 측정
- 동시성 테스트
-
보안 테스트 통합
- SQL Injection 테스트
- XSS 방어 테스트
- 권한 우회 시도
-
UI 자동화 연동
- Widget 테스트와 통합
- 스크린샷 비교
- 시각적 회귀 테스트
-
AI 기반 테스트 생성
- 사용 패턴 학습
- 엣지 케이스 자동 발견
- 테스트 시나리오 추천
본 계획서는 SuperPort 애플리케이션의 품질 보증을 위한 포괄적인 자동화 테스트 전략을 제공합니다. 모든 테스트는 실제 API를 사용하며, 발생하는 오류를 자동으로 진단하고 수정하여 안정적인 테스트 환경을 보장합니다.