상세 컨텐츠

본문 제목

[노드 1팀] 11장. 노드 서비스 테스트하기

23-24/Node.js 1

by Hetbahn 2024. 1. 5. 10:00

본문

728x90

 

 

 

 

 

 

 

11.1  테스트 준비하기

 

- 테스트에 사용할 패키지 jest

- 9장의 NodeBird 프로젝트에 jest 패키지 설치 

 

 

* jest

$npm i -D jset

 

 

 

package.json에 test라는 명령어를 등록하고 명령어 실행 시 jest 실행

//package.json
{
	"name":...
    "version":...
    ...
    "scrpits":{
    	"start":...
        "test":"jest"
    },
    ...
}

 

 

routes 폴더 안에 middlewares.test.js를 만든다.
테스트용 파일은 파일명과 확장자 사이에 test나 spec을 넣으면 된다.

//middlewares.test.js
test('1+1은 2 입니다.',()=>{
    expect(1+1).toEqual(2);
})

 

함수의 첫 번째 인수로는 테스트에 대한 설명을 적고, 두 번째 인수인 함수에는 테스트 내용을 적는다.

expect 함수의 인수에는 실제 코드, toEqual 함수의 인수로는 예상되는 결괏값을 넣는다.

 

//결과
> nodejs@1.0.0 test
> jest

 PASS  testing/middlewares.test.js
  √ 1+1은 2 입니다. (3 ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        2.822 s
Ran all test suites.

 

테스트를 아무것도 작성하지 않으면 에러가 발생하고 이를 테스트가 실패했다고 표현한다.

expect에 넣은 값과 toEqual에 넣은 값이 일치하면 테스트를 통과한다.

 

 

 

 

11.2  유닛 테스트

//middlewares.test.js
const { isLoggedIn,isNotLoggedIn} = require("./middlewares");

describe('isLoggedIn',()=>{
    const res={
        status:jest.fn(()=>res),
        send:jest.fn(),
    };
    const next = jest.fn();

    test('로그인되어 있으면 isLoggedIn이 next를 호출해야 함',()=>{
        const req={
            isAuthenticated:jest.fn(()=>true),
        };
        isLoggedIn(req,res,next);
        expect(next).toBeCalledTimes(1);
    });
    
    test('로그인되어 있지 않으면 isLoggedIn이 에러를 응답해야 함',()=>{
        const req={
            isAuthenticated:jest.fn(()=>false),
        };
        isLoggedIn(req,res,next);
        expect(res.status).toBeCalledWith(403);
        expect(res.send).toBeCalledWith('로그인 필요');
    });
});

 

작은 단위의 함수나 모듈이 의도된 대로 정확히 작동하는지 테스트하는 것을 유닛 테스트라고 한다.

 

describe 함수는 테스트를 그룹화해주는 역할을 한다.

-> 첫 번째 인수는 그룹에 대한 설명, 두 번째 인수인 함수는 그룹에 대한 내용이다.

 

 

 

모킹

 

테스트의 역할은 코드나 함수가 제대로 실행되는지를 검사하고 값이 일치하는지를 검사하는 것이므로, 테스트 코드의 객체가 실제 익스프레스 객체가 아니어도 된다. 

-> 가짜 객체, 가짜 함수를 넣는 행위를 모킹이라고 한다. 

 

- 함수를 모킹 할 때는 jest.fn 메서드를 사용한다. 

- 반환값을 지정하고 싶다면 jest.fn(() => 반환값)을 사용하면 된다.

 

const { isLoggedIn, isNotLoggedIn } = require('./');

describe('isLoggedIn', () => {
  const res = {
    status: jest.fn(() => res),
    send: jest.fn(),
  };
  const next = jest.fn();

  test('로그인 되어있으면 isLoggedIn이 next를 호출해야 함', () => {
    const req = {
      isAuthenticated: jest.fn(() => true),
    };
    isLoggedIn(req, res, next);
    expect(next).toBeCalledTimes(1);
  });

  test('로그인 되어있지 않으면 isLoggedIn이 에러를 응답해야 함', () => {
    const req = {
      isAuthenticated: jest.fn(() => false),
    };
    isLoggedIn(req, res, next);
    expect(res.status).toBeCalledWith(403);
    expect(res.send).toBeCalledWith('로그인 필요');
  });
});

 

 - isAuthenticated : 로그인 여부를 알려주는 함수 -> true 나 false 반환

- res.status : 메서드 체이닝이 가능해야 하므로 res 반환

- test 함수 내부에서는 모킹된 객체와 함수를 사용해 isLoggedIn 미들웨어를 호출한 후 expect로 원하는 내용대로 실행되었는지 체크한다.

- toBeCalledTimes(숫자)는 정확히 몇 번 호출되었는지 체크하는 메서드이고, toBeCalledWith(인수)는 특정 인수와 함께 호출되었는지를 체크하는 메서드이다.

 

 

 

 

1) 미들웨어 테스트 

 

- 유닛 테스트를 위해 미들웨어를 분리해야 한다.

const express = require('express');

const { isLoggedIn } = require(./middlewares');
const User = require('../models/user');
const router = express.Router();

router.post('/:id/follow',isLoggedIn, async (req, res, next) => {
  try {
    const user = await User.findOne({ where: { id: req.user.id } });
    if (user) { // req.user.id가 followerId, req.params.id가 followingId
      await user.addFollowing(parseInt(req.params.id, 10));
      res.send('success');
    } else {
      res.status(404).send('no user');
    }
  } catch (error) {
    console.error(error);
    next(error);
  }
};

module.exports = router;

 

POST /:id/follow 라우터의 asyn 함수 부분을 따로 분리하여 controllers 폴더를 만들고 그 안에 user.js를 만든다. 

-> 라우터에서 응답을 보내는 미들웨어를 컨트롤러라고 부른다.

 

 

controllers/user.js

router.post('/:id/follow',isLoggedIn, async (req, res, next) => {
  try {
    const user = await User.findOne({ where: { id: req.user.id } });
    if (user) { // req.user.id가 followerId, req.params.id가 followingId
      await user.addFollowing(parseInt(req.params.id, 10));
      res.send('success');
    } else {
      res.status(404).send('no user');
    }
  } catch (error) {
    console.error(error);
    next(error);
  }
};

 

 

 

routes/user.js

const express = require('express');

const { isLoggedIn } = require(./middlewares');
const { addFollowing } = require(../controllers/user');

const router = express.Router();

router.post('/:id/follow', isLoggedIn, addFollowing);

module.exports = router;

 

 

 

addFollowing 컨트롤러 테스트

 

controllers/user.test.js 

jest.mock('../models/user');
const User = require('../models/user');
const { follow } = require('./user');

describe('follow', () => {
  const req = {
    user: { id: 1 },
    params: { id: 2 },
  };
  const res = {
    status: jest.fn(() => res),
    send: jest.fn(),
  };
  const next = jest.fn();

  test('사용자를 찾아 팔로잉을 추가하고 success를 응답해야 함', async () => {
    User.findOne.mockReturnValue({
      addFollowing(id) {
        return Promise.resolve(true);
      }
    });
    await follow(req, res, next);
    expect(res.send).toBeCalledWith('success');
  });

  test('사용자를 못 찾으면 res.status(404).send(no user)를 호출함', async () => {
    User.findOne.mockReturnValue(null);
    await follow(req, res, next);
    expect(res.status).toBeCalledWith(404);
    expect(res.send).toBeCalledWith('no user');
  });

  test('DB에서 에러가 발생하면 next(error) 호출함', async () => {
    const message = 'DB에러';
    User.findOne.mockReturnValue(Promise.reject(message));
    await follow(req, res, next);
    expect(next).toBeCalledWith(message);
  });
});

 

 

addFollowing 컨트롤러가 async 함수이므로 await를 붙여야 컨트롤러가 실행 완료된 후 expect 함수가 실행된다.

-> 위의 코드는 실패 

-> User 모델 때문이다.

-> jest에서는 jest.mock 메서드를 사용해서 모킹할 수 있다. 

 

jest.mock('../models/user');
const User = require('../models/user');

 

- jest.mock 메서드에 모킹할 모듈의 경로를 인수로 넣고 그 모듈을 불러온다.

- 모킹할 메서드에 mockReturnValue라는 메서드를 넣고, 이 메서드로 가짜 반환값을 지정할 수 있다.

 

 

 

 

11.3 테스트 커버리지

 

전체 코드 중에서 어떤 부분이 테스트되고 어떤 부분이 테스트되지 않는지 확인하는 방법이다.

 

전체 코드 중에서 테스트되고 있는 코드의 비율과 테스트되고 있지 않는 코드의 위치를 알려주는 jest의 기능을 커버리지 라고 한다.

//package.json
{
	...
  	"scripts": {
      	"start":"nodemon app",
      	"test": "jest",
      	"coverage":"jest --coverage"
    },
}

 

jest 명령어 뒤에 --coverage 옵션 붙이면 jest가 테스트 커버리지를 분석한다.

 

$npm run coverage
테스트 결과가 출력되고 추가적으로 표가 하나 더 출력된다.

퍼센티지가 높을수록 많은 코드가 테스트되었다는 뜻이다.

% Stmts
//구문 비율

% Branch
//if문 등의 분기점 비율

% Funcs
//함수 비율

% Lines
코드 줄 수 비율

Uncovered Line
커버되지 않은 줄 위치

 

 

 

11.4 통합 테스트

 

- 하나의 라우터에는 여러 개의 미들웨어가 붙어 있고 다양한 라이브러리가 사용되는데 이런 것들이 유기적으로 잘 작동하는지 테스트하는 것이 통합 테스트이다.

 

$npm i -D supertest


- supertest를 사용하기 위해서는 app객체를 모듈로 만들어 분리해야 한다.
- app.js파일에서 app객체를 모듈로 만든 후, server.js에서 불러와 listen 한다.

- server.js는 app의 포트 리스닝만 담당한다.

- package.json을 상황에 맞게 수정한다.

 

 

  "scripts": {
    "start":"nodemon server",
    "test": "jest",
    "coverage":"jest --coverage"
  },

 

통합 테스트에서는 데이터베이스 코드를 모킹하지 않는다. 따라서 테스트용 데이터베이스를 따로 만드는 것이 좋다.

 

 

//routes/auth.test.js
const request = require('supertest');
const { sequelize } = require('../models');
const app = require('../app');

beforeAll(async ()=>{
	await sequilize.sync();
});

describe('POST /login',()=>{
	test('로그인 수행', (done)=>{
    	request(app)
        .post('/auth/login')
        .send({
        	email: 'jinmin645@gmail.com',
            password: '12345678',
        })
        .expect('Location','/')
        .expect(302, done);
    });
});

 

1) beforeAll 함수

 

- 현재 테스트를 실행하기 전에 실행하는 함수이다.
- afterAll(모든 테스트가 끝난 후) , beforeEach(각각의 테스트 수행 전) , afterEach(각각의 테스트 수행 후)등이 있다.

 

 

 

 

11.5 부하 테스트

 

- 서버가 얼마만큼의 요청을 견딜 수 있는지 테스트하는 방법이다.

- 서버는 접속자들의 정보를 저장하기 위해 각가의 접속자마다 일정한 메모리를 할당한다. 사용하는 메모리의 야잉 증가하다가 서버의 메모리 용량을 넘어서게 되면 문제가 발생하는데 이를 OOM(out of memeory)라고 한다.

$npm i -D artillery
$npm start

$npx artillery quick --count 100 -n 50 http://localhost:8001

 

localhost:8001에 빠르게 부하 테스트를 하는 방법이다.
--count는 가상 사용자 수를 의미하고, -n옵션은 요청 횟수를 의미한다.

request latency (응답 지연속도) 가 중요한데, 보통 median(중간값)과 p95(하위 95%)값의 차이가 크지 않으면 좋다.

 

 

 

* 시나리오 테스트

//loadtest.json
{
    "config":{
        "target":"http://localhost:8001",
        "phases":[
            {
                "duration":60,
                "arrivalRate":30
            }
        ]
    },
    "scenarios":[{
        "flow":[
            {
                "get":{
                    "url":"/"
                }
            },{
                "post":{
                    "url":"/auth/login",
                    "json":{
                        "email":"jinmin645@gmail.com",
                        "password":"12345678"
                    }
                }
            },{"get":{
                "url":"/"
                }
            }
        ]
    }]
}

target: 목표 서버
phases.duration: 60초 동안
phases.arrivalRate: 매 초 30명의 사용자
scenarios대로 동작한다.

 

 


빈칸문제 (드래그하여 답을 확인하시오.)

 

1. test 함수의 첫 번째 인수로는 테스트에 대한 (설명)을 적고, 두 번째 인수인 함수에는 테스트 (내용)을 적는다.

2. (expect) 함수의 인수에는 실제 코드, (toEqual ) 함수의 인수로는 예상되는 결괏값을 넣는다.

3. 작은 단위의 함수나 모듈이 의도된 대로 정확히 작동하는지 테스트하는 것을 (유닛 테스트)라고 한다.

4. 테스트 코드에 가짜 객체, 가짜 함수를 넣는 행위를(모킹)이라고 한다.

5. 함수를 모킹할 때는 (jest.fn) 메서드를 사용하고, 모듈을 모킹할 때는 (jest.mock) 메서드를 사용한다.

6. 라우터에서 응답을 보내는 미들웨어를 (컨트롤러)라고 부른다.

7. 하나의 라우터에는 여러 개의 미들웨어가 붙어 있고 다양한 라이브러리가 사용되는데 이런 것들이 유기적으로 잘 작동하는지 테스트하는 것이 (통합 테스트)이다.

 

 

코드 문제

 

1. 아래 코드에 '10+20은 30입니다.'  테스트가 성공하도록 코드를 작성하시오.

test(//여기에 입력,()=>{
    //여기에 입력
})

 

답:

test('10+20은 30 입니다.',()=>{
    expect(10+20).toEqual(30);
})

 

 

2.  현재 서버에 40초 동안 1200명이 접속하도록 초당 사용자를 생성하고 코드를 작성하시오.

{
    "config":{
        "target":"http://localhost:8001",
        "phases":[
            {
               //여기에 코드 작성
            }
        ]
    },
    "scenarios":[{
        "flow":[
            {
                "get":{
                    "url":"/"
                }
            },{
                "post":{
                    "url":"/auth/login",
                    "json":{
                        "email":"jinmin645@gmail.com",
                        "password":"12345678"
                    }
                }
            },{"get":{
                "url":"/"
                }
            }
        ]
    }]
}

 

답 :

 "duration": 40,
 "arrivalRate": 30

 

 

 

 


출처: 조현영 ,  『Node.js 교과서』 개정판 3판, 길벗, 11장

Node.js #1

Editor : 크리스탈_수정

 

728x90

관련글 더보기