본문으로 건너뛰기

AccessCode API 테스트 명세

관련 문서

TBD

개요

1. 테스트 범위

1.1 단위 테스트

  • AccessCodeService의 각 메소드
  • DTO 유효성 검증
  • 에러 처리
  • 유틸리티 함수

1.2 통합 테스트

  • API 엔드포인트
  • 데이터베이스 연동
  • 캐시 동작
  • 이벤트 발행/구독

1.3 E2E 테스트

  • 전체 사용자 시나리오
  • 성능 테스트
  • 부하 테스트

2. 테스트 시나리오

2.1 코드 생성 테스트

describe('AccessCodeService.createCode', () => {
it('should create a new access code', async () => {
const dto = {
type: AccessCodeType.TREATMENT,
creatorId: 'user_123',
treatmentPeriod: 90,
usagePeriod: 30,
registrationChannel: RegistrationChannel.WEB,
deliveryMethod: DeliveryMethod.EMAIL
};

const result = await service.createCode(dto);

expect(result).toBeDefined();
expect(result.code).toMatch(/^[A-Z0-9]{16}$/);
expect(result.status).toBe(AccessCodeStatus.UNUSED);
});

it('should fail with invalid treatment period', async () => {
const dto = {
...validDto,
treatmentPeriod: 366 // 최대 365일
};

await expect(service.createCode(dto)).rejects.toThrow();
});
});

2.2 코드 검증 테스트

describe('AccessCodeService.validateCode', () => {
it('should validate unused code', async () => {
const code = 'AB12CD34EF56GH78';
const deviceId = 'DEVICE_001';

const result = await service.validateCode(code, deviceId);

expect(result.isValid).toBe(true);
expect(result.codeInfo).toBeDefined();
});

it('should fail with expired code', async () => {
// 만료된 코드 테스트
const expiredCode = 'XX99YY88ZZ77WW66';

await expect(
service.validateCode(expiredCode, 'DEVICE_001')
).rejects.toThrow(ErrorResponseDto);
});
});

2.3 코드 사용 테스트

describe('AccessCodeService.useCode', () => {
it('should mark code as used', async () => {
const code = await service.useCode({
codeId: 'code_123',
userId: 'user_123',
deviceId: 'DEVICE_001'
});

expect(code.status).toBe(AccessCodeStatus.USED);
expect(code.usedAt).toBeDefined();
});

it('should fail with already used code', async () => {
// 이미 사용된 코드 테스트
await expect(
service.useCode({
codeId: 'used_code_123',
userId: 'user_123',
deviceId: 'DEVICE_001'
})
).rejects.toThrow(ErrorResponseDto);
});
});

3. 테스트 환경 설정

3.1 테스트 데이터베이스

const testConfig = {
type: 'postgres',
host: process.env.TEST_DB_HOST,
port: process.env.TEST_DB_PORT,
username: process.env.TEST_DB_USER,
password: process.env.TEST_DB_PASSWORD,
database: process.env.TEST_DB_NAME,
entities: ['src/**/*.entity.ts'],
synchronize: true
};

3.2 테스트 Redis

const testRedisConfig = {
host: process.env.TEST_REDIS_HOST,
port: process.env.TEST_REDIS_PORT,
db: 1 // 테스트용 DB
};

3.3 TimeMachine 모의 객체

@Injectable()
class MockTimeMachineService {
private currentTime: Date = new Date();

async getCurrentTime(): Promise<Date> {
return this.currentTime;
}

setCurrentTime(time: Date): void {
this.currentTime = time;
}
}

4. API 테스트

4.1 엔드포인트 테스트

describe('AccessCodeController (e2e)', () => {
it('/access-codes (POST)', () => {
return request(app.getHttpServer())
.post('/v1/access-codes')
.set('Authorization', `Bearer ${token}`)
.send(createCodeDto)
.expect(201)
.expect(res => {
expect(res.body.data).toBeDefined();
expect(res.body.data.code).toMatch(/^[A-Z0-9]{16}$/);
});
});

it('/access-codes/validate (POST)', () => {
return request(app.getHttpServer())
.post('/v1/access-codes/validate')
.send({ code: 'AB12CD34EF56GH78', deviceId: 'DEVICE_001' })
.expect(200)
.expect(res => {
expect(res.body.data.isValid).toBe(true);
});
});
});

4.2 에러 응답 테스트

describe('Error Responses', () => {
it('should return 400 for invalid input', () => {
return request(app.getHttpServer())
.post('/v1/access-codes')
.set('Authorization', `Bearer ${token}`)
.send({}) // 빈 요청
.expect(400)
.expect(res => {
expect(res.body.code).toBe(ErrorCode.INVALID_INPUT);
});
});

it('should return 401 for invalid token', () => {
return request(app.getHttpServer())
.post('/v1/access-codes')
.set('Authorization', 'Bearer invalid_token')
.send(createCodeDto)
.expect(401)
.expect(res => {
expect(res.body.code).toBe(ErrorCode.UNAUTHORIZED);
});
});
});

5. 성능 테스트

5.1 부하 테스트 시나리오

describe('Load Testing', () => {
it('should handle concurrent code creation', async () => {
const concurrentRequests = 100;
const requests = Array(concurrentRequests).fill(createCodeDto);

const results = await Promise.all(
requests.map(dto => service.createCode(dto))
);

expect(results).toHaveLength(concurrentRequests);
results.forEach(result => {
expect(result.code).toBeDefined();
});
});

it('should maintain response time under load', async () => {
const start = Date.now();

await service.validateCode('TEST_CODE', 'DEVICE_001');

const duration = Date.now() - start;
expect(duration).toBeLessThan(200); // 200ms 이내
});
});

5.2 캐시 성능 테스트

describe('Cache Performance', () => {
it('should use cache for repeated validation', async () => {
const code = 'TEST_CODE';
const deviceId = 'DEVICE_001';

// 첫 번째 요청 (캐시 미스)
const start1 = Date.now();
await service.validateCode(code, deviceId);
const duration1 = Date.now() - start1;

// 두 번째 요청 (캐시 히트)
const start2 = Date.now();
await service.validateCode(code, deviceId);
const duration2 = Date.now() - start2;

expect(duration2).toBeLessThan(duration1);
});
});

6. 테스트 데이터

6.1 테스트 데이터 생성

const createTestData = async () => {
const codes = [
{
type: AccessCodeType.TREATMENT,
status: AccessCodeStatus.UNUSED,
expiresAt: addDays(new Date(), 30)
},
{
type: AccessCodeType.CLINICAL_TRIAL,
status: AccessCodeStatus.USED,
expiresAt: addDays(new Date(), -1)
}
];

await accessCodeRepository.save(codes);
};

6.2 테스트 데이터 정리

const cleanupTestData = async () => {
await accessCodeRepository.clear();
await cacheManager.reset();
};

변경 이력

버전날짜작성자변경 내용
0.1.02025-03-16bok@weltcorp.com최초 작성