AI 단위 테스트 가이드 — 환각 없음, 크로스 스택 표준
출처: Dev.to
포커스: 유닛 테스트 ONLY — 통합 없음, E2E 없음
스택: Node.js (NestJS/Express) · React.js · Python · Angular · Laravel
목표: AI가 일관되게, 결정적으로, 환각 없이 유닛 테스트를 생성합니다
IDE: Cursor (주요) + Claude (보조)
파트 1 — 스택별 최적 라이브러리 (최종 결정)
Do not mix libraries. Pick one per stack, configure it fully, never deviate.
| Stack | Library | Why This One |
|---|---|---|
| Node.js / NestJS / Express | Jest | Native DI mocking, @nestjs/testing를 기반으로 함, 가장 넓은 생태계 |
| React.js | Vitest + @testing-library/react | Vite/ESM 지원 네이티브, Jest와 호환되는 API, 3~10배 빠름 |
| Python | pytest | 사실 표준이며, 팩시티 시스템으로 보일러플레이트 제거, 최고의 플러그인 생태계 |
| Angular | Jest (Karma 대체) | Karma는 Angular 17+에서 더 이상 사용되지 않으며, Jest가 공식 마이그레이션 대상 |
| Laravel | Pest | 현대적인 구문, PHPUnit을 기반으로 하며 신호‑노이즈 비율이 높음 |
Rule: If someone suggests a second library for the same stack, reject it. One library per stack, configured once, followed always.
파트 2 — IDE: Cursor (이 프로젝트에 유일한 선택)
| Capability | Cursor | VS Code + Copilot | WebStorm |
|---|---|---|---|
| 프로젝트 수준 AI 규칙 | ✅ .cursor/rules/ | ❌ | ❌ |
| 코드베이스 인식 컨텍스트 | ✅ @codebase | Partial | Partial |
| 터미널 실행 및 출력 읽기 | ✅ Composer | ❌ | ❌ |
| 다중 파일 생성 | ✅ 에이전트 모드 | 제한적 | ❌ |
| 파일 유형별 커스텀 인스트럭션 | ✅ | ❌ | ❌ |
| MCP 서버 통합 | ✅ | ❌ | ❌ |
Cursor의 .cursor/rules/ 시스템은 유일한 IDE 내장 메커니즘으로, 모든 AI 상호작용에 지속적이고 프로젝트 범위 내의 지침을 주입합니다 — 이것이 바로 출처에서 환각을 방지하는 것입니다.
파트 3 — Cursor 규칙 파일 (환각 방지 핵심)
이 파일들은 해당 파일을 작업할 때 자동으로 모든 AI 프롬프트에 주입됩니다.
3.1 전역 유닛 테스트 규칙
---
description: Global unit test rules — applies to all files in this project
globs: ["**/*.spec.ts", "**/*.test.ts", "**/*.spec.tsx", "**/*_test.py", "**/Test*.php"]
alwaysApply: true
---
# Unit Test Contract — MUST FOLLOW, NO EXCEPTIONS
## What is a unit test here
- Tests ONE class or function in complete isolation
- ALL external dependencies are mocked — no real DB, no real HTTP, no real file system
- Each test runs independently — no shared mutable state between tests
- Runs in useMyHook();
act(() => result.current.doSomething());
expect(result.current.value).toBe('expected');
유닛 테스트 계약 — 필수 준수, 예외 없음
여기서 유닛 테스트가 무엇인가
- ONE 클래스 또는 함수에 완전한 고립 상태에서 테스트
- 모든 외부 의존성은 모킹됩니다 — 실제 DB 없음, 실제 HTTP 없음, 실제 파일 시스템 없음
- 각 테스트는 독립적으로 실행되며 테스트 간 공유 가변 상태가 없습니다
- Runs in useMyHook(); act(() => result.current.doSomething()); expect(result.current.value).toBe(‘expected’);
3.4 파이썬 규칙
---
description: Python unit test rules
globs: ["**/*.py", "!**/*migrations*"]
---
# Python Unit Test Rules
## Library: pytest + pytest-mock + factory-boy
## File naming
- Source: `app/services/user_service.py`
- Test: `tests/unit/test_user_service.py`
- ALWAYS mirror the source directory structure under tests/unit/
## Class under test — always use dependency injection
def __init__(self, repo: UserRepository):
self.repo = repo
BAD — untestable
class UserService:
def __init__(self):
self.repo = UserRepository() # can't mock this
모킹 패턴 — pytest-mock (mocker 팁)
def test_raises_when_user_not_found(mocker):
mock_repo = mocker.Mock()
mock_repo.find_by_id.return_value = None
service = UserService(mock_repo)
with pytest.raises(UserNotFoundException):
service.get_user("missing-id")
비동기 테스트 — pytest-asyncio
import pytest
@pytest.mark.asyncio
async def test_async_service(mocker):
mock_repo = mocker.AsyncMock()
...
팩시티 패턴 — 공유 설정용
@pytest.fixture
def user_service(mocker):
repo = mocker.Mock()
return UserService(repo), repo
예시 테스트
def test_get_user_happy_path(user_service):
service, repo = user_service
repo.find_by_id.return_value = User(id="1", email="a@b.com")
result = service.get_user("1")
assert result.email == "a@b.com"
Naming
- 파일:
test_[module_name].py - 함수:
test_[예상 동작]_when_[조건]
markdown
3.5 Angular Rule
**File:** `.cursor/rules/unit-test-angular.mdc`
---
Karma에서 Jest로 마이그레이션 (일회성)
ng add @angular-builders/jest
TestBed 설정 — 항상 최소화
await TestBed.configureTestingModule({
imports: [ComponentUnderTest], // standalone components
providers: [
{ provide: UserService, useValue: mockUserService }
]
}).compileComponents();
서비스 모킹 패턴
const mockUserService = {
getUser: jest.fn(),
createUser: jest.fn(),
};
컴포넌트 테스트 — 내부 상태가 아닌 DOM 동작 확인
fixture.detectChanges(); // trigger ngOnInit
const button = fixture.debugElement.query(By.css('[data-testid="submit"]'));
button.nativeElement.click();
fixture.detectChanges();
expect(fixture.debugElement.query(By.css('.error-msg'))).toBeTruthy();
파이프 테스트 — 순수 함수, TestBed 필요 없음
it('should transform date correctly', () => {
const pipe = new DateFormatPipe();
expect(pipe.transform(new Date('2024-01-01'))).toBe('Jan 1, 2024');
});
Guard/Resolver 테스트 — 직접 주입하여 호출
const guard = TestBed.inject(AuthGuard);
const result = await guard.canActivate(mockRoute, mockState);
expect(result).toBe(fal