Notice
Recent Posts
Recent Comments
Link
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | ||
6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 |
Tags
- 이터레이터
- 티스토리 쿠키 삭제
- Kernighan의 C언어 프로그래밍
- 자료구조와 함께 배우는 알고리즘 입문
- 선형대수
- d
- 스프링 시큐리티
- iterator
- GIT
- 스프링부트핵심가이드
- baeldung
- 알파회계
- resttemplate
- 친절한SQL튜닝
- 페이징
- network configuration
- 서버설정
- 목록처리
- /etc/network/interfaces
- 코드로배우는스프링웹프로젝트
- 리눅스
- 자료구조와함께배우는알고리즘입문
- 코드로배우는스프링부트웹프로젝트
- 구멍가게코딩단
- 처음 만나는 AI 수학 with Python
- 처음 만나는 AI수학 with Python
- 데비안
- 자바편
- ㅒ
- 네트워크 설정
Archives
- Today
- Total
bright jazz music
[nestjs] jest를 사용한 테스트 코드 작성(단위, e2e) 본문
1. 단위 테스트
1.1. members.controller.specs.ts
// src/members/members.controller.spec.ts
// 회원 관리 컨트롤러 테스트
import { Test, TestingModule } from '@nestjs/testing';
import { MembersController } from './members.controller';
import { MembersService } from './members.service';
import { AuthProvider } from '@common/enums';
describe('MembersController', () => {
let controller: MembersController;
let service: MembersService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [MembersController],
providers: [
{
provide: MembersService,
useValue: {
create: jest.fn(),
findOne: jest.fn(),
// ... 다른 메서드들
},
},
],
}).compile();
controller = module.get<MembersController>(MembersController);
service = module.get<MembersService>(MembersService);
});
describe('create', () => {
it('should create a member', async () => {
const dto = {
email: 'test@example.com',
password: 'password123',
termsAgreed: true,
privacyAgreed: true,
marketingAgreed: false,
provider: AuthProvider.LOCAL,
};
await controller.create(dto);
expect(service.create).toHaveBeenCalledWith(dto);
});
});
});
1.2. members.service.specs.ts
// src/members/members.service.spec.ts
// 회원 관리 서비스 테스트
import { Test, TestingModule } from '@nestjs/testing';
import { MembersService } from './members.service';
import { getRepositoryToken } from '@nestjs/typeorm';
import { Member } from './entities/member.entity';
import { Repository } from 'typeorm';
import { CreateMemberDto } from './dto/create-member.dto';
import { UpdateMemberDto } from './dto/update-member.dto';
import { ConflictException, NotFoundException, BadRequestException } from '@nestjs/common';
import { AuthProvider, MemberStatus } from '@common/enums';
import { EmailService } from '@common/services/email.service';
describe('MembersService', () => {
let service: MembersService;
let repository: Repository<Member>;
let emailService: EmailService;
const mockRepository = {
findOne: jest.fn(),
save: jest.fn(),
softDelete: jest.fn(),
};
const mockEmailService = {
send: jest.fn().mockResolvedValue(undefined),
};
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
MembersService,
{
provide: getRepositoryToken(Member),
useValue: mockRepository,
},
{
provide: EmailService,
useValue: mockEmailService,
},
],
}).compile();
service = module.get<MembersService>(MembersService);
repository = module.get<Repository<Member>>(getRepositoryToken(Member));
emailService = module.get<EmailService>(EmailService);
});
describe('create', () => {
const createMemberDto: CreateMemberDto = {
email: 'test@example.com',
password: 'password123',
provider: AuthProvider.LOCAL,
termsAgreed: true,
privacyAgreed: true,
marketingAgreed: false
};
it('필수 약관 미동의 시 예외가 발생해야 함', async () => {
const invalidDto = {
...createMemberDto,
termsAgreed: false
};
await expect(service.create(invalidDto)).rejects.toThrow(BadRequestException);
});
it('회원가입 성공 시 이메일이 발송되어야 함', async () => {
mockRepository.findOne.mockResolvedValue(null);
const emailServiceSpy = jest.spyOn(emailService, 'send');
await service.create(createMemberDto);
expect(emailServiceSpy).toHaveBeenCalled();
});
it('비밀번호가 해시되어 저장되어야 함', async () => {
mockRepository.findOne.mockResolvedValue(null);
const savedMember = jest.fn();
mockRepository.save.mockImplementation((member) => {
savedMember(member);
return member;
});
await service.create(createMemberDto);
// save 호출 시 전달된 엔티티의 password를 검증
expect(savedMember).toHaveBeenCalledWith(
expect.objectContaining({
password: expect.stringMatching(/^\$2[aby]\$\d{1,2}\$[./A-Za-z0-9]{53}$/)
})
);
});
it('약관 동의 시간이 기록되어야 함', async () => {
mockRepository.findOne.mockResolvedValue(null);
mockRepository.save.mockImplementation(member => member);
const result = await service.create(createMemberDto);
expect(result.termsAgreedAt).toBeInstanceOf(Date);
expect(result.privacyAgreedAt).toBeInstanceOf(Date);
expect(result.marketingAgreedAt).toBeUndefined(); // marketingAgreed가 false이므로
});
});
describe('findOneByUuid', () => {
const uuid = 'test-uuid';
const mockMember = {
uuid,
email: 'test@example.com',
status: MemberStatus.ACTIVE,
};
it('UUID로 회원을 찾아야 함', async () => {
mockRepository.findOne.mockResolvedValue(mockMember);
const result = await service.findOneByUuid(uuid);
expect(result).toBeDefined();
expect(result.uuid).toBe(uuid);
});
it('회원이 없으면 NotFoundException을 발생시켜야 함', async () => {
mockRepository.findOne.mockResolvedValue(null);
await expect(service.findOneByUuid(uuid)).rejects.toThrow(
NotFoundException,
);
});
});
describe('update', () => {
const uuid = 'test-uuid';
const updateMemberDto: UpdateMemberDto = {
// nickname: '새로운닉네임',
};
const mockMember = {
uuid,
email: 'test@example.com',
nickname: '기존닉네임',
};
it('회원 정보를 업데이트해야 함', async () => {
mockRepository.findOne.mockResolvedValue(mockMember);
mockRepository.save.mockResolvedValue({
...mockMember,
...updateMemberDto,
});
const result = await service.update(uuid, updateMemberDto);
// expect(result.nickname).toBe(updateMemberDto.nickname);
expect(mockRepository.save).toHaveBeenCalled();
});
it('존재하지 않는 회원이면 NotFoundException을 발생시켜야 함', async () => {
mockRepository.findOne.mockResolvedValue(null);
await expect(service.update(uuid, updateMemberDto)).rejects.toThrow(
NotFoundException,
);
});
it('마케팅 동의 상태 변경 시 동의 시간이 기록되어야 함', async () => {
const uuid = 'test-uuid';
const member = {
uuid,
marketingAgreed: false,
marketingAgreedAt: null,
};
const updateDto = {
marketingAgreed: true,
};
mockRepository.findOne.mockResolvedValue(member);
mockRepository.save.mockImplementation(m => ({
...m,
marketingAgreedAt: expect.any(Date),
}));
const result = await service.update(uuid, updateDto);
expect(result.marketingAgreed).toBe(true);
expect(result.marketingAgreedAt).toBeDefined();
});
it('비밀번호 변경 시 해시되어 저장되어야 함', async () => {
const uuid = 'test-uuid';
const member = {
uuid,
password: 'old-hashed-password',
};
const updateDto = {
password: 'NewPassword123!',
};
mockRepository.findOne.mockResolvedValue(member);
const savedMember = jest.fn();
mockRepository.save.mockImplementation((member) => {
savedMember(member);
return member;
});
await service.update(uuid, updateDto);
// save 호출 시 전달된 엔티티의 password를 검증
expect(savedMember).toHaveBeenCalledWith(
expect.objectContaining({
password: expect.stringMatching(/^\$2[aby]\$\d{1,2}\$[./A-Za-z0-9]{53}$/),
passwordChangedAt: expect.any(Date)
})
);
});
});
describe('softDelete', () => {
const uuid = 'test-uuid';
it('회원을 소프트 삭제해야 함', async () => {
mockRepository.softDelete.mockResolvedValue({ affected: 1 });
await service.softDelete(uuid);
expect(mockRepository.softDelete).toHaveBeenCalledWith({ uuid });
});
it('존재하지 않는 회원이면 NotFoundException을 발생시켜야 함', async () => {
mockRepository.softDelete.mockResolvedValue({ affected: 0 });
await expect(service.softDelete(uuid)).rejects.toThrow(NotFoundException);
});
});
describe('email verification', () => {
it('이메일 인증 토큰이 유효하면 회원 상태가 ACTIVE로 변경되어야 함', async () => {
const token = 'valid-token';
const member = {
verificationToken: token,
status: MemberStatus.PENDING,
verificationTokenExpiresAt: new Date(Date.now() + 3600000),
};
mockRepository.findOne.mockResolvedValue(member);
const result = await service.verifyEmail(token);
expect(result.status).toBe(MemberStatus.ACTIVE);
expect(result.emailVerified).toBe(true);
});
it('만료된 토큰으로 인증 시 새로운 토큰이 발급되어야 함', async () => {
const token = 'expired-token';
const member = {
verificationToken: token,
status: MemberStatus.PENDING,
verificationTokenExpiresAt: new Date(Date.now() - 3600000),
};
mockRepository.findOne.mockResolvedValue(member);
const emailServiceSpy = jest.spyOn(emailService, 'send');
await expect(service.verifyEmail(token)).rejects.toThrow(BadRequestException);
expect(emailServiceSpy).toHaveBeenCalled();
});
it('잘못된 토큰으로 인증 시 NotFoundException이 발생해야 함', async () => {
mockRepository.findOne.mockResolvedValue(null);
await expect(service.verifyEmail('invalid-token')).rejects.toThrow(NotFoundException);
});
it('이미 인증된 회원은 BadRequestException이 발생해야 함', async () => {
const token = 'valid-token';
const member = {
verificationToken: token,
status: MemberStatus.ACTIVE,
emailVerified: true,
verificationTokenExpiresAt: new Date(Date.now() + 3600000),
};
mockRepository.findOne.mockResolvedValue(member);
await expect(service.verifyEmail(token)).rejects.toThrow(BadRequestException);
});
});
describe('password security', () => {
it('비밀번호 변경 시 토큰 버전이 증가해야 함', async () => {
const uuid = 'test-uuid';
const member = {
uuid,
tokenVersion: 1,
};
mockRepository.findOne.mockResolvedValue(member);
await service.update(uuid, { password: 'newPassword123!' });
expect(mockRepository.save).toHaveBeenCalledWith(
expect.objectContaining({
tokenVersion: 2,
}),
);
});
});
describe('findOneByEmailWithPassword', () => {
it('이메일로 회원을 찾을 때 필요한 필드만 조회해야 함', async () => {
const email = 'test@example.com';
mockRepository.findOne.mockResolvedValue({
id: 1,
uuid: 'test-uuid',
email,
status: MemberStatus.ACTIVE,
loginAttempts: 0,
});
const result = await service.findOneByEmailWithPassword(email);
expect(result).toHaveProperty('id');
expect(result).toHaveProperty('uuid');
expect(result).toHaveProperty('email');
expect(result).toHaveProperty('status');
expect(result).toHaveProperty('loginAttempts');
expect(result).not.toHaveProperty('password'); // 비밀번호는 기본적으로 조회되지 않아야 함
});
});
});
2. e2e 테스트 디렉토리 설정 및 전체 테스트 코드 작성
2.1. 테스트 디렉토리 설정 파일 작성
루트 디렉토리(프로젝트 디렉토리, src의 상위 디렉토리)에 존재하는 test디렉토리에 jest-e2e.json 파일을 만들고 아래의 내용을 적어준다. json파일은 내외로 주석을 달면 에러가 발생하기 때문에 여기서는 달지 않았다.
{
"moduleFileExtensions": ["js", "json", "ts"],
"rootDir": ".",
"testEnvironment": "node",
"testRegex": ".e2e-spec.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
}
}
2.2 애플리케이션 전체 테스트 코드 작성
// test/app.e2e-spec.ts
// 애플리케이션 전체 테스트
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import * as request from 'supertest';
import { AppModule } from './../src/app.module';
describe('AppController (e2e)', () => {
let app: INestApplication;
beforeEach(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleFixture.createNestApplication();
await app.init();
});
afterAll(async () => {
await app.close();
});
it('/ (GET)', () => {
return request(app.getHttpServer())
.get('/')
.expect(200);
});
});
3. members의 e2e 테스트 작성
test/e2e/members 디렉토리 만들고 아래 파일 작성
// test/e2e/members/members.e2e-spec.ts
// 회원 관리 테스트
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import * as request from 'supertest';
import { AppModule } from '../../../src/app.module';
describe('MembersController (e2e)', () => {
let app: INestApplication;
beforeEach(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleFixture.createNestApplication();
await app.init();
});
it('/members (POST)', () => {
return request(app.getHttpServer())
.post('/members')
.send({
email: 'test@example.com',
password: 'password123',
termsAgreed: true,
privacyAgreed: true,
marketingAgreed: false,
})
.expect(201)
.expect((res) => {
expect(res.body.email).toBe('test@example.com');
expect(res.body.uuid).toBeDefined();
});
});
});
'Framework > NestJS' 카테고리의 다른 글
[nestjs] 인증 기능(auth) 작성하기 (0) | 2024.12.26 |
---|---|
[nestjs] 멤버 컨트롤러, 서비스, 리포지토리 (0) | 2024.12.25 |
[nestjs] 스웨거 설치 및 DTO 에 적용 (0) | 2024.12.25 |
[nestjs] 회원 관련 엔티티와(member.entity.ts) 과 관련 파일 작성(enum등) (0) | 2024.12.23 |
[nestjs] TypeORM을 사용해 DB에 테이블 생성 (0) | 2024.12.21 |
Comments