Back-end/Node.js

[Node.js] 노드 서비스 테스트 하기

poppy 2021. 8. 3. 18:32
반응형

이번 포스팅에는 노드 서비스를 테스트하는 방법에 대해 알아보겠습니다. 유닛테스트, 통합테스트, 부하테스트 등 여러 가지 테스트가 있는데 하나씩 차근차근 알아보겠습니다!

 

1. 유닛 테스트

- 작은 단위의 함수나 모듈이 의도된 대로 정확인 작동하는지 테스트하는 것

 

테스트에 사용할 패키지를 설치하고, package.json 을 수정합니다. 테스트용 파일은 파일명과 확장자 사이에 test 나 spec을 넣으면 됩니다. 콘솔에 "npm test" 를 입력하면 테스트 코드를 실행할 수 있는데 파일명에 test 나 spec 이 들어간 파일들을 모두 찾아 실행합니다.

npm install -D jest // 개발시에만 사용하므로 -D 옵션 사용
// package.json
{
  //.. 생략
  "scripts": {
    "start": "nodemon app",
    "test": "jest" // 추가할 부분
  },
  // .. 생략
}

 

테스트 코드를 작성하겠습니다. 테스트할 대상은 middlewares.js 파일이고, 테스트 코드는 middlewares.test.js 파일입니다.

// middlewares.js
exports.isLoggedIn = (req, res, next) => {
  if (req.isAuthenticated()) {
    next();
  } else {
    res.status(403).send('로그인 필요');
  }
};

 

테스트 코드의 객체로 가짜 객체를 쓰는데 가짜 객체를 넣는 행위를 모킹(mocking) 이라고 합니다. 함수를 모킹할 때는 jest.fn 메서드를 사용하는데 반환값을 지정하고 싶다면 jest.fin() => 반환값 을 사용하면 됩니다. 아래 코드에서는 req, res, next 를 모킹했습니다. 

const { isLoggedIn } = 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); // expect - 원하는 내용이 실행되었는지, toBeCalledTimes - 몇 번 호출되었는지 체크
  });

  test('로그인 되어있지 않으면 isLoggedIn이 에러를 응답해야 함', () => {
    const req = {
      isAuthenticated: jest.fn(() => false),
    };
    isLoggedIn(req, res, next); // 미들웨어 호출하여 테스트
    expect(res.status).toBeCalledWith(403); // toBeCalledWith - 특정 인수와 함께 호출되었는지
    expect(res.send).toBeCalledWith('로그인 필요');
  });
});

 

2. 테스트 커버리지

- 유닛 테스트를 작성하다 보면 어떤 부분이 테스트되고 어떤 부분이 테스트되지 않는지 확인할 때가 생깁니다. 전체 코드 중에서 테스트되고 있는 코드의 비율과 테스트 코드 있지 않은 코드의 위치를 알려주는 jest의 커버리지 기능이 있습니다. 

 

커버리지 기능을 사용하기 위해 package.json을 수정합니다.

{
// .. 생략
  "scripts": {
    "start": "nodemon server",
    "test": "jest",
    "coverage": "jest --coverage" // 추가할 부분
  },
  // .. 생략
}

 

콘솔에 "npm run coverage" 를 입력하여 테스트 커버리지를 분석합니다. 분석하면 콘솔에 다음과 같이 뜨는데 File - 파일과 폴더 이름, % Stmts - 구문 비율, % Branch - 분기점 비율, % Funcs - 함수 비율, % Lines - 코드 줄 수 비율, Uncoverd Line - 커버되지 않은 줄 위치 입니다. 숫자가 높을수록 테스트 커버리지가 높다는 의미이지만 100% 라도 실제로 모든 코드를 테스트한 것은 아닐 수 있습니다.

 

 

3. 통합 테스트

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

 

통합 테스트를 하기 위해 패키지를 설치하고 app 객체를 모듈로 만들어 분리하겠습니다. app.js 파일에서 app 객체를 모듈로 만든 후 server.js 파일에서 불러와 listen 합니다. package.json 도 이것에 맞게 수정합니다.

npm install -D supertest
// app.js
// ..생략
app.use((err, req, res, next) => {
  console.error(err);
  res.locals.message = err.message;
  res.locals.error = process.env.NODE_ENV !== 'production' ? err : {};
  res.status(err.status || 500);
  res.render('error');
});

module.exports = app;
// server.js
const app = require('./app');

app.listen(app.get('port'), () => {
  console.log(app.get('port'), '번 포트에서 대기중');
});
// package.json
"scripts": {
    "start": "nodemon server", // 수정할 부분
    "test": "jest"
  },

 

테스트할 때 실제 사용하는 데이터베이스로 하면 테스트용 데이터가 저장되므로 테스트용 데이터베이스를 따로 만들고 데이터베이스와 연결하는 파일을 다음과 같이 수정합니다.

// config/config.json
{
  "development": {
    "username": "root",
    "password": "자신의 비밀번호",
    "database": "nodebird",
    "host": "127.0.0.1",
    "dialect": "mysql"
  },
  "test": {
    "username": "root",
    "password": "자신의 비밀번호",
    "database": "nodebird_test",
    "host": "127.0.0.1",
    "dialect": "mysql"
  },
  // ...
}

 

테스트 코드를 작성합니다. 로그인 라우터에 대한 테스트 코드를 작성했습니다. beforeAll 은 현재 테스트를 수행하기 전에 수행되는 코드입니다. afterAll 은 테스트 종료시 수행되는 코드입니다. 

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

beforeAll(async () => {
  await sequelize.sync(); // 데이터 베이스에 테이블 생성
});

describe('POST /login', () => {
  test('가입되지 않은 회원', async (done) => {
    const message = encodeURIComponent('가입되지 않은 회원입니다.');
    request(app)
      .post('/auth/login')
      .send({
        email: 'zerohch1@gmail.com',
        password: 'nodejsbook',
      })
      .expect('Location', `/?loginError=${message}`)
      .expect(302, done);
  });

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

  test('비밀번호 틀림', async (done) => {
    const message = encodeURIComponent('비밀번호가 일치하지 않습니다.');
    request(app)
      .post('/auth/login')
      .send({
        email: 'zerohch0@gmail.com',
        password: 'wrong',
      })
      .expect('Location', `/?loginError=${message}`)
      .expect(302, done);
  });
});

afterAll(async () => {
  await sequelize.sync({ force: true }); // force: true - 테이블을 다시 만듬
});

 

콘솔에 "npm test" 를 입력하면 테스트 결과를 확인할 수 있습니다.

 

 

4. 부하 테스트

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

 

부하 테스트를 하기 위해 패키지를 설치한 후 테스트 시나리오를 작성합니다. 테스트 시나리오는 loadtest.json 파일이며 JSON 형식으로 작성해야 합니다. 

npm install -D artillery
// loadtest.json
{
  "config":{
    "target": "http://localhost:8001", // 타겟인 서버
    "phases": [
      {
        "duration": 60, // 60초 동안
        "arrivalRate": 30 // 30명의 사용자 생성
      }
    ]
  },
  "scenarios": [{ // 어떤 동작을 할지 시나리오 작성하는 부분
    "flow": [{ // 메인 페이지에 접속
      "get": {
        "url": "/" 
      }
    }, { // 로그인 수행
      "post": {
        "url": "/auth/login",
        "json": {
          "email": "zerohch0@naver.com",
          "password": "nodejsbook"
        }
      }
    }, { // 해시태그 검색
      "get": {
        "url": "/hashtag?hashtag=nodebird"
      }
    }]
  }]
}

 

콘솔을 두 개 열어서 하나는 "npm start" 를 하여 서버를 실행시키고 하나는 "npx artillery run loadtest.json" 하여 부하테스트를 합니다. 콘솔에서 테스트 결과를 다음과 같이 볼 수 있습니다. 로그가 많이 찍혀서 마지막 부분만 캡쳐한 것입니다.

 

 

반응형