Test

JEST

kimddakki 2022. 6. 10. 07:39
Test Driven Developement

 - 테스트 주도 개발은 매우 짧은 개발 사이클을 반복하는 소프트웨어 개발 프로세스 중 하나이다.

   개발자는 먼저 요구 사항을 검증하는 자동화된 테스트 케이스를 작성한다. 그런 후에, 그 테스트 케이스를 통과하기

   위한 최소한의 코드를 작성한다.

 

※ 장점.

 

1. 코드의 모듈화

 - 코드를 작성할 때 코드가 방대해지지 않고 모듈화가 이루어진다.

   따라서 리팩토링이 쉬워진다.

2. 디버깅 시간 단축

 - 구현한 기능이 요구사항을 충족하는지, 버그에 대한 낭비 시간도 줄여준다!

 


단위 테스트 Unit Test

 - 개발자가 수행하고 자신이 개발한 코드 단위를 테스트합니다. 소스 코드의 개별 단위를 테스트하며 사용 할 준비가

   되었는지 확인하는 테스트 방법

 - 개발 라이프 사이클의 초기 단계에서 버그가 식별되므로 버그 수정비용을 줄이는 데 도움이 된다.

 

⭕ 단위 테스트의 조건

1. 독립적이어야 한다. 어떤 테스트도 다른 테스트에 의존하지 않아야 한다.

2. 격리 되어야 한다. Ajax, Axios, LocalStorage 등 테스트 대상이 의존하는 것을 다른 것으로 대체해야 한다.

 

⭕ 단위 테스트를 하는 이유

1. 프로그램이 크고, 메모리가 많이 들거나 DB 등이 필요한 경우 로컬 환경에서 쉽게 코드를 실행시켜보기 어렵기 때문에

   유닛 테스트를 만들어 빠르게 자신의 코드가 정상적으로 작동하는 지 확인 할 수 있다.

 

2. 종속성이 있는 다른 클래스들에서 버그가 나는 것을 방지하기 위해서이다!

 

JEST

 - Facebook에 의해 만들어진 테스팅 프레임 워크

 - Test Case를 만들어 어플이 잘 돌아가는지 테스트 해준다.

 

// Package.json
  "scripts": {
    "test": "jest"
  }

⭕ Jest에서 Test 파일을 찾는 방법

1. __test__ 폴더 안에 있는 모든 파일들

2. filename.test.js

3. filename.spec.js

 

Jest 구조

⭕ describe : 말하다 묘사하다 서술하다

 - 여러 관련 테스트들을 그룹화하는 블록을 만든다.

 

⭕ It ( Test )

 - 개별 테스트를 수행하는 곳

 - 각 테스트를 작은 문장처럼 설명한다.

 

1. Expect: 예상하다.

 - 값을 테스트 할 때마다 사용 Matcher 와 짝을 이루어서 사용한다.

2. Matcher : 어울리다. 일치하다

 - 다른 방법으로 값을 테스트 하도록 Matcher를 사용

test("2+2 = 4", () => {
  expect(2 + 2).toBe(4);
});

 - Describe로 묶어 사용하기.

describe("Calculation", () => {
  test("two plus tow is foure ", () => {
    expect(2 + 2).toBe(4);
  });
  test("two plus tow is foure ", () => {
    expect(2 + 2).not.toBe(5);
  });
});

 

⭕ jest.fn ()

 - Mock 함수를 생성하는 함수, mock은 모의를 의미하며 단위 테스트를 작성할 때 해당 코드가 의존하는 부분을 가짜로

   대체해준다. ( 단위 테스트를 독립적으로 유지하게끔 해준다. )

 - 데이터 베이스에서 데이터를 삭제하는 코드를 사용한다고 가정하면 실제 데이터 베이스를 사용하는 경우 문제가 생길

   수 있다. 테스트가 인프라 환경에 영향을 받게 되므로 좋지 않다.

 

⭕ 순서

1. 구현해야 할 목록 생각 및 함수 생성 => 단위 테스트 작성 => 실제 코드 작성.

 

⭕ Create 단위 테스트 해보기

1. product.test.js 를 작성해 준다.

   ProductController의 createProduct가 함수인지 파악하는 테스트 이다.

const productController = require("../../controller/products");

describe("Product Controller Create가 함수인가", () => {
  it("함수일 것", () => {
    expect(typeof productController.createProduct).toBe("function");
  });
});

 

2. createProduct 함수를 호출 할 때 Product Model의 Create 메소드가 호출이 되는지 확인해보기

// controller/products.js

const productModel = require('../models/Product');

const createProduct = () => {
    productModel.create();
}

module.exports = {createProduct}
// products.test.js

const productController = require('../../controller/products');
const productModel = require('../../models/Product');

// mock함수
productModel.create = jest.fn();

describe("Product Controller Create", () => {
    it("should have a createProduct function", () => {
        // ProductController에 createProduct가 함수인지 파악하는것
        // 예상을 하면서 만드는것
        expect(typeof productController.createProduct).toBe("function");
    })
    it("should call ProductModel.create", () => {
        productController.createProduct();
        // productController의 createProduct가 실행될때
        // productModel의 create가 호출되는지 확인
        // DB에 직접적인 영향을 받으면 안되기 때문에 
        // mock함수인 jest.fn()을 확인한다. 
        expect(productModel.create).toBeCalled();
    })
})

 - 단위테스트는 독립적으로 이루어져야 하고 또한 실제 productModel에 영향을 받아 직접 호출되면 안되기 때문에

   jest.fn()을 활용한다.

 - 테스트를 통과 하지만 나오는 경고 문구로써

   몽구스를 사용할 때 나오는 경고 메시지이다. jest 기본 test 환경은 jsdom으로 되어 있는데 mongoose 는 jsdom을 

   지원하지 않으므로 경고 문구가 발생하게 된다.

   이에 따라 test 환경을 jsdom에서 node로 변경해 주면 된다.

   이를 위해 jest.config.js를 작성해 준다.

// jest.config.js

module.exports = {
    testEnvironment: "node"
}

 

⭕ node-mocks-http

 - 현재 Product.create()는 아직 저장할 Product 데이터를 넣어주지 않는다. 이제 DB에 저장할 데이터를 넣어준다.

 - 원래 몽구스 모델을 이용해 데이터를 저장할 때 Product.create(req.body) 등으로 http요청으로 함께 들어온 body를

   create 메소드에 인자로 넣어줘서 DB에 저장하는데 테스트환경에서는 위와같은 방식으로 진행할 수 없기 때문에

   node-mocks-http 라이브러리를 사용한다.  

 

 - 먼저 더미 데티러를 넣는 방식을 위해 json 형식의 더미 데이터를 만들어 준다.

// data/new-product.json

{
  "name": "Chicken",
  "description": "delicious",
  "price": 16000
}
// controller/product.js

const productModel = require('../models/Product');

const createProduct = (req, res, next) => {
    productModel.create(req.body);
}

module.exports = {createProduct}

 

 - httpMocks를 통해 request와 response를 작성하고 req.body안에 json으로 생성한 더미데이터를 넣어주었다.

 

⭕ beforeEach : 각자 앞에

 - 여러 개의 테스트에 공통된 Code가 있다면 beforeEach안에 넣어 반복을 줄여 줄 수 있다.

 - 해당 부분은 앞으로도 중복적으로 사용할 것이기에 beforeEach로 빼줄 수 있다.

const productController = require("../../controller/products");
const productModel = require("../../models/Product");

const httpMocks = require("node-mocks-http");
const newProduct = require("../data/new-product.json");

// mock 함수
productModel.create = jest.fn();

let req, res, next;

beforeEach(() => {
  req = httpMocks.createRequest();
  res = httpMocks.createResponse();
  next = null;
});

describe("프로덕트 컨트롤러 Create 함수", () => {
  beforeEach(() => {
    req.body = newProduct;
  });

  it("createProduct 함수일 것이다.", () => {
    // ProductController에 createProduct가 함수인지 파악하는것
    // 예상을 하면서 만드는것
    expect(typeof productController.createProduct).toBe("function");
  });

  it("프로덕트 컨트롤러의 크리에이트프로덕트 함수를 호출하면 프로덕트 모델의 크리에읕 함수가 호출 될 것이다.", () => {
    // req.body에 newProduct를 넣어준다.
    req.body = newProduct;
    productController.createProduct(req, res, next);
    // productController의 createProduct가 실행될 때
    // productModel의 create가 호출 되는지 확인한다.
    // DB에 직접적은 영향을 주면 안되기 때문에
    // mock 함수인 jest.fn()을 사용하여 확인한다.
    expect(productModel.create).toBeCalledWith(newProduct);
  });
});

 

⭕ 상태값 전달하기

 - 요청이 성공적으로 처리되는 경우 자원이 생성되었을 때 HTTP 응답 코드로써 201을 사용한다.

 - productController의 createProduct가 실제로 성공적으로 동작하게 되면 201 response Code를 반환해야 한다.

   이에 따라서 product.test.js에 201을 return 하는지 테스트하는 테스트 코드를 넣어준다.

 

 - 위 코드로 테스트를 하면 200이 return 되어 에러가 발생한다. 따라서 controller/product.js 또한 수정해줘야 한다. 

// controller/product.js
const productModel = require('../models/Product');

const createProduct = (req, res, next) => {
    productModel.create(req.body);
    res.status(201).send();
}

module.exports = {createProduct}
const productController = require("../../controller/products");
const productModel = require("../../models/Product");

const httpMocks = require("node-mocks-http");
const newProduct = require("../data/new-product.json");

// mock 함수
productModel.create = jest.fn();

let req, res, next;

beforeEach(() => {
  req = httpMocks.createRequest();
  res = httpMocks.createResponse();
  next = null;
});

describe("프로덕트 컨트롤러 Create 함수", () => {
  beforeEach(() => {
    req.body = newProduct;
  });

  it("createProduct 함수일 것이다.", () => {
    // ProductController에 createProduct가 함수인지 파악하는것
    // 예상을 하면서 만드는것
    expect(typeof productController.createProduct).toBe("function");
  });

  it("프로덕트 컨트롤러의 크리에이트프로덕트 함수를 호출하면 프로덕트 모델의 크리에읕 함수가 호출 될 것이다.", () => {
    // req.body에 newProduct를 넣어준다.
    req.body = newProduct;
    productController.createProduct(req, res, next);
    // productController의 createProduct가 실행될 때
    // productModel의 create가 호출 되는지 확인한다.
    // DB에 직접적은 영향을 주면 안되기 때문에
    // mock 함수인 jest.fn()을 사용하여 확인한다.
    expect(productModel.create).toBeCalledWith(newProduct);
  });

  // data를 성공적으로 create시 201 반환
  it("201 응답 코드를 반환할 것임", () => {
    productController.createProduct(req, res, next);
    expect(res.statusCode).toBe(201);
    expect(res._isEndCalled()).toBeTruthy();
  });
});

 

⭕ 에러 핸들링하기

// routes/products.js

const express = require("express");
const router = express.Router();
const { createProduct } = require("../controller/products");

router.post("/", createProduct);

module.exports = router;
// controller/products.js

const productModel = require('../models/Product');

const createProduct = (req, res, next) => {
    const createdProduct = productModel.create(req.body);
    console.log('createdProduct', createdProduct);
    return res.status(201).json(createdProduct);
}

module.exports = {createProduct}

 - 요청을 보내게 되면 pending 상태가 되기에 async await 비동기 코드를 사용해야 한다.

 -

// controller/products.js

const productModel = require('../models/Product');

const createProduct = async(req, res, next) => {
    const createdProduct = await productModel.create(req.body);
    console.log('createdProduct', createdProduct);
    return res.status(201).json(createdProduct);
}

module.exports = {createProduct}

 - 요청이 정상적으로 완료되었으므로 async await을 실제 코드에도 적용했다면 test code 또한 async await을 

   적용해야한다.

 

const productController = require("../../controller/products");
const productModel = require("../../models/Product");

const httpMocks = require("node-mocks-http");
const newProduct = require("../data/new-product.json");

// mock 함수
productModel.create = jest.fn();

let req, res, next;

beforeEach(() => {
  req = httpMocks.createRequest();
  res = httpMocks.createResponse();
  next = null;
});

describe("프로덕트 컨트롤러 Create 함수", () => {
  beforeEach(() => {
    req.body = newProduct;
  });

  it("createProduct 함수일 것이다.", () => {
    // ProductController에 createProduct가 함수인지 파악하는것
    // 예상을 하면서 만드는것
    expect(typeof productController.createProduct).toBe("function");
  });

  it("프로덕트 컨트롤러의 크리에이트프로덕트 함수를 호출하면 프로덕트 모델의 크리에읕 함수가 호출 될 것이다.", async () => {
    // req.body에 newProduct를 넣어준다.
    req.body = newProduct;

    await productController.createProduct(req, res, next);
    // productController의 createProduct가 실행될 때
    // productModel의 create가 호출 되는지 확인한다.
    // DB에 직접적은 영향을 주면 안되기 때문에
    // mock 함수인 jest.fn()을 사용하여 확인한다.
    expect(productModel.create).toBeCalledWith(newProduct);
  });

  // data를 성공적으로 create시 201 반환
  it("201 응답 코드를 반환할 것임", async () => {
    await productController.createProduct(req, res, next);
    expect(res.statusCode).toBe(201);
    expect(res._isEndCalled()).toBeTruthy();
  });

  it("응답 코드에 json body를 반환할 것임", async () => {
    productModel.create.mockReturnValue(newProduct);
    await productController.createProduct(req, res, next);
    // res json data가 newProduct와 일치하는지 여부
    expect(res._getJSONData()).toStrictEqual(newProduct);
  });
});

 

⭕ 요청이 정상적으로 오지 않았을 때의 경우를 처리해주어야 한다. try-catch를 활용해 에러 코드 방법을 나눠준다.

 

// controller/product.js

const productModel = require("../models/Product");

const createProduct = async (req, res, next) => {
  try {
    const createdProduct = await productModel.create(req.body);
    console.log("createdProduct", createdProduct);
    return res.status(201).json(createdProduct);
  } catch (error) {
    next(error);
  }
};

module.exports = { createProduct };

 - error 발생 시 error 메세지를 next를 통해 넘겨주는 방식이다.

const productController = require("../../controller/products");
const productModel = require("../../models/Product");

const httpMocks = require("node-mocks-http");
const newProduct = require("../data/new-product.json");

// mock 함수
productModel.create = jest.fn();

let req, res, next;

beforeEach(() => {
  req = httpMocks.createRequest();
  res = httpMocks.createResponse();
  next = jest.fn();
});

describe("프로덕트 컨트롤러 Create 함수", () => {
  beforeEach(() => {
    req.body = newProduct;
  });

  it("createProduct 함수일 것이다.", () => {
    // ProductController에 createProduct가 함수인지 파악하는것
    // 예상을 하면서 만드는것
    expect(typeof productController.createProduct).toBe("function");
  });

  it("프로덕트 컨트롤러의 크리에이트프로덕트 함수를 호출하면 프로덕트 모델의 크리에읕 함수가 호출 될 것이다.", async () => {
    // req.body에 newProduct를 넣어준다.
    req.body = newProduct;

    await productController.createProduct(req, res, next);
    // productController의 createProduct가 실행될 때
    // productModel의 create가 호출 되는지 확인한다.
    // DB에 직접적은 영향을 주면 안되기 때문에
    // mock 함수인 jest.fn()을 사용하여 확인한다.
    expect(productModel.create).toBeCalledWith(newProduct);
  });

  // data를 성공적으로 create시 201 반환
  it("201 응답 코드를 반환할 것임", async () => {
    await productController.createProduct(req, res, next);
    expect(res.statusCode).toBe(201);
    expect(res._isEndCalled()).toBeTruthy();
  });

  it("응답 코드에 json body를 반환할 것임", async () => {
    productModel.create.mockReturnValue(newProduct);
    await productController.createProduct(req, res, next);
    // res json data가 newProduct와 일치하는지 여부
    expect(res._getJSONData()).toStrictEqual(newProduct);
  });

  it("에러 반환 메시지 확인!!", async () => {
    const errorMessage = { message: "description property missing" };
    const rejectedPromise = Promise.reject(errorMessage);
    productModel.create.mockReturnValue(rejectedPromise);
    await productController.createProduct(req, res, next);
    expect(next).toBeCalledWith(errorMessage);
  });
});

 - next 부분을 jest.fn을활용해서 mock 함수로 넣어주었다. next에 인자를 넣어 넘겨주면 그 다음 부분의 미들웨어에서

   해당 next의 인자를 가지고 처리할 수 있게 된다.

 


 

TDD - Node 로 TTD하기 2 (단위테스트와 통합테스트)

github.com/loy124/node-express-tdd loy124/node-express-tdd express에서 jest와 supertest를 활용한 단위 테스트및 통합 테스트 . Contribute to loy124/node-express-tdd development by creating an account..

loy124.tistory.com