AI 단위 테스트 가이드 — 환각 없음, 크로스 스택 표준

발행: (2026년 6월 19일 PM 09:14 GMT+9)
6 분 소요
원문: Dev.to

출처: 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.

StackLibraryWhy This One
Node.js / NestJS / ExpressJestNative DI mocking, @nestjs/testing를 기반으로 함, 가장 넓은 생태계
React.jsVitest + @testing-library/reactVite/ESM 지원 네이티브, Jest와 호환되는 API, 3~10배 빠름
Pythonpytest사실 표준이며, 팩시티 시스템으로 보일러플레이트 제거, 최고의 플러그인 생태계
AngularJest (Karma 대체)Karma는 Angular 17+에서 더 이상 사용되지 않으며, Jest가 공식 마이그레이션 대상
LaravelPest현대적인 구문, 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 (이 프로젝트에 유일한 선택)

CapabilityCursorVS Code + CopilotWebStorm
프로젝트 수준 AI 규칙.cursor/rules/
코드베이스 인식 컨텍스트@codebasePartialPartial
터미널 실행 및 출력 읽기✅ 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
0 조회
Back to Blog

관련 글

더 보기 »