[2024.06.12 TIL] 내일배움캠프 40일차 (Node.js 심화 강의, 테스트 코드, Jest, 단위 테스트)

My_Code·2024년 6월 12일
0

TIL

목록 보기
52/113
post-thumbnail

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


💻 TIL(Today I Learned)

📌 Today I Done

✏️ 테스트 코드 (Test Code)


✏️ 테스트 코드의 종류

  • 단위 테스트 (Unit Test) : 가장 작은 규모의 기능을 테스트 ex)서비스 단위

  • 통합 테스트 (Integration Test) : 다양한 기능을 합쳤을 때 생기는 문제를 방지하기 위한 테스트 ex) 컨트롤러 + 서비스 단위

  • E2E 테스트 (End-to-End Test) : 종단 간을 의미하는 End to End 테스트 ex) 백엔드부터 웹페이지까지


✏️ Jest 설치 및 설정

  • 1) jest, cross-env, @jest/globals 모듈 설치
# DevDependencies로 jest, cross-env 를 설치합니다.
yarn add -D jest cross-env @jest/globals
  • 2) Jest Config 설정
// jest.config.js

export default {
  // 해당 패턴에 일치하는 경로가 존재할 경우 테스트를 하지 않고 넘어갑니다.
  testPathIgnorePatterns: ['/node_modules/'],
  // 테스트 실행 시 각 TestCase에 대한 출력을 해줍니다.
  verbose: true,
};
  • 3) Jest Script 설정
// package.json

{
  ...

  "scripts": {
    ...

    "test": "cross-env NODE_ENV=test NODE_OPTIONS=--experimental-vm-modules jest --forceExit",
    "test:silent": "cross-env NODE_ENV=test NODE_OPTIONS=--experimental-vm-modules jest --silent --forceExit",
    "test:coverage": "cross-env NODE_ENV=test NODE_OPTIONS=--experimental-vm-modules jest --coverage --forceExit",
    "test:unit": "cross-env NODE_ENV=test NODE_OPTIONS=--experimental-vm-modules jest __tests__/unit --forceExit"
  },

  ...
}
  • cross-env 모듈이란?
    • OS마다 다른 환경 변수 설정 방식을 일괄적으로 통일해주는 모듈
    • macOS는 NODE_ENV=test 형식으로 환경 변수를 설정하지만, Windows에서는 set NODE_ENV=test 로 설정해야 함

✏️ Jest CLI Options

  • --forceExit : 테스트 코드가 완료되었을 때, 강제로 Jest 종료 (서버와 Pirsma의 연결이 남아있어 테스트 코드가 종료되지 않을 때 사용)

  • --silent : 테스트 코드 실행 시, console.log와 같은 메세지 출력하지 않음

  • --coverage : 테스트 코드 완료 후 현재 프로젝트의 테스트 코드 완료 퍼센트를 출력

  • --verbose : 테스트 코드의 개별 테스트 결과를 출력해줌


✏️ 자주 사용하는 Jest 문법 정리하기

  • .mockReturnValue(value) : Mock 함수의 반환값을 설정

  • .toBe(value) : 입력받은 예상값과 결과값이 일치하는지 비교 (엄격하게 비교)

  • .toEqual(value) : 입력받은 예상값과 결과값이 일치하는지 비교

  • .toMatch(regexp | string) : 입력받은 예상값이 결과값과 같은지 검증

  • .toBeTruthy() : 결과값이 true인지 검증

  • .toBeInstanceOf(Class) : 입력받은 예상값과 Class가 동일한지 검증 (주로 Error 검증에 사용)

  • .toHaveProperty(keyPath, value?) : 입력받은 객체의 Key와 Value가 일치하는지 검증

  • .toMatchObject(object) : 입력받은 객체와 결과 객체가 일치하는지 검증

  • afterAll(fn, timeout) : 모든 test()가 완료된 이후에 수행

  • afterEach(fn, timeout) : 각 test()가 완료된 이후에 수행

  • beforeAll(fn, timeout) : 모든 test()가 실행되기 전에 수행

  • beforeEach(fn, timeout) : 각 test()가 실행되기 전에 수행


✏️ Mock Function

  • Mocking이란 모조품(Mock)을 만드는 것을 의미함

  • 함수나 객체, 클래스의 모조품을 만들어서 실행 흐름대로 돌아가게 만드는 것

  • 즉, 단위 테스트를 작성할 때, 해당 코드가 의존하는 부분을 가짜(mock)로 대체하는 기법

  • 테스트 하고싶은 기능이 다른 기능들과 엮여있을 경우(의존하는 경우) 정확한 테스트를 하기 힘들기 때문에 Mocking을 사용

  • 만약에 Repository Layer에서 테스트 코드를 실행할 때마다 DB에 접근하면 매번 새로운 데이터가 생성, 수정, 삭제되어 리소스가 낭비됨

  • 그래서 실제 DB에 접근하지 않아도 동일한 상황을 예상하기 위해 DB에 접근하는 함수를 Mocking하면 간단하게 테스트가 가능함

  • .toHaveBeenCalledTimes(number) : Mock이 몇 번 호출되었는지 검증

  • .toHaveBeenCalledWith(arg1, arg2, ...) : 어떤 인자를 이용해 Mock이 호출되었는지 검증


✏️ 단위 테스트 - Prisma 의존성 주입하기

  • 의존성 주입(DI: Dependency Injection)은 객체 사이의 의존 관계를 외부에서 제공하는 방법을 의미함

  • 만약 아래와 같이 Prisma 클라이언트를 직접 사용하면 findMany와 같은 메서드로 직접 DB에 접근할 수 있음

// src/repositories/posts.repository.js

import { prisma } from '../utils/prisma/index.js';

export class PostsRepository {
  findAllPosts = async () => {
    // ORM인 Prisma에서 Posts 모델의 findMany 메서드를 사용해 데이터를 요청합니다.
    const posts = await prisma.posts.findMany();

    return posts;
  };

  ...
}
  • 그래서 Prisma 클라이언트에 직접 의존하는 게 아니라 외부에서 Mocking된 Prisma 클라이언트를 주입받아서 사용하면 됨

  • 이러한 행위를 의존성 주입이라고 하고, 밑의 예제는 생성자 주입이라는 방법을 통해서 작성된 예제임

  • 생성자 주입은 객체의 생성자(Constructor)를 호출할 때, 의존성을 전달하는 방식을 의미함

// src/repositories/posts.repository.js

export class PostsRepository {
  constructor(prisma) {
    // 생성자에서 전달받은 Prisma 클라이언트의 의존성을 주입합니다.
    this.prisma = prisma;
  }

  findAllPosts = async () => {
    // ORM인 Prisma에서 Posts 모델의 findMany 메서드를 사용해 데이터를 요청합니다.
    const posts = await this.prisma.posts.findMany();

    return posts;
  };

  ...
}
// src/services/posts.service.js

export class PostsService {
  constructor(postsRepository) {
    // 생성자에서 전달받은 PostsRepository 의존성을 주입합니다.
    this.postsRepository = postsRepository;
  }

  ...
}
// src/controllers/posts.controller.js

export class PostsController {
  constructor(postsService) {
    // 생성자에서 전달받은 PostsService의 의존성을 주입합니다.
    this.postsService = postsService;
  }

  ...
}
// src/routes/posts.router.js

import express from 'express';
import { prisma } from '../utils/prisma/index.js';
import { PostsRepository } from '../repositories/posts.repository.js';
import { PostsService } from '../services/posts.service.js';
import { PostsController } from '../controllers/posts.controller.js';

const router = express.Router();

// 3계층의 의존성을 모두 주입합니다.
const postsRepository = new PostsRepository(prisma);
const postsService = new PostsService(postsRepository);
const postsController = new PostsController(postsService);

...
  • 라우터들을 관리하는 최상단 계층인 posts.router.js에서 모든 계층의 의존성을 관리함

  • 단위 테스트를 할 때 주로 데이터베이스와 가까운 Repository Layer(Repository)부터 작성함


✏️ 단위 테스트 - Repository

  • Repository(저장소)는 데이터베이스 관리 및 데이터의 CRUD 작업을 담당함

  • Repository Layer가 사용하는 DB를 Mocking하여 실제 DB에 접근하지 않도록 테스트 코드를 아래와 같이 작성함

// src/repositories/posts.repository.js

// 실제로 데이터베이스에 접근해서 데이터를 CRUD하는 클래스
export class PostsRepository {
  constructor(prisma) {
    this.prisma = prisma;
  }

  // 모든 게시물 조회 메서드
  findAllPosts = async () => {
    const posts = await this.prisma.posts.findMany();

    return posts;
  };

...

  // 게시물 생성 메서드
  createPost = async (nickname, password, title, content) => {
    const createdPost = await this.prisma.posts.create({
      data: {
        nickname,
        password,
        title,
        content,
      },
    });

    return createdPost;
  };

...
// __tests__/unit/repositories/posts.repository.unit.spec.js

import { expect, jest } from '@jest/globals';
import { PostsRepository } from '../../../src/repositories/posts.repository.js';

// Prisma 클라이언트에서는 아래 5개의 메서드만 사용
let mockPrisma = {
  posts: {
    findMany: jest.fn(),
    findFirst: jest.fn(),
    create: jest.fn(),
    update: jest.fn(),
    delete: jest.fn(),
  },
};

let postsRepository = new PostsRepository(mockPrisma);

describe('Posts Repository Unit Test', () => {
  // 각 test가 실행되기 전에 실행
  beforeEach(() => {
    jest.resetAllMocks(); // 모든 Mock을 초기화
  });

  test('findAllPosts Method', async () => {
    /* 설정 부분 */
    // Mocking된 Prisma 클라이언트의 findMany 메서드의 반환값 (임시 결과값)
    const mockReturn = 'findMany String';
    // Mocking된 Prisma 클라이언트의 findMany 메서드가 실행됐을 때, 반환되는 값 설정
    mockPrisma.posts.findMany.mockReturnValue(mockReturn);

    /* 실행 부분, 실제 저장소(Repository)의  findAllPosts 메서드 실행 */
    const posts = await postsRepository.findAllPosts();

    /* 테스트(조건) 부분 */
    // 저장소(Repository)의 findAllPosts 메서드의 반환값 posts와
    // Mocking된 Prisma 클라이언트의 findMany 메서드의 반환값 mockReturn과 같은지 검사
    expect(posts).toBe(mockReturn);
    // Mocking된 Prisma 클라이언트의 findMany 메서드가 1번만 실행되는지 검사
    expect(postsRepository.prisma.posts.findMany).toHaveBeenCalledTimes(1);
  });

  test('createPost Method', async () => {
    /* 설정 부분 */
    // Mocking된 Prisma 클라이언트의 create 메서드의 반환값 (임시 결과값)
    const mockReturn = 'create Post Return String';
    // Mocking된 Prisma 클라이언트의 create 메서드가 실행됐을 때, 반환되는 값 설정
    mockPrisma.posts.create.mockReturnValue(mockReturn);

    // 저장소(Repository)의 createPost 메서드를 실행하기 위한 임시 전달 데이터 설정
    const createPostParams = {
      nickname: 'createPostNickname',
      password: 'createPostPassword',
      title: 'createPostTitle',
      content: 'createPostContent',
    };

    /* 실행 부분, 실제 저장소(Repository)의  createPost 메서드 실행 */
    const createPostData = await postsRepository.createPost(
      createPostParams.nickname,
      createPostParams.password,
      createPostParams.title,
      createPostParams.content
    );

    /* 테스트(조건) 부분 */
    // 저장소(Repository)의 createPost 메서드 반환값 createPostData와
    // Mocking된 Prisma 클라이언트의 create 메서드 반환값 mockReturn과 같은지 검사
    expect(createPostData).toEqual(mockReturn);
    // Mocking된 Prisma 클라이언트의 create 메서드는 1번만 실행되는지 검사
    expect(mockPrisma.posts.create).toHaveBeenCalledTimes(1);
    // Mocking된 Prisma 클라이언트의 create 메서드를 실행할 때
    // nickname, password, title, content 데이터를 전달하는 지 검사
    expect(mockPrisma.posts.create).toHaveBeenCalledWith({
      data: {
        nickname: createPostParams.nickname,
        password: createPostParams.password,
        title: createPostParams.title,
        content: createPostParams.content,
      },
    });
  });
});

✏️ 단위 테스트 - Service

  • Service Layer는 비즈니스 로직, 에러 핸들링과 같은 핵심적인 작업을 수행하는 곳

  • 데이터베이스의 데이터가 필요할 때는 Repository에게 요청함

  • 저장소(Repository)를 Mocking해서 원하는 비즈니스 로직이 동작하는지 테스트

  • 또한 고의로 에러를 발생시켜서 정상적으로 에러가 발생하는지도 테스트

// src/services/posts.service.js

// 핵심적인 비즈니스 로직을 수행하는 클래스
export class PostsService {
  constructor(postsRepository) {
    this.postsRepository = postsRepository;
  }

  // 모든 게시물 조회 메서드
  findAllPosts = async () => {
    const posts = await this.postsRepository.findAllPosts();

    // 게시글을 생성 날짜로 부터 내림차순 정렬
    posts.sort((a, b) => {
      return b.createdAt - a.createdAt;
    });

    // password, content를 뺀 상태로, Controller에게 Response를 전달
    return posts.map((post) => {
      return {
        postId: post.postId,
        nickname: post.nickname,
        title: post.title,
        createdAt: post.createdAt,
        updatedAt: post.updatedAt,
      };
    });
  };

...

  // 게시물 삭제 메서드
  deletePost = async (postId, password) => {
    // postId에 해당하는 게시물이 있는지 체크
    const post = await this.postsRepository.findPostById(postId);
    if (!post) throw new Error('존재하지 않는 게시글입니다.');

    await this.postsRepository.deletePost(postId, password);

    return {
      postId,
      nickname: post.nickname,
      title: post.title,
      content: post.content,
      createdAt: post.createdAt,
      updatedAt: post.updatedAt,
    };
  };
}
// __tests__/unit/services/posts.service.unit.spec.js

import { expect, jest } from '@jest/globals';
import { PostsService } from '../../../src/services/posts.service.js';

// PostsRepository는 아래의 5개 메서드만 지원
let mockPostsRepository = {
  findAllPosts: jest.fn(),
  findPostById: jest.fn(),
  createPost: jest.fn(),
  updatePost: jest.fn(),
  deletePost: jest.fn(),
};

// postsService의 Repository를 Mock Repository로 의존성을 주입
let postsService = new PostsService(mockPostsRepository);

describe('Posts Service Unit Test', () => {
  // 각 test가 실행되기 전에 실행
  beforeEach(() => {
    jest.resetAllMocks(); // 모든 Mock을 초기화
  });

  test('findAllPosts Method', async () => {
    /* 설정 부분 */
    // 저장소(Repository)의 findAllPosts 메서드의 반환값 (임시 결과값)
    const samplePosts = [
      {
        postId: 2,
        nickname: '홍길동1',
        title: '제목1 테스트입니다.',
        createdAt: '2024-06-11T03:58:33.994Z',
        updatedAt: '2024-06-11T03:58:33.994Z',
      },
      {
        postId: 3,
        nickname: '홍길동2',
        title: '제목2 테스트입니다.',
        createdAt: '2024-06-11T03:58:40.041Z',
        updatedAt: '2024-06-11T03:58:40.041Z',
      },
    ];
    // 저장소(Repository)의 findAllPosts 메서드가 실행됐을 때, 반환되는 값 설정
    mockPostsRepository.findAllPosts.mockReturnValue(samplePosts);

    /* 실행 부분, 실제 서비스(Service)의 findAllPosts 메서드 실행 */
    const allPosts = await postsService.findAllPosts();

    /* 테스트(조건) 부분 */
    // 서비스(Service)의 findAllPosts 메서드 반환값 allPosts와
    // 저장소(Repository)의 findAllPosts 메서드의 반환값 samplePosts의 정렬과 같은지 검사
    expect(allPosts).toEqual(
      samplePosts.sort((a, b) => {
        return b.createdAt - a.createdAt;
      })
    );
    // 저장소(Repository)의 findAllPosts 메서드가 1번만 실행되는지 검사
    expect(mockPostsRepository.findAllPosts).toHaveBeenCalledTimes(1);
  });

  test('deletePost Method By Success', async () => {
    /* 설정 부분 */
    // 저장소(Repository)의 deletePost 메서드의 반환값 (임시 결과값)
    const samplePost = {
      postId: 2,
      nickname: '홍길동1',
      password: '1234',
      title: '제목1 테스트입니다.',
      content: '테스트 코드용 내용입니다.',
      createdAt: '2024-06-11T03:58:33.994Z',
      updatedAt: '2024-06-11T03:58:33.994Z',
    };
    // 저장소(Repository)의 findPostById 메서드가 실행됐을 때, 반환되는 값 설정
    mockPostsRepository.findPostById.mockReturnValue(samplePost);

    /* 실행 부분, 실제 서비스(Service)의 deletePost 메서드 실행 */
    const deletedPost = await postsService.deletePost(2, '1234');

    /* 테스트(조건) 부분 */
    // 저장소(Repository)의 findPostById 메서드가 1번만 실행되는지 검사
    expect(mockPostsRepository.findPostById).toHaveBeenCalledTimes(1);
    // 저장소(Repository)의 findPostById 메서드를 실행할 때,
    // postId 데이터를 전달하는지 검사
    expect(mockPostsRepository.findPostById).toHaveBeenCalledWith(
      samplePost.postId
    );
    // 저장소(Repository)의 deletePost 메서드가 1번만 실행되는지 검사
    expect(mockPostsRepository.deletePost).toHaveBeenCalledTimes(1);
    // 저장소(Repository)의 deletePost 메서드를 실행할 때,
    // postId, password 데이터를 전달하는지 검사
    expect(mockPostsRepository.deletePost).toHaveBeenCalledWith(
      samplePost.postId,
      samplePost.password
    );
    // 서비스(Service)의 deletePost 메서드 반환값 deletedPost와 아래의 데이터 객체와 같은지 검사
    expect(deletedPost).toEqual({
      postId: samplePost.postId,
      nickname: samplePost.nickname,
      title: samplePost.title,
      content: samplePost.content,
      createdAt: samplePost.createdAt,
      updatedAt: samplePost.updatedAt,
    });
  });

  test('deletePost Method By Not Found Post Error', async () => {
    /* 설정 부분 */
    // 저장소(Repository)의 findPostById 메서드의 반환값 (임시 결과값)
    // 고의로 에러를 만들기 위해 null 값을 사용 (post를 찾지 못했을 때의 에러)
    const samplePost = null;
    // 저장소(Repository)의 findPostById 메서드가 실행됐을 때, 반환되는 값 설정
    mockPostsRepository.findPostById.mockReturnValue(samplePost);

    // 무조건 에러가 발생하기 때문에 try catch문으로 에러 이후를 테스트
    try {
      /* 실행 부분, 실제 서비스(Service)의 deletePost 메서드 실행 */
      await postsService.deletePost(123123, 'adsfasdf');
    } catch (err) {
      /* 테스트(조건) 부분 */
      // 저장소(Repository)의 findPostById 메서드가 1번만 실행되는지 검사
      expect(mockPostsRepository.findPostById).toHaveBeenCalledTimes(1);
      // 저장소(Repository)의 findPostById 메서드를 실행할 때,
      // postId 데이터를 전달하는지 검사
      expect(mockPostsRepository.findPostById).toHaveBeenCalledWith(123123);

      // 저장소(Repository)의 deletePost 메서드가 0번 실행되는지 검사
      expect(mockPostsRepository.deletePost).toHaveBeenCalledTimes(0);
      // err의 메세지가 '존재하지 않는 게시글입니다.'와 일치하는지 검사
      expect(err.message).toEqual('존재하지 않는 게시글입니다.');
    }
  });
});

✏️ 단위 테스트 - Controller

  • Controller는 API가 호출되었을 때 가장 처음으로 실행되는 계층

  • Controller는 클라이언트가 전달한 요청(Request)의 유효성 검사 및 데이터를 Service Layer로 전달하는 계층

  • Service Layer를 Mocking하여 독립적으로 테스트

// src/controllers/posts.controller.js

// 사용자의 입력을 받아서 Service로 넘기는 역할의 클래스
export class PostsController {
  // Post 서비스 클래스를 컨트롤러 클래스의 멤버 변수로 할당
  constructor(postsService) {
    this.postsService = postsService;
  }

  // 게시물 목록 조회 메서드
  getPosts = async (req, res, next) => {
    try {
      // 서비스 계층에 구현된 findAllPosts 로직을 실행합니다.
      const posts = await this.postsService.findAllPosts();

      return res.status(200).json({ data: posts });
    } catch (err) {
      next(err);
    }
  };

...

  // 게시물 생성 메서드
  createPost = async (req, res, next) => {
    try {
      const { nickname, password, title, content } = req.body;

      if (!nickname || !password || !title || !content) {
        throw new Error('InvalidParamsError');
      }

      // 서비스 계층에 구현된 createPost 로직을 실행합니다.
      const createdPost = await this.postsService.createPost(
        nickname,
        password,
        title,
        content
      );

      return res.status(201).json({ data: createdPost });
    } catch (err) {
      next(err);
    }
  };

...
// __tests__/unit/controllers/posts.controller.unit.spec.js

import { expect, jest } from '@jest/globals';
import { PostsController } from '../../../src/controllers/posts.controller.js';

// posts.service.js 에서는 아래 5개의 Method만을 사용
const mockPostsService = {
  findAllPosts: jest.fn(),
  findPostById: jest.fn(),
  createPost: jest.fn(),
  updatePost: jest.fn(),
  deletePost: jest.fn(),
};

// Request 요청 Mocking (req)
const mockRequest = {
  body: jest.fn(),
};

// Response 요청 Mocking (res)
const mockResponse = {
  status: jest.fn(),
  json: jest.fn(),
};

// Next 요청 Mocking (next)
const mockNext = jest.fn();

// postsController의 Service를 Mock Service로 의존성을 주입
const postsController = new PostsController(mockPostsService);

describe('Posts Controller Unit Test', () => {
  // 각 test가 실행되기 전에 실행
  beforeEach(() => {
    jest.resetAllMocks(); // 모든 Mock을 초기화

    // mockResponse.status의 경우 메서드 체이닝으로 인해 반환값이 Response(자신: this)로 설정
    mockResponse.status.mockReturnValue(mockResponse);
  });

  test('getPosts Method by Success', async () => {
    /* 설정 부분 */
    // 서비스(Service)의 findAllPosts 메서드의 반환값 (임시 결과값)
    const samplePosts = [
      {
        postId: 2,
        nickname: 'Nickname_2',
        title: 'Title_2',
        createdAt: new Date('07 October 2011 15:50 UTC'),
        updatedAt: new Date('07 October 2011 15:50 UTC'),
      },
      {
        postId: 1,
        nickname: 'Nickname_1',
        title: 'Title_1',
        createdAt: new Date('06 October 2011 15:50 UTC'),
        updatedAt: new Date('06 October 2011 15:50 UTC'),
      },
    ];
    // 서비스(Service)의 findAllPosts 메서드가 실행됐을 때, 반환되는 값 설정
    mockPostsService.findAllPosts.mockReturnValue(samplePosts);

    /* 실행 부분, 실제 컨트롤러(Controller)의 getPosts 메서드 실행 */
    await postsController.getPosts(mockRequest, mockResponse, mockNext);

    /* 테스트(조건) 부분 */
    // 서비스(Service)의 findAllPosts 메서드가 1번만 실행되는지 검사
    expect(mockPostsService.findAllPosts).toHaveBeenCalledTimes(1);

    // Mocking된 응답(Response) 객체의 status 메서드가 1번만 실행되는지 검사
    expect(mockResponse.status).toHaveBeenCalledTimes(1);
    // Mocking된 응답(Response) 객체의 status 메서드를 실행할 때, 200 상태코드가 전달되는지 검사
    expect(mockResponse.status).toHaveBeenCalledWith(200);

    // Mocking된 응답(Response) 객체의 json 메서드가 1번만 실행되는지 검사
    expect(mockResponse.json).toHaveBeenCalledTimes(1);
    //Mocking된 응답(Response) 객체의 json 메서드를 실행할 때, samplePosts를 전달하는지 검사
    expect(mockResponse.json).toHaveBeenCalledWith({
      data: samplePosts,
    });
  });

  test('createPost Method by Success', async () => {
    /* 설정 부분 */
    // 컨트롤러(Controller)의 createPost 메서드가 실행되기 위한 Body 입력 인자값 설정 (임시 입력값)
    const createPostRequestBodyParams = {
      nickname: 'Nickname_Success',
      password: 'Password_Success',
      title: 'Title_Success',
      content: 'Content_Success',
    };
    // 요청(Request)의 body에 입력할 인자값 설정
    mockRequest.body = createPostRequestBodyParams;

    // 서비스(Service)의 createPost 메서드 반환값 데이터 형식 설정 (임시 결과값)
    const createPostReturnValue = {
      postId: 1,
      ...createPostRequestBodyParams,
      createdAt: new Date().toString(),
      updatedAt: new Date().toString(),
    };
    // 서비스(Service)의 createPost 메서드가 실행되었을 때의 반환값 설정
    mockPostsService.createPost.mockReturnValue(createPostReturnValue);

    /* 실행 부분, 실제 컨트롤러(Controller)의 createPost 메서드 실행 */
    await postsController.createPost(mockRequest, mockResponse, mockNext);

    /* 테스트(조건) 부분 */
    // 서비스(Service)의 createPost 메서드가 1번만 실행되는지 검사
    expect(mockPostsService.createPost).toHaveBeenCalledTimes(1);
    // 서비스(Service)의 createPost 메서드를 실행할 때, req.body의 값들이 전달되는지 검사
    expect(mockPostsService.createPost).toHaveBeenCalledWith(
      createPostRequestBodyParams.nickname,
      createPostRequestBodyParams.password,
      createPostRequestBodyParams.title,
      createPostRequestBodyParams.content
    );

    // Response status 검증
    // Mocking된 응답(Response)의 status 메서드가 1번만 실행되는지 검사
    expect(mockResponse.status).toHaveBeenCalledTimes(1);
    // Mocking된 응답(Response)의 status 메서드를 실행할 때, 201 상태코드를 전달하는지 검사
    expect(mockResponse.status).toHaveBeenCalledWith(201);

    // Response json  검증
    // Mocking된 응답(Response)의 json 메서드가 1번만 실행되는지 검사
    expect(mockResponse.json).toHaveBeenCalledTimes(1);
    // Mocking된 응답(Response)의 json 메서드를 실행할 때,
    // data로 서비스(Service)의 createPost 메서드의 반환값이 전달되는지 검사
    expect(mockResponse.json).toHaveBeenCalledWith({
      data: createPostReturnValue,
    });
  });

  test('createPost Method by Invalid Params Error', async () => {
    /* 설정 부분 */
    // 요청(Request)의 body에 입력할 인자값 설정 (에러 확인용)
    mockRequest.body = {
      nickname: 'Nickname_InvalidParamsError',
      password: 'Password_InvalidParamsError',
    };

    /* 실행 부분, 실제 컨트롤러(Controller)의 createPost 메서드 실행 */
    await postsController.createPost(mockRequest, mockResponse, mockNext);

    /* 테스트(조건) 부분 */
    // Mocking된 Next 메서드를 실행했을 때, 에러 메세지가 전달되는지 검사
    expect(mockNext).toHaveBeenCalledWith(new Error('InvalidParamsError'));
  });
});


📌 Tomorrow's Goal

✏️ Node.js 심화 개인과제

  • 오늘 강의 정리가 끝났기에 오늘부터 계속 개인과제 구현을 할 예정

  • 우선 기존의 코드 일부분을 리팩토링 할 예정

  • 그 다음에 Layered Architecture Pattern를 적용해서 코드 모듈화 진행

  • 테스트 코드도 적용할 예정



📌 Today's Goal I Done

✔️ Node.js 심화 강의 시청

  • Layered Architecture Pattern과 테스트 코드에 대한 내용을 한 곳에서 정리하기에 너무 많아서 나눠서 정리함

  • 테스트 코드 부분은 생각보다 이해가 되지 않음

  • 특히 단위 테스트 코드 예제 작성 시 코드를 어떤 흐름으로 작성하는 지 감이 오지 않음

  • 일단은 코드마다 주석을 달아서 구조부터 이해해야겠음

  • 그래도 주석을 달면서 해석하다보니 각 Jest 메서드들의 사용방법을 조금 알게 되었음



⚠️ 구현 시 발생한 문제

✔️ JavaScript yarn SyntaxError: missing ) after argument list

// package.json
{
...
  "type": "module",
  "scripts": {
    "test": "node --experimental-vm-modules node_modules/.bin/jest"
  },
...
}
  • Jest의 test기능을 사용하기 위해서 위와 같이 작성하고 yarn test를 실행하니 다음과 같은 에러가 발생함
basedir=$(dirname "$(echo  | sed -e 's,\\,/,g')")
SyntaxError: missing ) after argument list
...
  • 인터넷에 찾아보니 이 스크립트에서 발생한 문제는 셸 스크립트 내에서 $(dirname ...) 표현식에 의한 문제라고 나왔음

  • 솔직히 무슨 말인지는 잘 모르겠지만 디렉터리 이름에서의 차이 때문에 생긴 것 같음

  • 해결 방법은 두 가지가 있음

  • 해결 방법 1) 셸 스크립트를 우회하는 방법

// package.json
{
...
  "type": "module",
  "scripts": {
    "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js"
  },
...
}
  • 해결 방법 2) cross-env 모듈을 이용하는 방법
# DevDependencies로 cross-env 를 설치
yarn add -D cross-env
// package.json
{
...
  "type": "module",
  "scripts": {
    "test": "test": "cross-env NODE_ENV=test NODE_OPTIONS=--experimental-vm-modules jest"
  },
...
}


📌 참고 자료

profile
조금씩 정리하자!!!

0개의 댓글