상세 컨텐츠

본문 제목

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

21-22/21-22 Node.js

by Kimpeep 2022. 1. 6. 14:16

본문

728x90

1. 테스트 준비하기

   1) jest란?

jest : 페이스북에서 만든 오픈소스로, 테스팅에 필요한 툴들을 대부분 갖추고 있는 패키지
  • 패키지 설치 명령어
npm i -D jest
  • 명령어 등록
{
  "name": "nodebird",
  "version": "0.0.1",
  "description": "익스프레스로 만드는 SNS 서비스",
  "main": "app.js",
  "scripts": {
    "start": "nodemon app",
    "test": "jest"
  },
  ...
}

 

   2) 테스트용 파일 생성

// routes/middlewares.test.js

test("1 + 1은 2입니다.", () => {
  expect(1 + 1).toEqual(2);
});
test(테스트에 대한 설명, 함수(테스트 내용))
                                          expect(실제 코드)
                                          toEqual(예상되는 결과값)

 

   3) 테스트 실행

npm test
  • 결과 화면(expect 값과 toEqual 값이 일치할 때) - 통과

  • 결과 화면(expect 값과 toEqual 값이 불일치할 때) - 실패

 

 

2. 유닛 테스트

   1) isLoggedIn, isNotLoggedIn 함수 테스트

  • 테스트 코드 파일 생성
describe(그룹 설명, 함수(그룹에 대한 내용)) 함수 : 테스트그룹화 해주는 역할
const { describe } = require("../models/user");
const { isLoggedIn, isNotLoggedIn } = require("./middlewares");

describe("isLoggedIn", () => {
  test("로그인 되어 있으면 isLoggedIn이 next를 호출해야 함", () => {

  });

  test("로그인 되어 있지 않으면 isLoggedIn이 에러를 응답해야 함", () => {

  });
});

describe("isNotLoggedIn", () => {
  test("로그인되어 있으면 isNotLoggedIn이 에러를 응답해야 함", () => {

  });

  test("로그인되어 있지 않으면 isNotLoggedIn이 next를 호출해야 함", () => {
      
  });
});
  • middlewares.js 파일
exports.isLoggedIn = (req, res, next) => {
  if (req.isAuthenticated()) {
    next();
  } else {
    res.status(403).send("로그인 필요");
  }
};

exports.isNotLoggedIn = (req, res, next) => {
  if (!req.isAuthenticated()) {
    next();
  } else {
    const message = encodeURIComponent("로그인한 상태입니다.");
    res.redirect(`/?error=${message}`);
  }
};
  • 모킹 코드 작성
모킹 : 가짜 객체, 가짜 함수를 넣는 행위

jest.fn() : 모킹할 때 사용하는 메서드
jest.fn(() => 반환값) : 함수의 반환값을 지정하고 싶을 때

toBeCalledTimes(숫자) : 정확하게 몇 번 호출되었는지를 체크하는 메서드
toBeCalledWith(인수) : 특정 인수와 함께 호출되었는지를 체크하는 메서드
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("isNotLoggedIn", () => {
  const res = {
    redirect: jest.fn(),
  };
  const next = jest.fn();

  test("로그인 되어있으면 isNotLoggedIn이 에러를 응답해야 함", () => {
    const req = {
      isAuthenticated: jest.fn(() => true),
    };
    isNotLoggedIn(req, res, next);
    const message = encodeURIComponent("로그인한 상태입니다.");
    expect(res.redirect).toBeCalledWith(`/?error=${message}`);
  });

  test("로그인 되어있지 않으면 isNotLoggedIn이 next를 호출해야 함", () => {
    const req = {
      isAuthenticated: jest.fn(() => false),
    };
    isNotLoggedIn(req, res, next);
    expect(next).toHaveBeenCalledTimes(1);
  });
});
  • 유닛 테스트 실행
유닛 테스트 : 작은 단위의 함수모듈이 의도된 대로 정확히 작동하는지 테스트하는 것

 

   2) 미들웨어 테스트

  • routes/user.js 파일
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) {
      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;
  • 컨트롤러 분리
const User = require('../models/user');

exports.addFollowing = async (req, res, next) => {
  try {
    const user = await User.findOne({ where: { id: req.user.id } });
    if (user) {
      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;
  • 테스트 코드 작성
jest.mock("../models/user");
const User = require("../models/user");
const { addFollowing } = require("../controllers/user");

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

  test("사용자를 찾아 팔로잉을 추가하고 success를 응답해야 함", async () => {
    User.findOne.mockReturnValue(
      Promise.resolve({
        addFollowing(id) {
          return Promise.resolve(true);
        },
      })
    );
    await addFollowing(req, res, next);
    expect(res.send).toBeCalledWith("success");
  });
  
  test("사용자를 못 찾으면 res.status(404).send(no user)를 호출함", async () => {
  	User.findOne.mockReturnValue(null);
    await addFollowing(req, res, next);
    expect(res.status).toBeCalledWith(404);
    expect(res.send).toBeCalledWith("no user");
  });

  test("DB에서 에러가 발생하면 next(error) 호출함", async () => {
    const error = "테스트용 에러";
    User.findOne.mockReturnValue(Promise.reject(error));
    await addFollowing(req, res, next);
    expect(next).toBeCalledWith(error);
  });
});
  • 유닛 테스트 실행

 

 

3. 테스트 커버리지

   1) 커버리지 기능이란?

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

 

  2) jest 설정 입력

jest --coverage : jest가 테스트 커버리지를 분석
{
  "name": "nodebird",
  "version": "0.0.1",
  "description": "익스프레스로 만드는 SNS 서비스",
  "main": "app.js",
  "scripts": {
    "start": "nodemon app",
    "test": "jest",
    "coverage": "jest --coverage"
  },

 

   3) 테스트 커버리지 분석

 

   4) models/user.js 테스트 코드 작성 (models/user.test.js)

const Sequelize = require('sequelize');
const User = require('./user');
const config = require('../config/config')['test'];
const sequelize = new Sequelize(
  config.database, config.username, config.password, config,
);

describe('User 모델', () => {
  test('static init 메서드 호출', () => {
    expect(User.init(sequelize)).toBe(User);
  });
  test('static associate 메서드 호출', () => {
    const db = {
      User: {
        hasMany: jest.fn(),
        belongsToMany: jest.fn(),
      },
      Post: {},
    };
    User.associate(db);
    expect(db.User.hasMany).toHaveBeenCalledWith(db.Post);
    expect(db.User.belongsToMany).toHaveBeenCalledTimes(2);
  });
});

 

   5) 테스트 수행 - 통과

 

   6) 테스트 커버리지 분석 - 커버리지 증가

 

 

4. 통합 테스트

   1) supertest 패키지 설치

supertest 용도 - auth.js의 라우터들 테스트
npm i -D supertest

 

   2) app.js 파일에서 app 객체 모듈로 만들기 (supertest 사용하기 위함)

...

app.use((err, req, res, next) => {
  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;

 

   3) server.js에서 app 객체 불러와 listen

server.js 역할 - app의 포트 리스닝
const app = require("./app");

app.listen(app.get("port"), () => {
  console.log(app.get("port"), "번 포트에서 대기중");
});

 

   4) npm start 명령어 변경

{
  "name": "nodebird",
  "version": "0.0.1",
  "description": "익스프레스로 만드는 SNS 서비스",
  "main": "app.js",
  "scripts": {
    "start": "nodemon server",
    "test": "jest",
  ...
}

 

   5) config/config.json에서 test 속성 수정

{
  "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"
  },
  ...
}

 

   6) 콘솔에서 nodebird_test 데이터베이스 생성

npx sequelize db:create --env test

 

   7) 테스트 코드 작성

beforeAll : 테스트 실행하기 전에 사용되는 함수
afterAll : 모든 테스트가 끝난 후 사용되는 함수
beforeEach : 각각의 테스트 수행 전에 사용되는 함수
afterEach : 각각의 테스트 수행 후에 사용되는 함수
const request = require('supertest');
const { sequelize } = require('../models');
const app = require('../app');

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

describe('POST /join', () => {
  test('로그인 안 했으면 가입', (done) => {
    request(app)
      .post('/auth/join')
      .send({
        email: 'zerohch0@gmail.com',
        nick: 'zerocho',
        password: 'nodejsbook',
      })
      .expect('Location', '/')
      .expect(302, done);
  });
});

describe('POST /login', () => {
  const agent = request.agent(app);
  beforeEach((done) => {
    agent
      .post('/auth/login')
      .send({
        email: 'zerohch0@gmail.com',
        password: 'nodejsbook',
      })
      .end(done);
  });

  test('이미 로그인했으면 redirect /', (done) => {
    const message = encodeURIComponent('로그인한 상태입니다.');
    agent
      .post('/auth/join')
      .send({
        email: 'zerohch0@gmail.com',
        nick: 'zerocho',
        password: 'nodejsbook',
      })
      .expect('Location', `/?error=${message}`)
      .expect(302, done);
  });
});

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

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

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

describe('GET /logout', () => {
  test('로그인 되어있지 않으면 403', (done) => {
    request(app)
      .get('/auth/logout')
      .expect(403, done);
  });

  const agent = request.agent(app);
  beforeEach((done) => {
    agent
      .post('/auth/login')
      .send({
        email: 'zerohch0@gmail.com',
        password: 'nodejsbook',
      })
      .end(done);
  });

  test('로그아웃 수행', (done) => {
    agent
      .get('/auth/logout')
      .expect('Location', `/`)
      .expect(302, done);
  });
});

afterAll(async () => {
  await sequelize.sync({ force: true });
});

 

   7) 통합 테스트 결과

 

 

5. 부하 테스트

   1) artillery 설치 및 서버 실행

npm i -D artillery 
npm start

 

   2) 새로운 콘솔 띄운 후 명령어 입력

npx artillery quick --count 100 -n 50 http://localhost:8001
[안될 경우 artillery 버전 변경]
npm i -D artillery@1.5.0-12 후에, 명령어 다시 실행

[명령어 옵션]
--count : 가상의 사용자 수
-n : 요청 횟수

 

   3) 부하 테스트 결과

 

   4) 시나리오 작성

{
  "config":{
    "target": "http://localhost:8001",
    "phases": [
      {
        "duration": 60,
        "arrivalRate": 30
      }
    ]
  },
  "scenarios": [{
    "flow": [{
      "get": {
        "url": "/"
      }
    }, {
      "post": {
        "url": "/auth/login",
        "json": {
          "email": "zerohch0@naver.com",
          "password": "nodejsbook"
        }
      }
    }, {
      "get": {
        "url": "/hashtag?hashtag=nodebird"
      }
    }]
  }]
}

 

  5) 시나리오 부하 테스트 실행

npx artillery run loadtest.json

 

 6) 부하 테스트 결과

 

728x90

관련글 더보기