signUp service code

@Transactional()
  async signUp(signUpRequestDto: SignUpRequestDto): Promise<any> {
    const { email, name, password } = signUpRequestDto;
    const user = await this.userRepository.findOneUserByEmailLockMode(email);
    if (user) {
      throw new ConflictException('The email is already in use');
    }
    const saltRounds = this.configService.get<string>('SALT_ROUNDS');
    if (saltRounds === undefined) {
      throw new Error('SALT_ROUNDS is not defined in the configuration.');
    }
    const hashedPassword = await bcrypt.hash(password, parseInt(saltRounds));

    const newUserEntity = new User({ name, email, password: hashedPassword });
    const newUser = await this.userRepository.signUp(newUserEntity);
    return new SignUpResponseDto({ ...newUser });
  }

 

고려해야 할 것 들이 많아서 한참 고생을 했다.

다음에 참고하기 위해서 기록을 남김

 

목적

  • signUp 안에 구현된 코드들이 원하는대로 작동하는 가만 확인하기

테스트시 확인해야 하는 사항들

  • configService.get
    • 'SALT_ROUNDS' 를 정상적으로 받는가?
    • undefined 이면 에러를 throw 하는가?
  • bcrypt
    • password, parseInt(saltRounds) 변수를 받아서 사용하고 있는가?
    • hashedPassword 를 잘 반환하는가?
  • userRepository
    • 중복되는 email 이 있을시 에러를 throw 하는가?

 

bcrypt mocking

import * as bcrypt from 'bcrypt';

// import 한 함수를 중 사용할 method 를 테스트하기위해서 mocking
jest.mock('bcrypt', () => ({
  hash: jest.fn(),
}));

// 값을 반환 받기 위한 코드
jest.spyOn(bcrypt, 'hash').mockImplementation(async () => 'hashedpassword');

 

transactional 을 사용하고 있기 때문에 이 부분을 mocking

jest.mock('typeorm-transactional', () => ({
  Transactional: () => jest.fn(),
  initializeTransactionalContext: jest.fn(),
}));

 

 .env 파일을 읽어오는 configureService 를 mocking

const mockConfigService = {
  get: jest.fn(),
};

// 원하는 값이 없는 경우
configService.get.mockReturnValue(undefined);

// 원하는 값이 있는 경우
const saltRounds = '10';
configService.get.mockReturnValue(saltRounds);

 

 

 

 

완성된 코드

import { UserService } from '../application/user.service';
import { UserRepository } from '../domain/user.repository';
import { Test, TestingModule } from '@nestjs/testing';
import { USER_REPOSITORY } from '../../common/const/inject.constant';
import { SignUpRequestDto } from '../dto/signUp.request.dto';
import { ConfigService } from '@nestjs/config';
import { AuthService } from '../../auth/application/auth.service';
import { User } from '../domain/User.entity';
import * as bcrypt from 'bcrypt';
import { ConflictException } from '@nestjs/common';

jest.mock('typeorm-transactional', () => ({
  Transactional: () => jest.fn(),
  initializeTransactionalContext: jest.fn(),
}));

jest.mock('bcrypt', () => ({
  hash: jest.fn(),
}));

const mockUserRepository: jest.Mocked<UserRepository> = {
  signUp: jest.fn(),
  findOneUserByEmailLockMode: jest.fn(),
  findOneUserByEmail: jest.fn(),
  findOneUserById: jest.fn(),
  softDeleteUser: jest.fn(),
};

const mockConfigService = {
  get: jest.fn(),
};

describe('UserRepository', () => {
  let userRepository: jest.Mocked<typeof mockUserRepository>;
  let userService: UserService;
  let configService: jest.Mocked<typeof mockConfigService>;

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [
        UserService,
        {
          provide: USER_REPOSITORY,
          useValue: mockUserRepository,
        },
        {
          provide: ConfigService,
          useValue: mockConfigService,
        }
      ],
    }).compile();

    userService = module.get<UserService>(UserService);
    configService = module.get(ConfigService);
    userRepository = module.get(USER_REPOSITORY);
  });

  describe('signUp', () => {
    it('should throw ConflictException if email is already in use', async () => {
      const usedEmail = 'useremail@email.com';
      const signUpRequestDto: SignUpRequestDto = { name: 'tester', email: usedEmail, password: '12345678' };
      const { email, name, password } = signUpRequestDto;
      const user: User = new User({ email, name, password });
      user.id = 1;

      userRepository.findOneUserByEmailLockMode.mockResolvedValue(user);

      await expect(userService.signUp(signUpRequestDto)).rejects.toThrow(ConflictException);
    });

    it('should throw Error if saltRounds is not set in configService', async () => {
      const usedEmail = 'useremail@email.com';
      const signUpRequestDto: SignUpRequestDto = { name: 'tester', email: usedEmail, password: '12345678' };
      const { email, name, password } = signUpRequestDto;
      const user: User = new User({ email, name, password });
      user.id = 1;

      userRepository.findOneUserByEmailLockMode.mockResolvedValue(null);
      configService.get.mockReturnValue(undefined);

      await expect(userService.signUp(signUpRequestDto)).rejects.toThrow(Error);
    });

    it('should sign up a new user ', async () => {
      const signUpRequestDto: SignUpRequestDto = { name: 'tester', email: 'test@email.com', password: '12345678' };
      const { email, name, password } = signUpRequestDto;
      const newUser: User = new User({ email, name, password });
      newUser.id = 1;
      const saltRounds = '10';

      userRepository.findOneUserByEmailLockMode.mockResolvedValue(null);
      configService.get.mockReturnValue(saltRounds);
      jest.spyOn(bcrypt, 'hash').mockImplementation(async () => 'hashedpassword');
      userRepository.signUp.mockResolvedValue(newUser);

      const result = await userService.signUp(signUpRequestDto);
      console.log(result);
      expect(bcrypt.hash).toHaveBeenCalledWith(password, parseInt(saltRounds));
    });
  });
});

목차

 

실행 중이거나 중지된 모든 도커 컨테이너를 강제로 삭제

docker container rm -f $(docker container ls -aq)
  • docker container ls -aq :  모든 컨테이너의 ID를 나열
    • -a는 중지된 컨테이너를 포함하여 모든 컨테이너를 나열
    • -q는 컨테이너 ID만 출력
  • docker container rm -f  :  나열된 컨테이너 ID를 이용하여 해당 컨테이너들을 강제로 삭제
    • -f 옵션은 실행 중인 컨테이너도 강제로 삭제

특정 패턴이나 조건에 맞는 도커 이미지들을 삭제

docker image rm -f $(docker image ls -f reference='diamol/*' -q)
  • docker image ls -f reference='diamol/*' -q : 특정 패턴을 가진 이미지 ID 나열
    • docker image ls는 도커 이미지 목록을 나열
    • -f reference='diamol/*'   : 옵션은 이미지 이름이 'diamol/'로 시작하는 이미지만 필터링하여 나열하도록 지정
      • reference 필터를 사용하면 특정 패턴이나 조건에 맞는 이미지를 검색 가능
    • -q 옵션은 명령어의 출력을 이미지 ID만 나타내도록 제한, 다음 명령어에서 해당 이미지 ID를 사용하기 위한 설정

 

도커 이미지 빌드

cd dockerfile 이 있는 위치
docker image build -t <이미지 이름>

 

 

도커 네트워크 확인

# 네트워크 이름 리스트 확인
docker network ls

# nat 네트워크가 필요하다면 생성
docker network create nat

 

/^[a-zA-Z\uAC00-\uD7A3][a-zA-Z0-9\uAC00-\uD7A3\s]*[a-zA-Z0-9\uAC00-\uD7A3]$/
  • 문자열의 시작을 의미.
  • [a-zA-Z\uAC00-\uD7A3]  문자열이 반드시 영문자(대소문자) 또는 한글로 시작해야 함.
    • \uAC00-\uD7A3  한글
  • [a-zA-Z0-9\uAC00-\uD7A3\s]* 중간에는 영문자, 숫자, 한글, 또는 공백이 포함될 수 있음.
  • [a-zA-Z0-9\uAC00-\uD7A3]$: 문자열이 반드시 영문자, 숫자 또는 한글로 끝나야 함.
    • $: 문자열의 끝을 의미.

 

목차

런던파

주요 특징

  • Mock 사용: Mock 객체로 외부 의존성을 대체하여 단위 테스트를 독립적으로 수행
    • 특정 객체나 메서드의 동작을 검증하기 위해, 의존성을 실제 구현 대신 Mock 객체로 대체하여 테스트
  • 행동 기반 테스트 (Behavior Verification): 객체가 올바른 메서드를 호출했는지, 호출된 횟수는 적절한지 등을 확인
  • 격리성: 테스트 대상 객체는 의존성과 완전히 격리
  • 단위 테스트에 집중: 특정 기능이나 메서드의 동작을 작게 쪼개어 검증

장점

  • 다른 모듈/시스템의 변경에 영향을 받지 않음
  • 특정 메서드 호출이나 내부 동작 검증에 적합

단점

  • Mock이 많아지면 테스트 코드가 복잡해지고, 유지보수가 어려워질 수 있음
  • 실제 시스템과의 상호작용을 놓칠 가능성 있음
  • Mock 객체에 의존하여 테스트가 비현실적

고전파

주요 특징

  • 상태 기반 테스트 (State Verification): 테스트 결과로 반환된 상태나 값을 검증.
  • 실제 구현 사용: 의존성에 Mock을 사용하는 대신, 가능한 실제 구현체나 시스템을 사용
  • 결과 중심: 행동(메서드 호출)이 아닌 결과 상태(출력, 반환값 등)를 검증
  • 통합 테스트 지향: 시스템 전체 흐름을 테스트하는 데 적합

장점

  • 실제 의존성을 사용하기 때문에 현실에 가까운 테스트를 수행가능
  • 시스템 전반의 통합 및 작동 여부를 확인하기에 적합

단점

  • 외부 시스템의 변경에 민감하며, 테스트의 안정성이 떨어질 수 있음
  • 특정 메서드 호출이나 내부 동작을 검증하기 어려움
  • 의존성 때문에 테스트 속도가 느려질 수 있음

 

런던파와 고전파의 비교

  런던파 고전파
핵심 특징 Mock을 사용하여 독립적이고 행동 중심의 테스트 수행 실제 의존성을 사용하여 상태 중심의 테스트 수행
검증 방식 메서드 호출, 행동 검증 상태와 출력검증
테스트 범위 작은 단위 (메서드 단위) 통합 또는 큰 단위
속도 빠름 ( 외부 시스템 미사용) 느림(외부 시스템 사용 시)
테스트 안정성 외부 변화에 둔감 외부 시스템 변경시 취약
유지 보수성 Mock 이 많아지면 복잡 간결하지만 외부 의존성 변경시 취약
적용 사례 단위 테스트, 서비스 내부 로직 테스트 통합 테스트, 시스템 흐흠 검증
적합한 경우 테스트 속도가 중요할 때
객체의 행동을 검증할 경우
외부 의존성이 복잡하거나 불안정한 경우
전체 시스템의 실제 동작과 통합을 검증할 경우
현실적인 테스트 환경을 유지하려는 경우
주요 관심사가 결과 상태와 출력 일때

 

현대적인 테스트 접근 방식에서는 두 방법론을 혼합하여 사용하는 경우가 많으며, 테스트의 목적과 환경에 따라 적절히 선택하는 것이 중요

'TDD > 개념' 카테고리의 다른 글

[ TDD ] [ Fixture ] Test Fixture?  (0) 2024.11.16
@Post('path')
@UseGuards(JwtAuthGuard)
@HttpCode(201)
postRoutine(@Body() saveRoutines: SaveRoutinesRequestDto, @Request() req: any) {
return this.routineService.bulkInsertRoutines(req.user, saveRoutines);
}

 

주어진 코드에서 데코레이션이 정확하게 작동하는지 테스트하는 방법은

Reflect.getMetadata(metadataKey, target) 를 사용하면 된다.

 

사용방법

 

@UseGaurds(JwtAuthGuard)

import {JwtAuthGuard} from '../common/jwtPassport/'
import {GUARDS_METADATA} from '@nestjs/common/constants';

const guards = Reflect.getMetadata(GUARDS_METADATA, controller.postRoutine);
expect(guards[0]).toBe(JwtAuthGuard);
  • __guards__   NestJS에서 @UseGuards() 데코레이터를 사용하면, 해당 메서드에 가드가 설정됩니다. 이 정보는 __guards__라는 메타데이터 키로 저장됩니다.
  • __guards__ 를 이용해서 postRoutine 에서 사용된 @UseGaurds(JwtAuthGuard) 의 동작여부 확인

@HttpCode(201)

import {HTTP_CODE_METADATA} from '@nestjs/common/constants';

const httpCode = Reflect.getMetadata(HTTP_CODE_METADATA, controller.postRoutine);
expect(httpCode).toBe(201);
  • __httpCode__ : NestJS에서 @HttpCode() 데코레이터를 사용하면, 해당 메서드의 http 상태 코드가 설정됩니다.이 정보는 __httpCode__라는 메타데이터키로 저장됩니다.
  • __httpCode__ 를 이용해서 postRoutine 에서 사용된 @HttpCode(201) 의 동작여부를 확인

const path = Reflect.getMetadata(ROUTE_METADATA, controller.getAllRoutineByUser); const method = Reflect.getMetadata(METHOD_METADATA, controller.getAllRoutineByUser);

 

@Post('path')

import { PATH_METADATA, METHOD_METADATA } from '@nestjs/common/constants';
import { RequestMethod } from '@nestjs/common';


const path = Reflect.getMetadata(PATH_METADATA, controller.postRoutine);
const method = Reflect.getMetadata(METHOD_METADATA, controller.postRoutine);

expect(method).toBe(RequestMethod.POST);
expect(path).toBe('path');
  • METHOD_METADATA =  NestJS에서 HTTP 메서드를 나타내는 상수로, RequestMethod 열거형(enum) 값을 반환
export enum RequestMethod {
  GET = 0,
  POST = 1,
  PUT = 2,
  DELETE = 3,
  PATCH = 4,
  ALL = 5,
  OPTIONS = 6,
  HEAD = 7,
}

 

+ Recent posts