클라이언트 앱 테스트 가이드
작성일: 2026-04-06 Last Updated: 2026-04-06 대상: Browser Extension, Tauri Desktop, Flutter Mobile
목차
1. 테스트 피라미드
/ E2E \ ← 적게, 핵심 플로우만
/----------\
/ Integration \ ← 모듈 간 상호작용
/----------------\
/ Unit \ ← 많이, 빠르게
/--------------------\
계층별 비율 가이드
| 계층 | 비율 | 실행 시간 | 주요 대상 |
|---|---|---|---|
| Unit | 70% | < 1분 | 순수 함수, 유틸리티, 상태 관리, 비즈니스 로직 |
| Integration | 20% | < 5분 | API 연동, 스토리지, 모듈 간 통신 |
| E2E | 10% | < 15분 | 핵심 사용자 플로우, 결제, 인증 |
2. 플랫폼별 테스트 도구
2.1 Browser Extension
| 계층 | 도구 | 설정 파일 | 비고 |
|---|---|---|---|
| Unit | Vitest | vitest.config.ts | 빠른 실행, ESM 네이티브 |
| Integration | Vitest + webextension-polyfill-mock | - | chrome.* API 모킹 |
| E2E | Playwright | playwright.config.ts | 실제 브라우저 + 확장 로드 |
// Vitest 설정 (Browser Extension)
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
environment: 'jsdom',
globals: true,
setupFiles: ['./tests/setup.ts'],
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html'],
thresholds: {
statements: 80,
branches: 75,
functions: 80,
lines: 80,
},
},
},
});
// Playwright E2E (확장 프로그램 로드)
import { test, chromium } from '@playwright/test';
test('extension popup loads', async () => {
const context = await chromium.launchPersistentContext('', {
headless: false,
args: [
`--disable-extensions-except=${extensionPath}`,
`--load-extension=${extensionPath}`,
],
});
// 확장 프로그램 테스트...
});
2.2 Tauri Desktop
| 계층 | 도구 | 비고 |
|---|---|---|
| Unit (Rust) | cargo test | #[cfg(test)] 모듈 |
| Unit (Frontend) | Vitest | UI 컴포넌트 |
| Integration | cargo test + tauri::test | Tauri 명령 테스트 |
| E2E | WebDriver (tauri-driver) 또는 Playwright | WebView 기반 |
// Tauri 명령 단위 테스트
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_process_data() {
let result = process_data("input");
assert_eq!(result, "expected_output");
}
}
// Tauri 통합 테스트 (tauri::test)
#[cfg(test)]
mod integration_tests {
use tauri::test::{mock_builder, MockRuntime};
#[tauri::command]
async fn my_command() -> Result<String, String> {
Ok("result".into())
}
#[test]
fn test_tauri_command() {
let app = mock_builder()
.invoke_handler(tauri::generate_handler![my_command])
.build(tauri::test::mock_context(tauri::generate_context!()))
.unwrap();
// 명령 호출 테스트...
}
}
2.3 Flutter Mobile
| 계층 | 도구 | 디렉토리 | 비고 |
|---|---|---|---|
| Unit | flutter_test | test/ | Widget 테스트 포함 |
| Integration | flutter_test + mockito | test/ | 서비스 통합 |
| E2E | integration_test | integration_test/ | 실제 디바이스/에뮬레이터 |
// Unit 테스트 (flutter_test)
import 'package:flutter_test/flutter_test.dart';
void main() {
group('AuthService', () {
test('validates email format', () {
expect(AuthService.isValidEmail('user@example.com'), isTrue);
expect(AuthService.isValidEmail('invalid'), isFalse);
});
});
}
// Widget 테스트
testWidgets('Login button triggers auth', (tester) async {
await tester.pumpWidget(const MyApp());
await tester.enterText(find.byKey(Key('email')), 'user@test.com');
await tester.tap(find.byKey(Key('loginButton')));
await tester.pumpAndSettle();
expect(find.text('Welcome'), findsOneWidget);
});
// Integration 테스트 (integration_test/)
import 'package:integration_test/integration_test.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
testWidgets('full login flow', (tester) async {
app.main();
await tester.pumpAndSettle();
// 전체 로그인 플로우 테스트...
});
}
3. 스토어 심사 체크리스트 기반 테스트
3.1 Chrome Web Store 심사 기준 테스트
| 심사 항목 | 테스트 방법 | 자동화 |
|---|---|---|
| 단일 목적 원칙 | 코드 리뷰 (불필요 권한 없음) | 수동 |
| 최소 권한 사용 | manifest.json 권한 검증 테스트 | O |
| Content Security Policy | CSP 헤더 검증 | O |
| 원격 코드 실행 금지 | eval(), new Function() 사용 검사 | O |
| 사용자 데이터 보호 | 데이터 전송 암호화 테스트 | O |
| 개인정보처리방침 링크 | URL 접근성 테스트 | O |
// 권한 검증 테스트
describe('Manifest permissions', () => {
it('should only request necessary permissions', () => {
const manifest = JSON.parse(readFileSync('manifest.json', 'utf-8'));
const allowed = ['storage', 'activeTab', 'alarms'];
const extra = manifest.permissions.filter(
(p: string) => !allowed.includes(p)
);
expect(extra).toEqual([]);
});
it('should not use remote code', () => {
const csp = manifest.content_security_policy?.extension_pages;
expect(csp).not.toContain("'unsafe-eval'");
});
});
3.2 Google Play Store 심사 기준 테스트
| 심사 항목 | 테스트 방법 | 자동화 |
|---|---|---|
| 타겟 API 레벨 | build.gradle targetSdk 검증 | O |
| 64bit 지원 | APK 분석 (arm64-v8a 포함) | O |
| 백그라운드 제한 준수 | 서비스 사용 패턴 테스트 | O |
| 권한 사용 근거 | 런타임 권한 요청 테스트 | O |
| 데이터 안전 섹션 | 네트워크 요청 감사 | 반자동 |
| Deceptive behavior 없음 | UI 플로우 리뷰 | 수동 |
3.3 Apple App Store 심사 기준 테스트
| 심사 항목 | 테스트 방법 | 자동화 |
|---|---|---|
| App Transport Security | HTTPS 전용 통신 검증 | O |
| 개인정보 라벨 정확성 | 네트워크 트래픽 감사 | 반자동 |
| 앱 추적 투명성 (ATT) | ATT 프롬프트 표시 테스트 | O |
| 오프라인 기능 | 네트워크 차단 상태 테스트 | O |
| 크래시 없음 | 모든 화면 네비게이션 E2E | O |
| 로그인 필수 시 테스트 계정 | 리뷰 노트에 포함 확 인 | 수동 |
3.4 Firefox AMO 심사 기준 테스트
| 심사 항목 | 테스트 방법 | 자동화 |
|---|---|---|
| 소스 코드 심사 가능 | 빌드 재현성 테스트 | O |
| 난독화 금지 | 빌드 출력 검사 (minify만 허용) | O |
| 외부 스크립트 로드 금지 | CSP + 네트워크 요청 검사 | O |
| 사용자 동의 없는 데이터 수집 금지 | 옵트인 UI 테스트 | O |
4. 접근성 테스트
상세 가이드:
accessibility.md
자동화 가능 항목
| 항목 | 도구 | 플랫폼 |
|---|---|---|
| ARIA 라벨 검증 | axe-core, Lighthouse | Extension, Tauri |
| 색상 대비 | axe-core | 전체 |
| Tab 순서 | Playwright | Extension, Tauri |
| 스크린 리더 호환 | TalkBack/VoiceOver 수동 | Flutter |
| Semantics 트리 | flutter_test Semantics | Flutter |
// axe-core 접근성 자동 테스트 (Playwright)
import AxeBuilder from '@axe-core/playwright';
test('homepage has no accessibility violations', async ({ page }) => {
await page.goto('/');
const results = await new AxeBuilder({ page }).analyze();
expect(results.violations).toEqual([]);
});
// Flutter Semantics 테스트
testWidgets('has correct semantics', (tester) async {
await tester.pumpWidget(const MyButton(label: 'Submit'));
final semantics = tester.getSemantics(find.byType(MyButton));
expect(semantics.label, 'Submit');
expect(semantics.hasAction(SemanticsAction.tap), isTrue);
});
5. 성능 테스트
5.1 메모리 누수 검사
| 플랫폼 | 도구 | 방법 |
|---|---|---|
| Browser Extension | Chrome DevTools Memory | 힙 스냅샷 비교 (반복 작업 전후) |
| Tauri | Valgrind (Linux), Instruments (macOS) | Rust 메모리 프로파일링 |
| Flutter | DevTools Memory | flutter run --profile + 메모리 그래프 |
5.2 성능 메트릭 기준
| 메트릭 | 기준 | 측정 방법 |
|---|---|---|
| 앱 시작 시간 (Cold) | < 2초 | 프로세스 시작 ~ 첫 화면 렌더 |
| 앱 시작 시간 (Warm) | < 1초 | 백그라운드 복귀 ~ 상호작용 가능 |
| 프레임 레이트 | >= 60fps (모바일), >= 30fps (데스크톱 최소) | 프레임 드롭 카운트 |
| 메모리 사용량 | < 150MB (모바일), < 300MB (데스크톱) | 피크 메모리 |
| 배터리 소모 | 백그라운드 < 1%/hr | 배터리 모니터링 |
| 번들 크기 | Extension < 5MB, APK < 50MB | 빌드 출력 크기 |
| 네트워크 요청 | 초기 로드 < 5개 | 네트워크 탭 감사 |
5.3 번들 크기 분석
| 플랫폼 | 도구 | 명령 |
|---|---|---|
| Browser Extension | webpack-bundle-analyzer 또는 rollup-plugin-visualizer | 빌드 후 분석 |
| Tauri | cargo bloat | cargo bloat --release --crates |
| Flutter | flutter build --analyze-size | 빌드 시 --analyze-size 플래그 |
| Android | Android Studio APK Analyzer | .aab / .apk 분석 |
5.4 성능 회귀 감지 (CI)
# 번들 크기 체크 (GitHub Actions)
- name: Check bundle size
run: |
CURRENT_SIZE=$(stat -f%z dist/bundle.zip 2>/dev/null || stat -c%s dist/bundle.zip)
THRESHOLD=5242880 # 5MB
if [ "$CURRENT_SIZE" -gt "$THRESHOLD" ]; then
echo "::error::Bundle size ($CURRENT_SIZE bytes) exceeds threshold ($THRESHOLD bytes)"
exit 1
fi
6. 회귀 테스트 전략
6.1 회귀 테스트 범위
| 영역 | 테스트 항목 | 우선순위 |
|---|---|---|
| 인증/로그인 | 로그인, 로그아웃, 토큰 갱신, 세션 만료 | P0 |
| 데이터 동기화 | 로컬-서버 동기화, 충돌 해결 | P0 |
| 결제/구독 | 구매, 복원, 구독 상태 확인 | P0 |
| 핵심 기능 | 각 앱의 주요 비즈니스 로직 | P0 |
| UI 렌더링 | 주요 화면 레이아웃 | P1 |
| 오프라인 모드 | 네트워크 끊김 시 동작 | P1 |
| 업데이트 | 마이그레이션, 데이터 보존 | P1 |
| 알림 | 푸시, 로컬 알림 | P2 |
| 딥링크 | URL 스킴, 유니버설 링크 | P2 |