본문 바로가기

JavaScript

Node.js 심화 1주차_5_Unit Test

728x90

Node.js 심화 1주차_5_Unit Test


Layered Architecture Pattern 테스트 코드(설정)

 

단위 테스트(Unit Test), 통합 테스트(Integration Test)

 

📌 JEST 모듈 설치

npm i jest supertest -D

 

📄 jest.config.js

// jest.config.js

module.exports = {
    // 해당 패턴에 일치하는 경로가 존재할 경우 테스트를 하지 않고 넘어갑니다.
    "testPathIgnorePatterns": ["/node_modules/"],
    // 테스트 실행 시 각 TestCase에 대한 출력을 해줍니다.
    verbose: true,
  }

jest 옵션 설정해주기.

 

📄 package.json

  "scripts": {
    "test": "jest --forceExit",
    "test:silent": "jest --silent --forceExit",
    "test:coverage": "jest --coverage --forceExit",
    "test:unit": "jest __tests__/unit --forceExit",
    "test:integration": "jest __tests__/integration --forceExit"
  },

 

📌 Mock Functions

특정 코드를 실행하지 않고, 원하는 부분만을 테스트 하고 싶을 때 사용

실제로 존재하는 값처럼 사용할 수 있도록 만들어놓은 가짜 객체!


Layered Architecture Pattern 테스트 코드(단위테스트)

 

단위 테스트(Unit Test)

 

📌 의존성 주입(DI: Dependency Injection)

DB를 Mocking하여 실제 DB에 접근하지 않고 테스트 코드를 수행

Posts 모델을 모듈에서 가져와 바로 사용하는 것이 아닌, 외부에서 해당하는 모델주입하도록 변경

 

아래와 같은 방법은 의존성 주입을 구현하기 위한 여러가지의 방법 중 생성자 주입(Constructor Injection)을 이용하여 의존성을 주입

 

📄 repositories/posts.repository.js

// const { Posts } = require('../models');

class PostRepository {
  constructor(postsModel) {
    this.postsModel = postsModel;
  }

  // findAllPost = async () => {
  //   const posts = await Posts.findAll();

  //   return posts;
  // };

  findAllPost = async () => {
    const posts = await this.postsModel.findAll();

    return posts;
  };
  
}

생성자를 만들어 Server단에서 postsModel을 받아 Mocking!

 

📄 services/posts.services.js

const PostRepository = require('../repositories/posts.repository');
const { Posts } = require("../models/index");

class PostService {
  postRepository = new PostRepository(Posts);
}

server단에서 Posts 객체를 넘겨줌!


📌 Repository Layer 단위 테스트 (UnitTest)

Repository의 하위 계층 : DB

DB에 접근해 실제 데이터를 건드리지 않고 DB를 Mocking

 

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

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

const PostRepository = require("../../repositories/posts.repository");


// posts.repository.js 에서는 아래 5개의 Method만을 사용합니다.
let mockPostsModel = {
  findAll: jest.fn(),
  findByPk: jest.fn(),
  create: jest.fn(),
  update: jest.fn(),
  destroy: jest.fn(),
}

repositories

모델에 접근하는 함수들

 

 

 

📄 __tests__/unit/posts.repository.unit.spec.js [findAll 단위테스트]

  test('Posts Repository findAllPost Method', async () => {
    mockPostsModel.findAll = jest.fn(() => {
        return "findAll Result";
    });

    const posts = await postRepository.findAllPost();

    // postsModel에 있는 findAll Method는 1번만 실행된다.
    expect(mockPostsModel.findAll).toHaveBeenCalledTimes(1);

    // postsModel에 있는 findAll Method의 결과값이 바로 Return 되어야 한다.
    expect(posts).toEqual("findAll Result");
  });
  • 값을 제대로 반환하는지(findAllPost 는 posts값을 반환하므로)
  • 한번만 실행되는지

 

📄 __tests__/unit/posts.repository.unit.spec.js [create 단위테스트]

  test('Posts Repository createPost Method', async () => {
    mockPostsModel.create = jest.fn(() => {
        return "Hello Create Result";
    });

    // nickname, password, title, content
    const createPostParams = {
        nickname: "createPostNickname", 
        password: "createPostPassword", 
        title: "createPostTitle", 
        content: "createPostContent"
    }

    const createPostData = await postRepository.createPost(
        createPostParams.nickname,
        createPostParams.password,
        createPostParams.title,
        createPostParams.content
    );

    // postModel.create method의 결과값은 createPostData (method의 실행한 결과값) 변수와 일치한다.
    expect(createPostData).toEqual("Hello Create Result");

    // postModel.create method는 한번 호출된다.
    expect(mockPostsModel.create).toHaveBeenCalledTimes(1);

    // postModel.create method 호출할 때, {nickname, password, title, content} 객체 형태인지
    expect(mockPostsModel.create).toHaveBeenCalledWith({
        nickname: createPostParams.nickname,
        password: createPostParams.password,
        title: createPostParams.title,
        content: createPostParams.content
    })
    
  });
  • 값을 제대로 반환하는지(createPost함수가 createPostData를 반환하므로)
  • 한번만 실행되는지
  • 객체형태(key:value)의 값을 전달받는지

 

📌 UnitTest(Terminal)

npm run unit:test

package.json에 scripts에 설정해뒀기 때문에 위와 같은 shell 명령어를 입력했을 때,

unitTest를 할 수 있음!

repo unit test


📌 Server Layer 단위 테스트 (UnitTest)

 

📄 __tests__/unit/posts.server.unit.spec.js [findAll 단위테스트]

  test('Posts Service findAllPost Method', async () => {
    const findAllPostReturnValue = [
        {
            postId: 1,
            nickname: "Nickname_1",
            title: "Title_1",
            createdAt: new Date("11 October 2022 00:00"),
            updatedAt: new Date("11 October 2022 00:00"),
        },
        {
            postId: 2,
            nickname: "Nickname_2",
            title: "Title_2",
            createdAt: new Date("12 October 2022 00:00"),
            updatedAt: new Date("12 October 2022 00:00"),
        }
    ];

    mockPostsRepository.findAllPost = jest.fn(() =>{
        return findAllPostReturnValue;
    });

    const allPost = await postService.findAllPost();

    expect(allPost).toEqual(
        findAllPostReturnValue.sort((a,b) => {
            return b.createdAt - a.createdAt;
        })
    );

    expect(mockPostsRepository.findAllPost).toHaveBeenCalledTimes(1);
  });
  • 값을 반환할 때, 정렬기준 맞는지
  • 한번마 실행되는지

 

📄 __tests__/unit/posts.server.unit.spec.js [delete 단위테스트]

  test('Posts Service deletePost Method By Success', async () => {
    const findPostByIdReturnValue = {
        postId: 1,
        nickname: "Nickname_1",
        title: "Title_1",
        content: "Content_1",
        createdAt: new Date("11 October 2022 00:00"),
        updatedAt: new Date("11 October 2022 00:00"),
    }

    mockPostsRepository.findPostById = jest.fn(() => {
        return findPostByIdReturnValue;
    })

    const deletePost = await postService.deletePost(1, "0000");

    // findPostById Method를 1번 호출한다. 입력받은 인자는 postId이다.
    expect(mockPostsRepository.findPostById).toHaveBeenCalledTimes(1);
    expect(mockPostsRepository.findPostById).toHaveBeenCalledWith(1);

    // postId, password deletePost Method가 호출된다.
    expect(mockPostsRepository.deletePost).toHaveBeenCalledTimes(1);
    expect(mockPostsRepository.deletePost).toHaveBeenCalledWith(1, "0000");

    // Return된 결과값이, findPostById의 반환된 결과값과 일치한다.
    expect(deletePost).toMatchObject({
        postId: findPostByIdReturnValue.postId,
        nickname: findPostByIdReturnValue.nickname,
        title: findPostByIdReturnValue.title,
        content: findPostByIdReturnValue.content,
        createdAt: findPostByIdReturnValue.createdAt,
        updatedAt: findPostByIdReturnValue.updatedAt,
    })
  });
  • postId의 값을 찾는 걸 한번만 실행하는지
  • 값을 찾을 때 postId(integer)값을 전달 받는지
  • postId 값의 데이터를 삭제할 때 한번만 실행되는지
  • 삭제할 때 postId(integer)와 비밀번호(string) 값을 전달 받는지
  • 값을 삭제하면서 삭제할 데이터 값(객체)를 return 하는지

 

📄 __tests__/unit/posts.server.unit.spec.js [delete error 단위테스트]

  test('Posts Service deletePost Method By Not Found Post Error', async () => {
    const findPostByIdReturnValue = null;

    mockPostsRepository.findPostById = jest.fn(() => {
        return findPostByIdReturnValue;
    });

    try{
        const deletePost = await postService.deletePost(90, "1234");
    }catch(error){
        // postId를 입력한 findPostById Method 실행, 1번 호출
        expect(mockPostsRepository.findPostById).toHaveBeenCalledTimes(1);
        expect(mockPostsRepository.findPostById).toHaveBeenCalledWith(90);

        // return 된 findPostById의 결과가 존재하지 않을 때 에러 발생
        expect(error.message).toEqual("Post doesn't exist");
    }
  });

찾는 값이 없다는 전제하에, 오류를 발생시킨다.

  • 오류가 한번 실행되는지
  • 오류를 발생할때 postId(integer) 값 전달 받는지
  • 에러메세지가 올바른지

📌 Controller 단위 테스트 (UnitTest)

 

Controller의 하위 계층 : Server, request, response

하위계층을 Mocking

 

📄 __tests__/unit/posts.controller.unit.spec.js [getPosts 단위테스트]

  test('Posts Controller getPosts Method by Success', async () => {
    const findAllPostReturnValue = [
        {
            postId: 1,
            nickname: "Nickname_1",
            title: "Title_1",
            createdAt: new Date("11 October 2022 00:00"),
            updatedAt: new Date("11 October 2022 00:00"),
        },
        {
            postId: 2,
            nickname: "Nickname_2",
            title: "Title_2",
            createdAt: new Date("12 October 2022 00:00"),
            updatedAt: new Date("12 October 2022 00:00"),
        }
    ];

    mockPostService.findAllPost = jest.fn(() => {
        return findAllPostReturnValue;
    })

    await postsController.getPosts(mockRequest, mockResponse); //return 값이 없음

    // findAllPost Method 1번 호출되었는가
    expect(mockPostService.findAllPost).toHaveBeenCalledTimes(1);

    // Response.status가 200으로 정상 전달되었는가
    expect(mockResponse.status).toHaveBeenCalledTimes(1);
    expect(mockResponse.status).toHaveBeenCalledWith(200);

    // Response.json이 {data:posts}의 형태로 정상 전달 되었는가
    expect(mockResponse.json).toHaveBeenCalledWith({
        data: findAllPostReturnValue,
    });
  });

 

📄 __tests__/unit/posts.controller.unit.spec.js [createPost 단위테스트]

  test('Posts Controller createPost Method by Success', async () => {
    const createPostBodyParams = {
        nickname: "Nickname_Succese",
        password: "Password_Succese",
        title: "Title_Success",
        content: "Content_Success",
    };
    mockRequest.body = createPostBodyParams;

    const createPostReturnValue = {
        postId: 1,
        nickname: "Nickname_1",
        title: "Title_1",
        content: "Content_1",
        createdAt: new Date().toString(),
        updatedAt: new Date().toString(),
    }
    mockPostService.createPost = jest.fn(() => {
        return createPostReturnValue;
    })

    await postsController.createPost(mockRequest, mockResponse);

    // 리퀘스트에 있는 body데이터가 정상적으로 createPost에 전달되었는가
    expect(mockPostService.createPost).toHaveBeenCalledTimes(1);
    expect(mockPostService.createPost).toHaveBeenCalledWith(
        createPostBodyParams.nickname,
        createPostBodyParams.password,
        createPostBodyParams.title,
        createPostBodyParams.content
    );

    // mockResponse.json을 호출하는데, createPost의 Return Value가 맞는가
    expect(mockResponse.json).toHaveBeenCalledTimes(1);
    expect(mockResponse.json).toHaveBeenCalledWith({data: createPostReturnValue});

    // mockResponse.status가 201로 정상 전달 되었는가
    expect(mockResponse.status).toHaveBeenCalledWith(201);
  });

 

📄 __tests__/unit/posts.controller.unit.spec.js [createPost Error단위테스트]

  test('Posts Controller createPost Method by Invalid Params Error', async () => {
    mockRequest.body = {};

    await postsController.createPost(mockRequest, mockResponse);

    // mockResponse의 status가 400번
    expect(mockResponse.status).toHaveBeenCalledWith(400);

    // mockResponse.json이 {errorMessage : "InvalidParamsError"} 
    expect(mockResponse.json).toHaveBeenCalledWith({
        errorMessage : "InvalidParamsError"
    });


  });

📄 controllers/postscontroller.js

  createPost = async (req, res, next) => {
    try {
      const { nickname, password, title, content } = req.body;

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

      const createPostData = await this.postService.createPost(
        nickname,
        password,
        title,
        content
      );

      res.status(201).json({ data: createPostData });
    } catch (error) {
      res.status(400).json({ errorMessage: error.message });
    }
  };

'JavaScript' 카테고리의 다른 글

3차 미니프로젝트_회원가입  (0) 2022.12.30
Node.js 심화 1주차_5_Integration Test  (0) 2022.12.29
Node.js 심화 1주차_5  (0) 2022.12.29
Node.js 심화 1주차_4_과제  (0) 2022.12.28
Node.js 숙련주차 숙제 최종 제출  (0) 2022.12.28