Dandy Now!
  • Node.js + MySQL 기반 REST API 단위 테스트 실습 튜터리얼
    2025년 07월 26일 23시 27분 22초에 업로드 된 글입니다.
    작성자: DandyNow
    728x90
    반응형

    Node.js + MySQL 기반 REST API 단위 테스트 실습 튜터리얼

    1. 단위 테스트란?

    • 단위 테스트(Unit Test)는 애플리케이션의 가장 작은 단위(함수, 메서드 등)가 기대한 대로 동작하는지 독립적으로 검증하는 테스트이다.
    • 실제 DB, 네트워크 등 외부 시스템에 의존하지 않고, 함수의 로직만 집중적으로 테스트한다.

    2. 프로젝트 구조 예시

    myapp/
      ├── controller/
      │     └── products.js
      ├── models/
      │     └── Product.js
      ├── db.js
      ├── package.json
      └── test/
            └── unit/
                  └── products.test.js

    3. MySQL 데이터베이스 준비

    1. MySQL에서 테스트용 데이터베이스와 테이블을 생성한다.
    CREATE DATABASE testdb;
    USE testdb;
    CREATE TABLE products (
      id INT AUTO_INCREMENT PRIMARY KEY,
      name VARCHAR(255) NOT NULL,
      description TEXT NOT NULL
    );

    4. Node.js 코드 작성

    4-1. MySQL 연결 설정 (db.js)

    // db.js
    const mysql = require("mysql2/promise");
    
    const pool = mysql.createPool({
      host: "localhost",
      user: "root",
      password: "비밀번호", // 본인 환경에 맞게 수정
      database: "testdb",
      waitForConnections: true,
      connectionLimit: 10,
      queueLimit: 0,
    });
    
    module.exports = pool;

    4-2. Product 모델 (models/Product.js)

    // models/Product.js
    const db = require("../db");
    
    exports.create = async (product) => {
      const [result] = await db.query(
        "INSERT INTO products (name, description) VALUES (?, ?)",
        [product.name, product.description]
      );
      return { id: result.insertId, ...product };
    };
    
    exports.findAll = async () => {
      const [rows] = await db.query("SELECT * FROM products");
      return rows;
    };
    
    exports.findById = async (id) => {
      const [rows] = await db.query("SELECT * FROM products WHERE id = ?", [id]);
      return rows[0];
    };
    
    exports.update = async (id, product) => {
      await db.query("UPDATE products SET name = ?, description = ? WHERE id = ?", [
        product.name,
        product.description,
        id,
      ]);
      return { id, ...product };
    };
    
    exports.delete = async (id) => {
      await db.query("DELETE FROM products WHERE id = ?", [id]);
      return { id };
    };

    4-3. Product 컨트롤러 (controller/products.js)

    // controller/products.js
    const ProductModel = require("../models/Product");
    
    exports.createProduct = async (req, res, next) => {
      try {
        const product = await ProductModel.create(req.body);
        res.status(201).json(product);
      } catch (err) {
        next(err);
      }
    };
    
    exports.getProducts = async (req, res, next) => {
      try {
        const products = await ProductModel.findAll();
        res.status(200).json(products);
      } catch (err) {
        next(err);
      }
    };
    
    exports.getProductById = async (req, res, next) => {
      try {
        const product = await ProductModel.findById(req.params.id);
        if (!product) {
          res.status(404).json({ message: "Not found" });
          return;
        }
        res.status(200).json(product);
      } catch (err) {
        next(err);
      }
    };
    
    exports.updateProduct = async (req, res, next) => {
      try {
        const product = await ProductModel.update(req.params.id, req.body);
        res.status(200).json(product);
      } catch (err) {
        next(err);
      }
    };
    
    exports.deleteProduct = async (req, res, next) => {
      try {
        await ProductModel.delete(req.params.id);
        res.status(200).json({ message: "Deleted" });
      } catch (err) {
        next(err);
      }
    };

    5. 단위 테스트 코드 작성 (test/unit/products.test.js)

    5-1. 필요한 라이브러리 설치

    npm install --save-dev jest node-mocks-http

    5-2. 단위 테스트 코드

    // test/unit/products.test.js
    const productController = require("../../controller/products");
    const productModel = require("../../models/Product");
    const httpMocks = require("node-mocks-http");
    
    // 모델 함수 mock 처리
    productModel.create = jest.fn();
    productModel.findAll = jest.fn();
    productModel.findById = jest.fn();
    productModel.update = jest.fn();
    productModel.delete = jest.fn();
    
    let req, res, next;
    
    beforeEach(() => {
      req = httpMocks.createRequest();
      res = httpMocks.createResponse();
      next = jest.fn();
    });
    
    describe("Product Controller - Create", () => {
      it("should call ProductModel.create", async () => {
        req.body = { name: "테스트", description: "설명" };
        await productController.createProduct(req, res, next);
        expect(productModel.create).toHaveBeenCalledWith(req.body);
      });
    
      it("should return 201 and product data", async () => {
        const newProduct = { id: 1, name: "테스트", description: "설명" };
        productModel.create.mockResolvedValue(newProduct);
        req.body = { name: "테스트", description: "설명" };
        await productController.createProduct(req, res, next);
        expect(res.statusCode).toBe(201);
        expect(res._getJSONData()).toStrictEqual(newProduct);
      });
    
      it("should handle errors", async () => {
        const error = new Error("DB Error");
        productModel.create.mockRejectedValue(error);
        req.body = { name: "테스트", description: "설명" };
        await productController.createProduct(req, res, next);
        expect(next).toHaveBeenCalledWith(error);
      });
    });
    
    describe("Product Controller - Get All", () => {
      it("should call ProductModel.findAll", async () => {
        await productController.getProducts(req, res, next);
        expect(productModel.findAll).toHaveBeenCalled();
      });
    
      it("should return 200 and products array", async () => {
        const products = [
          { id: 1, name: "A", description: "descA" },
          { id: 2, name: "B", description: "descB" },
        ];
        productModel.findAll.mockResolvedValue(products);
        await productController.getProducts(req, res, next);
        expect(res.statusCode).toBe(200);
        expect(res._getJSONData()).toStrictEqual(products);
      });
    
      it("should handle errors", async () => {
        const error = new Error("DB Error");
        productModel.findAll.mockRejectedValue(error);
        await productController.getProducts(req, res, next);
        expect(next).toHaveBeenCalledWith(error);
      });
    });
    
    describe("Product Controller - Get By Id", () => {
      it("should call ProductModel.findById", async () => {
        req.params.id = 1;
        await productController.getProductById(req, res, next);
        expect(productModel.findById).toHaveBeenCalledWith(1);
      });
    
      it("should return 200 and product data", async () => {
        const product = { id: 1, name: "A", description: "descA" };
        productModel.findById.mockResolvedValue(product);
        req.params.id = 1;
        await productController.getProductById(req, res, next);
        expect(res.statusCode).toBe(200);
        expect(res._getJSONData()).toStrictEqual(product);
      });
    
      it("should return 404 if not found", async () => {
        productModel.findById.mockResolvedValue(undefined);
        req.params.id = 999;
        await productController.getProductById(req, res, next);
        expect(res.statusCode).toBe(404);
        expect(res._getJSONData()).toStrictEqual({ message: "Not found" });
      });
    
      it("should handle errors", async () => {
        const error = new Error("DB Error");
        productModel.findById.mockRejectedValue(error);
        req.params.id = 1;
        await productController.getProductById(req, res, next);
        expect(next).toHaveBeenCalledWith(error);
      });
    });
    
    describe("Product Controller - Update", () => {
      it("should call ProductModel.update", async () => {
        req.params.id = 1;
        req.body = { name: "수정", description: "수정설명" };
        await productController.updateProduct(req, res, next);
        expect(productModel.update).toHaveBeenCalledWith(1, req.body);
      });
    
      it("should return 200 and updated product", async () => {
        const updated = { id: 1, name: "수정", description: "수정설명" };
        productModel.update.mockResolvedValue(updated);
        req.params.id = 1;
        req.body = { name: "수정", description: "수정설명" };
        await productController.updateProduct(req, res, next);
        expect(res.statusCode).toBe(200);
        expect(res._getJSONData()).toStrictEqual(updated);
      });
    
      it("should handle errors", async () => {
        const error = new Error("DB Error");
        productModel.update.mockRejectedValue(error);
        req.params.id = 1;
        req.body = { name: "수정", description: "수정설명" };
        await productController.updateProduct(req, res, next);
        expect(next).toHaveBeenCalledWith(error);
      });
    });
    
    describe("Product Controller - Delete", () => {
      it("should call ProductModel.delete", async () => {
        req.params.id = 1;
        await productController.deleteProduct(req, res, next);
        expect(productModel.delete).toHaveBeenCalledWith(1);
      });
    
      it("should return 200 and message", async () => {
        productModel.delete.mockResolvedValue({ id: 1 });
        req.params.id = 1;
        await productController.deleteProduct(req, res, next);
        expect(res.statusCode).toBe(200);
        expect(res._getJSONData()).toStrictEqual({ message: "Deleted" });
      });
    
      it("should handle errors", async () => {
        const error = new Error("DB Error");
        productModel.delete.mockRejectedValue(error);
        req.params.id = 1;
        await productController.deleteProduct(req, res, next);
        expect(next).toHaveBeenCalledWith(error);
      });
    });

    6. 실행 방법

    1. MySQL에서 DB와 테이블을 생성한다.
    2. Node.js 프로젝트에 위 파일들을 생성한다.
    3. npm install --save-dev jest node-mocks-http로 테스트 환경을 구축한다.
    4. package.json에 아래와 같이 jest 스크립트를 추가한다.
    "scripts": {
      "test": "jest"
    }
    1. 아래 명령어로 테스트를 실행한다.
    npm test

    7. 마치며

    • 이 튜터리얼은 MySQL DB를 사용하는 Node.js API의 단위 테스트를 처음부터 끝까지 실습할 수 있도록 구성하였다.
    • 단위 테스트에서는 실제 DB에 접근하지 않고, 모델 함수만 mock 처리하여 컨트롤러 로직을 독립적으로 검증한다.
    • 이 방식을 익히면, 다양한 DB 환경에서도 안정적이고 빠른 단위 테스트를 작성할 수 있다.

    VSCode Extention Jest Runner(firsttris)를 이용하면 편리한 테스트 환경을 구성할 수 있다.


    728x90
    반응형
    댓글