본 내용은 내일배움캠프에서 활동한 내용을 기록한 글입니다.
Q : 테스트 코드에 대한 감이 없습니다. 인터넷에 찾아봐도 테스트 코드의 형식은 보여도 어떤 기준을 가지고 테스트 코드를 짜야 하는지 나와있지 않습니다. 대체 어디까지 테스트해야하고 가드나 인터셉터와 같은 것들도 테스트 해야 하나요?
일단, 모든 로직을 전부 테스트할 필요가 없음
중요도를 통해서 로직의 우선순위를 생각해야 함
사이트 이펙트가 존재할 수 있으니 그에 대한 계산도 필요함
사실상 가드나 인터셉터에 대한 TDD는 생각하지 않음
TDD에 너무 매몰되어서 다른 기능들에 소홀해지지 않도록 해야 함
엣지 케이스와 커먼 케이스를 생각해야 함
케이스에 대한 구멍이 있는 테스트 코드는 차라리 없는 게 좋음
입력한 비밀번호와 확인용 비밀번호가 같은지 확인이 필요 (에러 테스트)
입력한 이메일과 닉네임이 중복되는지 확인이 필요 (에러 테스트)
'should create user'에서 실제 테스트가 성공할 경우의 테스트를 진행함
describe('signUp', () => {
beforeEach(async () => {
// 해당 메서드의 매개변수
signUpDtoObject = signUpDto;
});
it('If not match password and passwordCheck', () => {
// GIVEN
const invalidSignUpDto = { ...signUpDto, passwordCheck: 'different' };
// WHEN
const response = authService.signUp(invalidSignUpDto);
// THEN
expect(response).rejects.toThrow(BadRequestException);
expect(response).rejects.toThrow('비밀번호가 일치하지 않습니다.');
});
it('If email conflict', async () => {
// GIVEN
mockUsersService.getUserByEmail.mockResolvedValueOnce({});
// WHEN
const response = authService.signUp(signUpDtoObject);
// THEN
expect(response).rejects.toThrow(ConflictException);
expect(response).rejects.toThrow('중복된 이메일입니다.');
});
it('If nickname conflict', async () => {
// GIVEN
mockUsersService.getUserByEmail.mockResolvedValueOnce(undefined);
mockUsersService.getUserByNickname.mockResolvedValueOnce({});
// WHEN
const response = authService.signUp(signUpDtoObject);
// THEN
expect(response).rejects.toThrow(ConflictException);
expect(response).rejects.toThrow('중복된 닉네임입니다.');
});
it('should create user', async () => {
// GIVEN
const user = {
id: 1,
email: 'test12@test.com',
nickname: 'Test12',
profileImg: 'test_profile_image_url',
createdAt: '2024-07-05T23:08:07.001Z',
updatedAt: '2024-07-05T23:08:07.001Z',
};
mockUsersService.createUser.mockResolvedValue(user);
jest
.spyOn(bcrypt, 'hash')
.mockImplementation(() => Promise.resolve(signUpDtoObject.password));
// WHEN
const response = await authService.signUp(signUpDto);
// THEN
expect(mockUsersService.getUserByEmail).toHaveBeenCalledWith(signUpDtoObject.email);
expect(mockUsersService.getUserByNickname).toHaveBeenCalledWith(signUpDtoObject.nickname);
expect(response).toEqual({ ...user, password: undefined });
expect(mockUsersService.createUser).toHaveBeenCalledWith(
signUpDtoObject.email,
signUpDtoObject.password,
signUpDtoObject.nickname
);
});
});
payload
에 담아서 토큰화 하는 작업이 필요 describe('signIn', () => {
let userId: number;
beforeEach(async () => {
userId = 1;
});
it('should sign in', async () => {
// GIVEN
const mockToken = {
accessToken: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...',
refreshToken: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...',
};
jest.spyOn(jwtService, 'sign').mockImplementation((payload, options) => {
if (options?.expiresIn) {
return mockToken.accessToken;
} else {
return mockToken.refreshToken;
}
});
// WHEN
const response = await authService.signIn(userId);
// THEN
expect(jwtService.sign).toHaveBeenCalledTimes(2);
expect(jwtService.sign).toHaveBeenCalledWith({ id: userId }, { expiresIn: '12h' });
expect(jwtService.sign).toHaveBeenCalledWith(
{ id: userId },
{ secret: process.env.REFRESH_SECRET_KEY }
);
expect(mockUsersService.saveRefreshToken).toHaveBeenCalledWith(
userId,
mockToken.refreshToken
);
expect(response).toEqual(mockToken);
});
});
이미 로그아웃 상태인지 확인이 필요
성공적으로 로그아웃 절차를 거치는지 확인이 필요
describe('signOut', () => {
let userId: number;
beforeEach(async () => {
userId = 1;
});
it('If already sign out', async () => {
// GIVEN
const user = {
id: 1,
refreshToken: '',
};
mockUsersService.getUserById.mockResolvedValue(user);
// WHEN
const response = authService.signOut(userId);
// THEN
expect(response).rejects.toThrow(BadRequestException);
expect(response).rejects.toThrow('이미 로그아웃한 상태입니다.');
});
it('should sign out', async () => {
// GIVEN
const signOutResult = {
status: 201,
message: '로그아웃에 성공했습니다.',
};
mockUsersService.getUserById.mockResolvedValue(signOutResult);
// WHEN
const response = await authService.signOut(userId);
// THEN
expect(mockUsersService.getUserById).toHaveBeenCalledWith(userId);
expect(mockUsersService.deleteRefreshToken).toHaveBeenCalledWith(userId);
expect(response).toEqual(signOutResult);
});
});
로그인 절차와 거의 동일함
사용자 정보를 payload
에 담아서 토큰화 하는 작업이 필요
describe('reissue', () => {
let userId: number;
beforeEach(async () => {
userId = 1;
});
it('should reissue', async () => {
// GIVEN
const mockToken = {
accessToken: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...',
refreshToken: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...',
};
jest.spyOn(jwtService, 'sign').mockImplementation((payload, options) => {
if (options?.expiresIn) {
return mockToken.accessToken;
} else {
return mockToken.refreshToken;
}
});
// WHEN
const response = await authService.reissue(userId);
// THEN
expect(jwtService.sign).toHaveBeenCalledTimes(2);
expect(jwtService.sign).toHaveBeenCalledWith({ id: userId }, { expiresIn: '12h' });
expect(jwtService.sign).toHaveBeenCalledWith(
{ id: userId },
{ secret: process.env.REFRESH_SECRET_KEY }
);
expect(mockUsersService.saveRefreshToken).toHaveBeenCalledWith(
userId,
mockToken.refreshToken
);
expect(response).toEqual(mockToken);
});
});
import { Test, TestingModule } from '@nestjs/testing';
import { AuthService } from './auth.service';
import { UsersService } from 'src/users/users.service';
import { JwtService } from '@nestjs/jwt';
import { Repository } from 'typeorm';
import { getRepositoryToken } from '@nestjs/typeorm';
import { User } from 'src/users/entities/user.entity';
import { SignUpDto } from './dto/sign-up.dto';
import { BadRequestException, ConflictException } from '@nestjs/common';
import * as bcrypt from 'bcrypt';
// 모킹된 bcrypt의 메서드 정의
jest.mock('bcrypt', () => ({
hash: jest.fn(),
}));
const mockUsersService = {
getUserById: jest.fn(),
getUserByEmail: jest.fn(),
getUserByNickname: jest.fn(),
createUser: jest.fn(),
saveRefreshToken: jest.fn(),
deleteRefreshToken: jest.fn(),
};
const mockUsersRepository = () => ({
findOne: jest.fn(),
save: jest.fn(),
});
// Sign Up DTO
const signUpDto: SignUpDto = {
email: 'test12@test.com',
password: '123123',
passwordCheck: '123123',
nickname: 'Test12',
};
describe('AuthService', () => {
let authService: AuthService;
let usersService: UsersService;
let jwtService: JwtService;
let signUpDtoObject: SignUpDto;
beforeEach(async () => {
// 테스트 전에 임시 데이터 초기화
jest.clearAllMocks();
jest.resetAllMocks();
jest.restoreAllMocks();
const module: TestingModule = await Test.createTestingModule({
providers: [
AuthService,
{ provide: UsersService, useValue: mockUsersService },
JwtService,
{ provide: getRepositoryToken(User), useValue: mockUsersRepository() },
],
}).compile();
authService = module.get<AuthService>(AuthService);
usersService = module.get<UsersService>(UsersService);
jwtService = module.get<JwtService>(JwtService);
});
// 테스트 후에 임시 데이터 초기화
afterAll(async () => {
jest.clearAllMocks();
});
it('should be defined', async () => {
expect(authService).toBeDefined();
expect(usersService).toBeDefined();
expect(jwtService).toBeDefined();
});
describe('signUp', () => {
beforeEach(async () => {
// 해당 메서드의 매개변수
signUpDtoObject = signUpDto;
});
it('If not match password and passwordCheck', () => {
// GIVEN
const invalidSignUpDto = { ...signUpDto, passwordCheck: 'different' };
// WHEN
const response = authService.signUp(invalidSignUpDto);
// THEN
expect(response).rejects.toThrow(BadRequestException);
expect(response).rejects.toThrow('비밀번호가 일치하지 않습니다.');
});
it('If email conflict', async () => {
// GIVEN
mockUsersService.getUserByEmail.mockResolvedValueOnce({});
// WHEN
const response = authService.signUp(signUpDtoObject);
// THEN
expect(response).rejects.toThrow(ConflictException);
expect(response).rejects.toThrow('중복된 이메일입니다.');
});
it('If nickname conflict', async () => {
// GIVEN
mockUsersService.getUserByEmail.mockResolvedValueOnce(undefined);
mockUsersService.getUserByNickname.mockResolvedValueOnce({});
// WHEN
const response = authService.signUp(signUpDtoObject);
// THEN
expect(response).rejects.toThrow(ConflictException);
expect(response).rejects.toThrow('중복된 닉네임입니다.');
});
it('should create user', async () => {
// GIVEN
const user = {
id: 1,
email: 'test12@test.com',
nickname: 'Test12',
profileImg: 'test_profile_image_url',
createdAt: '2024-07-05T23:08:07.001Z',
updatedAt: '2024-07-05T23:08:07.001Z',
};
mockUsersService.createUser.mockResolvedValue(user);
jest
.spyOn(bcrypt, 'hash')
.mockImplementation(() => Promise.resolve(signUpDtoObject.password));
// WHEN
const response = await authService.signUp(signUpDto);
// THEN
expect(mockUsersService.getUserByEmail).toHaveBeenCalledWith(signUpDtoObject.email);
expect(mockUsersService.getUserByNickname).toHaveBeenCalledWith(signUpDtoObject.nickname);
expect(response).toEqual({ ...user, password: undefined });
expect(mockUsersService.createUser).toHaveBeenCalledWith(
signUpDtoObject.email,
signUpDtoObject.password,
signUpDtoObject.nickname
);
});
});
describe('signIn', () => {
let userId: number;
beforeEach(async () => {
userId = 1;
});
it('should sign in', async () => {
// GIVEN
const mockToken = {
accessToken: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...',
refreshToken: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...',
};
jest.spyOn(jwtService, 'sign').mockImplementation((payload, options) => {
if (options?.expiresIn) {
return mockToken.accessToken;
} else {
return mockToken.refreshToken;
}
});
// WHEN
const response = await authService.signIn(userId);
// THEN
expect(jwtService.sign).toHaveBeenCalledTimes(2);
expect(jwtService.sign).toHaveBeenCalledWith({ id: userId }, { expiresIn: '12h' });
expect(jwtService.sign).toHaveBeenCalledWith(
{ id: userId },
{ secret: process.env.REFRESH_SECRET_KEY }
);
expect(mockUsersService.saveRefreshToken).toHaveBeenCalledWith(
userId,
mockToken.refreshToken
);
expect(response).toEqual(mockToken);
});
});
describe('signOut', () => {
let userId: number;
beforeEach(async () => {
userId = 1;
});
it('If already sign out', async () => {
// GIVEN
const user = {
id: 1,
refreshToken: '',
};
mockUsersService.getUserById.mockResolvedValue(user);
// WHEN
const response = authService.signOut(userId);
// THEN
expect(response).rejects.toThrow(BadRequestException);
expect(response).rejects.toThrow('이미 로그아웃한 상태입니다.');
});
it('should sign out', async () => {
// GIVEN
const signOutResult = {
status: 201,
message: '로그아웃에 성공했습니다.',
};
mockUsersService.getUserById.mockResolvedValue(signOutResult);
// WHEN
const response = await authService.signOut(userId);
// THEN
expect(mockUsersService.getUserById).toHaveBeenCalledWith(userId);
expect(mockUsersService.deleteRefreshToken).toHaveBeenCalledWith(userId);
expect(response).toEqual(signOutResult);
});
});
describe('reissue', () => {
let userId: number;
beforeEach(async () => {
userId = 1;
});
it('should reissue', async () => {
// GIVEN
const mockToken = {
accessToken: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...',
refreshToken: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...',
};
jest.spyOn(jwtService, 'sign').mockImplementation((payload, options) => {
if (options?.expiresIn) {
return mockToken.accessToken;
} else {
return mockToken.refreshToken;
}
});
// WHEN
const response = await authService.reissue(userId);
// THEN
expect(jwtService.sign).toHaveBeenCalledTimes(2);
expect(jwtService.sign).toHaveBeenCalledWith({ id: userId }, { expiresIn: '12h' });
expect(jwtService.sign).toHaveBeenCalledWith(
{ id: userId },
{ secret: process.env.REFRESH_SECRET_KEY }
);
expect(mockUsersService.saveRefreshToken).toHaveBeenCalledWith(
userId,
mockToken.refreshToken
);
expect(response).toEqual(mockToken);
});
});
});
각자가 맡았던 기능 구현의 마무리와 필요한 문서 작업들을 할 예정
API 명세서, Readme, PPT 자료, 시연 영상 등을 준비해야 함
나는 평소처럼 Readme를 작성할 예정
오늘 챌린지반에서 튜터님의 QnA 시간을 가짐
여기서 내가 작성한 질문은 테스트 코드에 대한 노하우 였음
매번 어떻게 짜야하고 어떤 구조로 짜고, 어느 정도까지 짜야 하는 지 알지 못해서 머리가 아픔
비즈니스 로직을 알아도 그게 테스트 코드로 만들어지지 않음...
마음 같아서는 다른 테스트 코드도 짜고 싶지만 프로젝트 기간이 짧아 그럴 시간이 없음
그래서 일단 Auth 부분만 최대한 공부하면서 작성함
이렇게 컨트롤러 + 서비스 하나의 세트로 테스트 코드를 만들었지만 사실 아직도 테스트 코드를 짜는 방법은 잘 모르겠음...
튜터님 말씀도 있었기 때문에 너무 매몰되지 않고 기간에 맞춰서 다른 작업들을 진행할 예정
기능 코드 상에서 비밀번호를 암호화 하기 위해서 bcrypt의 hash()를 사용함
같은 값을 암호화해도 시간에 따라서 조금씩 값이 달라짐
실제로 임의의 암호화된 비밀번호와 테스트 코드에서 암호화한 비밀번호는 원본 비밀번호가 같아도 계속 일치 하지 않다고 출력됨
찾아보니 테스트 코드에서는 실제 bcrypt의 hash()를 사용하는 것이 아니라 가짜 hash() 함수를 사용해야 함
그래서 jest.mock()를 이용해서 bcrypt를 모킹해서 hash()를 따로 정의해서 사용
// 모킹된 bcrypt의 메서드 정의
jest.mock('bcrypt', () => ({
hash: jest.fn(),
}));
it('should create user', async () => {
// GIVEN
const user = {
id: 1,
email: 'test12@test.com',
nickname: 'Test12',
profileImg: 'test_profile_image_url',
createdAt: '2024-07-05T23:08:07.001Z',
updatedAt: '2024-07-05T23:08:07.001Z',
};
mockUsersService.createUser.mockResolvedValue(user);
jest
.spyOn(bcrypt, 'hash')
.mockImplementation(() => Promise.resolve(signUpDtoObject.password));
// WHEN
const response = await authService.signUp(signUpDto);
// THEN
expect(mockUsersService.getUserByEmail).toHaveBeenCalledWith(signUpDtoObject.email);
expect(mockUsersService.getUserByNickname).toHaveBeenCalledWith(signUpDtoObject.nickname);
expect(response).toEqual({ ...user, password: undefined });
expect(mockUsersService.createUser).toHaveBeenCalledWith(
signUpDtoObject.email,
signUpDtoObject.password,
signUpDtoObject.nickname
);
});
});