Day2 - Node-mocks-http & Error Handling

RINM·2024년 2월 16일

TDD (Node.js)

목록 보기
2/3

Node-mocks-http

단위 테스트에서 request, response 객체를 이용할 수 있게 해주는 모듈이다. createRequest로 컨트롤러가 받을 request를, createResponse로 response 객체를 생성할 수 있다. 이렇게 생성한 객체 안에 원하는 body를 넣는 것도 가능하다.
새로 req와 res 객체를 만든 뒤, 테스트 데이터로 req.body를 채워주고 createProduct를 호출할 때 인자로 제공해주자.

test("should call ProductModel.create", () => {
    let req = httpMocks.createRequest()
    let res = httpMocks.createResponse()
    let next = null
    req.body = newProduct
    productController.createProduct(req,res,next);
    expect(productModel.create).toBeCalledWith(newProduct);
})

toBeCalledWith matcher를 통해서 productModel의 create 함수가 newProduct를 인자로 호출되는지 확인할 수 있다. 테스트를 모두 통과하려면 컨트롤러가 다음과 같이 작성되어야 한다.

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

테스트 여러개를 실행하기 전에 공통적으로 실행하고 싶은 코드가 있다면 beforeEach로 묶어줄 수 있다. 예를 들어 위 코드에서 createProduct의 인자로 넘기기 위하여 req,res,next를 생성하는 부분은 다른 테스트에서도 여러번 사용할 수 있다. describe 밖에서 전역적으로 beforeEach를 작성하면 해당 테스트 파일 안에 있는 test는 모두 적용된다.

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

req.body에 newProduct 객체를 넣는 부분은 Products 컨트롤러의 Create 기능을 테스트할 때만 사용된다. 이 부분은 해당 describe 안으로 넣어주자.

describe("Product Controller Create", () =>{
    beforeEach(()=>{
        req.body = newProduct
    })
  ...

테스트를 실행하면 이전과 같이 모든 테스트가 통과된다.
API 호출 후 상태 값은 res 객체에 statusCode를 toBe matcher로 검사한다. 예를들어 creatProduct는 서버에 데이터를 생성하는 API이므로 201을 반환해야한다.

test("should return 201 status code", () =>{
    productController.createProduct(req,res,next)
    expect(res.statusCode).toBe(201);
})

컨트롤러에서 임의로 201 코드를 보내주면 테스트를 통과한다.
API 반환값(결과값)이 제대로 전송되는지는 node-mocks-http에서 제공하는 res 객체의 _isEndCalled()로 확인할 수 있다. 이 값이 True면 제대로 반환이 된 것이다.

test("should return 201 status code", () =>{
    productController.createProduct(req,res,next)
    expect(res.statusCode).toBe(201);
    expect(res._isEndCalled()).toBeTruthy()
})

그럼 결과값이 제대로 반환되는지는 어떻게 알 수 있을까. createProduct에서는 Product모델의 create 메서드로 DB에 생성된 데이터 결과값을 보내주어야한다. 당연히 create 함수는 mock 함수이므로 테스트할 때 호출되면 반환할 값을 지정해주어야 한다.

test("should return json body in response",()=>{
    productModel.create.mockReturnValue(newProduct)
    productController.createProduct(req,res,next)
    expect(res._getJSONData()).toStrictEqual(newProduct)
})

mockReturnValue로 mock 함수인 create가 리턴해줄 값을 넣어준 후, 반환값에 담길 json 값은 node-mocks-http의 _getJSONData() 함수를 이용하여 가져올 수 있다. toStrictEqual matcher를 사용하여 이렇게 반환된 값이 newProduct (새로 생성된 객체)와 같은지 확인한다.

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

컨트롤러는 위와 같이 create 함수의 결과를 json 형태로 반환하도록 작성하면 테스트를 통과한다.

Async - await

createProduct 함수는 DB에 새로운 row를 추가한다. 이 작업은 당연히 시간이 걸리고, async로 처리해주어야한다. 컨트롤러의 createProduct 함수를 수정해준다.

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

이에 맞춰서 단위테스트도 수정을 해줘야한다. test 속 createProduct 호출부에 async - await 처리를 하면 된다.

test("should return json body in response", async ()=>{
    productModel.create.mockReturnValue(newProduct)
    await productController.createProduct(req,res,next)
    expect(res._getJSONData()).toStrictEqual(newProduct)
})

test에 비동기 처리를 안 해주면 pass가 안되다, 비동기 처리를 해주면 정상적으로 작동한 것을 볼 수 있다.

도중에 vscode의 jest 확장 프로그램을 설치해봤는데 편하다. 테스트 별로 코드줄옆의 아이콘을 눌러서 개별 실행할 수 있고, 테스트 이력도 쉽게 볼 수 있다.

[vscode] Jest Extension

Error Handling

API를 처리하면서 발생할 수 있는 여러 예외를 처리해주어야한다. createProduct의 경우 DB에 접근하는 부분은 mock함수이기 때문에 여기서 발생할 수 있는 에러 메시지도 mock 함수로 처리해주어야한다. 비동기 요청이기 때문에 실패하면 reject에 이 에러 메시지를 담아주면 된다.

const errorMessage = {message: "required proeprty missing"}
const rejectedPromise = Promise.reject(errorMessage)
productModel.create.mockReturnValue(rejectedPromise)

에러 처리를 위한 callback 함수 next가 실행될 때 이 에러메시지가 인자로 호출되어야한다. 이것을 테스트하기 위하여 next도 mock 함수로 생성해준다.

next = jest.fn()
...
await productController.createProduct(req,res,next)
expect(next).toBeCalledWith(errorMessage)

이제 테스트 케이스가 완성되었으니 컨트롤러의 실제 코드도 수정해준다. try-catch 문으로 오류가 발생한 경우 callbak 함수 next를 error와 함께 호출한다.

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

Products 컨트롤러의 creaetProduct 함수를 테스트하기 위한 단위 테스트 케이스 5가지가 완성되었다.

0개의 댓글