[2024.07.16 TIL] 내일배움캠프 64일차 (심화 플러스 팀프로젝트, 테스트 코드에 대한 궁금증, Auth 서비스 테스트 코드 구현)

My_Code·2024년 7월 16일
0

TIL

목록 보기
80/113
post-thumbnail

본 내용은 내일배움캠프에서 활동한 내용을 기록한 글입니다.


💻 TIL(Today I Learned)

📌 Today I Done

✏️ 테스트 코드에 대한 궁금증

Q : 테스트 코드에 대한 감이 없습니다. 인터넷에 찾아봐도 테스트 코드의 형식은 보여도 어떤 기준을 가지고 테스트 코드를 짜야 하는지 나와있지 않습니다. 대체 어디까지 테스트해야하고 가드나 인터셉터와 같은 것들도 테스트 해야 하나요?

  • 일단, 모든 로직을 전부 테스트할 필요가 없음

  • 중요도를 통해서 로직의 우선순위를 생각해야 함

  • 사이트 이펙트가 존재할 수 있으니 그에 대한 계산도 필요함

  • 사실상 가드나 인터셉터에 대한 TDD는 생각하지 않음

  • TDD에 너무 매몰되어서 다른 기능들에 소홀해지지 않도록 해야 함

  • 엣지 케이스와 커먼 케이스를 생각해야 함

  • 케이스에 대한 구멍이 있는 테스트 코드는 차라리 없는 게 좋음


✏️ 회원가입 Service 테스트 코드

  • 입력한 비밀번호와 확인용 비밀번호가 같은지 확인이 필요 (에러 테스트)

  • 입력한 이메일과 닉네임이 중복되는지 확인이 필요 (에러 테스트)

  • '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
      );
    });
  });

✏️ 로그인 Service 테스트 코드

  • 사용자 정보를 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);
    });
  });

✏️ 로그아웃 Service 테스트 코드

  • 이미 로그아웃 상태인지 확인이 필요

  • 성공적으로 로그아웃 절차를 거치는지 확인이 필요

  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);
    });
  });

✏️ 토큰 재발급 Service 테스트 코드

  • 로그인 절차와 거의 동일함

  • 사용자 정보를 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);
    });
  });

✏️ Auth Service 전체 테스트 코드

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);
    });
  });
});


📌 Tomorrow's Goal

✏️ 팀프로젝트 마무리

  • 각자가 맡았던 기능 구현의 마무리와 필요한 문서 작업들을 할 예정

  • API 명세서, Readme, PPT 자료, 시연 영상 등을 준비해야 함

  • 나는 평소처럼 Readme를 작성할 예정



📌 Today's Goal I Done

✔️ 서비스 테스트 코드 작성

  • 오늘 챌린지반에서 튜터님의 QnA 시간을 가짐

  • 여기서 내가 작성한 질문은 테스트 코드에 대한 노하우 였음

  • 매번 어떻게 짜야하고 어떤 구조로 짜고, 어느 정도까지 짜야 하는 지 알지 못해서 머리가 아픔

  • 비즈니스 로직을 알아도 그게 테스트 코드로 만들어지지 않음...

  • 마음 같아서는 다른 테스트 코드도 짜고 싶지만 프로젝트 기간이 짧아 그럴 시간이 없음

  • 그래서 일단 Auth 부분만 최대한 공부하면서 작성함

  • 이렇게 컨트롤러 + 서비스 하나의 세트로 테스트 코드를 만들었지만 사실 아직도 테스트 코드를 짜는 방법은 잘 모르겠음...

  • 튜터님 말씀도 있었기 때문에 너무 매몰되지 않고 기간에 맞춰서 다른 작업들을 진행할 예정



📌 ⚠️ 구현 시 발생한 문제

✔️ 테스트 코드에서 bcrypt hash 사용 시 매번 암호가 달라짐

  • 기능 코드 상에서 비밀번호를 암호화 하기 위해서 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
      );
    });
  });
profile
조금씩 정리하자!!!

0개의 댓글