티스토리 뷰

테스트 준비하기


Jest

Jest는 Meta에서 관리하는 테스트 프레임워크다. Jest는 단순성에 초점을 맞춰 개발돼 별도의 설정없이 바로 사용할 수 있다.

Jest 환경 설정

  • Jest 설치하기
> npm i -D jest
  • Jest를 npm 스크립트로 바로 실행할 수 있게 config.json 수정
{
  "name": "nodebird",
  "version": "0.0.1",
  "description": "익스프레스로 만드는 SNS 서비스",
  "main": "app.js",
  "scripts": {
    "start": "nodemon server.js",
    "test": "jest", // 추가
  },
  ...
}

Jest 기본 사용법

테스트용 파일은 파일명과 확장자 사이에 `test`나 `spec`을 넣는다. `npm test`로 테스트 코드를 실행하면 파일명에 test나 `spec`이 들어간 파일들을 모두 찾아 실행한다.

test 파일 예시

  • 테스트 코드 작성
test('1+1은 2입니다.', ()=>{
	expect(1+1).toEqual(2);
})
  • `test` 함수의 첫 번째 인수: 테스트에 대한 설명
  • `test` 함수의 두 번째 인수: 테스트 내용
  • `expect` 함수의 인수: 실제 코드
  • `toEqual` 함수의 인수: 예상되는 결과값

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

유닛 테스트


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

테스트 그룹화

`describe`함수는 테스트를 그룹화 해주는 역할을 하는 함수

  • `describe` 함수의 첫번째 인수: 그룹에 대한 설명
  • `describe` 함수의 두번째 인수: 그룹에 대한 내용
discribe('isLoggedIn',()=>{
	test('로그인되어 있으면 isLoggedIn이 next를 호출해야함', ()=>{
    	
    });
    
    test('로그인되어 있지 않으면 isLoggedIn이 에러를 응답해야 함', ()=>{
    	
    });
})

discribe('isNotLoggedIn',()=>{
    test('로그인되어 있으면 isNotLoggedIn이 에러를 응답해야 함', ()=>{
    	
    });
    
	test('로그인되어 있지 않으면 isNotLoggedIn이 next를 호출해야함', ()=>{
    	
    });
})

모킹(mocking)

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

익스프레스에서 req,res객체와 next함수는 직접 가져와 사용할 수 없기 때문에 모킹을 사용해 테스트한다.

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

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

discribe('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(status).toBeCalledWith(403);
        expect(send).toBeCalledWith('로그인 필요');
    });
})
	...
})
  • `toBeCalledTimes(숫자)`: 정확하게 몇 번 호출되었는지 체크하는 메서드
  • `toBeCalledWith(인수)`: 특정 인수와 함꼐 호출 되었는지 체크하는 메서드

DB 모델 모킹

테스트 환경에서는 실제 데이터 베이스에 연결하지 못하기 때문에 모델을 모킹해 사용한다. jest에서는 `jest.mock` 메서드를 사용한다.

jest.mock("../models/user");
jest.mock("../models/post");
const User = require("../models/user");
const Post = require("../models/post");
const { follow, unFollow, userPosts } = require("./user");

describe("follow", () => {
  const next = jest.fn();
  test("사용자를 찾아 팔로잉을 추가하고 success를 응답해야 함", async () => {
    const res = {
      send: jest.fn(),
    };
    const req = {
      user: { id: 1 },
      params: { id: 2 },
    };
    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 () => {
    const res = {
      status: jest.fn(() => res),
      send: jest.fn(),
    };
    const req = {
      user: { id: 100 },
      params: { id: 100 },
    };
    User.findOne.mockReturnValue(null);
    await follow(req, res, next);
    expect(res.status).toBeCalledWith(404);
    expect(res.send).toBeCalledWith("no user");
  });
  test("알 수 없는 응답이오면 next(error)를 호출함", async () => {
    const res = {
      status: jest.fn(() => res),
      send: jest.fn(),
    };
    const req = {
      user: { id: 100 },
      params: { id: 100 },
    };
    const message = "알 수 없는 응답";
    User.addFollowing.mockReturnValue(Promise.resolve(message));
    await follow(req, res, next);
    expect(next).toBeCalledWith(message);
  });
  test("DB에서 에러가 발생하면 next(error)를 호출함", async () => {
    const req = {
      user: { id: 1 },
      params: { id: 2 },
    };
    const res = {};
    const message = "DB 에러";
    User.findOne.mockReturnValue(Promise.reject(message));
    await follow(req, res, next);
    expect(next).toBeCalledWith(message);
  });
});

describe("unFollow", () => {
  const req = {
    user: { id: 1 },
    params: { id: 2 },
  };
  test("언팔로우할 사용자를 찾아 언팔로우하고 success를 호출해야함", async () => {
    const res = {
      send: jest.fn(() => "success"),
    };
    const next = jest.fn();
    User.findOne.mockReturnValue({
      removeFollowing(id) {
        return Promise.resolve(true);
      },
    });
    await unFollow(req, res, next);
    expect(res.send).toBeCalledWith("success");
  });
  test("언팔로우할 사용자가 존재하지 않으면 res.status(404).send(no user)를 호출해야함", async () => {
    const res = {
      status: jest.fn(() => res),
      send: jest.fn(),
    };
    const next = jest.fn();
    User.findOne.mockReturnValue(null);
    await unFollow(req, res, next);
    expect(res.status).toBeCalledWith(404);
    expect(res.send).toBeCalledWith("no user");
  });
  test("DB에서 에러가 발생하면 next(error)를 호출함", async () => {
    const req = {
      user: { id: 1 },
      params: { id: 2 },
    };
    const res = {};
    const next = jest.fn();
    const message = "DB 에러";
    User.findOne.mockReturnValue(Promise.reject(message));
    await unFollow(req, res, next);
    expect(next).toBeCalledWith(message);
  });
});

describe("userPosts", () => {
  const next = jest.fn();
  test("해당 유저의 포스트를 json으로 반환", async () => {
    const req = {
      params: { id: 1 },
    };
    const res = {
      json: jest.fn(),
    };

    Post.findAll.mockReturnValue(Promise.resolve(true));
    await userPosts(req, res, next);
    expect(res.json).toBeCalled();
  });
  test("DB에서 에러가 발생하면 next(error)를 호출함", async () => {
    const req = {
      params: { id: 1 },
    };
    const res = {};
    const message = "DB 에러";
    Post.findAll.mockReturnValue(Promise.reject(message));
    await userPosts(req, res, next);
    expect(next).toBeCalledWith(message);
  });
});
  • `jest.mock` 메서드에 모킹할 모듈의 경로를 인수로 넣고 해당 모듈을 불러옴 -> 해당 모듈의 메서드는 전부 가짜 메서드로 변함 -> `User.findOne` 등의 메서드는 가짜 메서드로 변함
  • 가짜 메서드에 `mockReturnValue` 등의 매서드가 생성됨 -> `User.findOne.mockReturnValue` 메서드로 `User.findOne`의 가짜 반환값을 지정
💡 매번 테스트를 할때 마다 `req`, `res`, `next`를 모킹하는것이 번거로우니 서비스로 분리해 사용하면 편리하다.

테스트 커버리지


Jest에는 전체 코드중에서 테스트되고 있는 코드의 비율과 테스트되고 있지 않은 코드의 위치를 알려주는 커버리지(coverage)기능이 있다.

  • jest 커버리지 실행
> jest --coverage

  • 테이블 설명
File % Stmts % Branch % Funcs % Lines Uncovered Line #s
파일과 폴더 이름 구문 비율 if문 등의 분기점 비율 함수 비율 코드 줄 수 비율 커버되지 않은 줄 위치

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

💡 명시적으로 require한 코드만 커버리지 분석이 된다.

통합 테스트


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

  • 통합 테스트를 위한 supertest패키지 설치
npm i -D supertest
💡 통합 테스트는 단위 테스트와 달리 데이터베이스 코드를 모킹하지 않고 사용하므로 실제 데이터베이스에 저장되니 테스트 데이터베이스를 따로 만들어 사용해야한다.
테스트를 할 때에 서버 listen을 할 필요가 없으므로 app.js의 서버 코드를 listen과 분리해야한다.
  • 테스트 코드 작성
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 /join", () => {
  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", `/?error=${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", `/?error=${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 });
});
  • `beforeAll`: 모든 테스트를 실행하기 전에 수행해야 할 코드를 넣으면 된다.
  • `afterAll`: 모든 테스트가 끝난 후 수행해야 할 코드를 넣으면 됨
  • `beforeEach`: 각각의 테스트 수행 전 실행해야할 코드
  • `afterEach`: 각각의 테스트 수행 후 실행해야할 코드

super 패키지로부터 `request` 함수를 불러와 서버코드 `app`을 객체로 넣고 `post`, `put`, `patch`, `delete` 등의 메서드로 원하는 라우터에 요청을 보낸다. 데이터는 `send` 메서드에 담아서 보낸다.

`request`함수는 비동기 함수이므로 `expect` 메서드의 두번째 인수로 `done`메서드를 넣어 테스트가 마무리되었음을 알린다.

 

`const agent = request.agent(app);`처럼 agent를 만들어 하나 이상의 요청에서 재사용할 수 있다. 이때 테스트가 마무리 되었다고 알려주는 `end(done)`으로 마무리 한다.

 

부하 테스트


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

  • 부하 테스트를 도와주는 artillery 설치
npm i -D artillery
  • 서버를 실행 후 artillery 부하 테스트 실행
> npm start // 서버 실행
> npx artillery quick --count 100 -n 50 http://localhost:8001
  • --count: 가상의 사용자 수 옵션
  • -n: 요청 횟수 옵션

  • vusers.created: 가상의 사용자가 몇명 생성됐는지
  • vusers.completed: 가상의 사용자가 성공적으로 완료한 횟수
  • http.requests, http.responses: 모든 요청과 응답이 몇번 수행 되었는지
  • http.request_rate: 처리 속도
  • http.codes.200: 성공한(200 상태값을 받은) 가상의 사용자
  • `http.response_time`: 응답 지연 속도
    • min: 최소
    • max: 최대
    • median: 중간값
    • p95: 하위 95%
    • p99: 하위 99%

 

  • 실제 사용자처럼 시나리오 대로 테스트하기
  • 시나리오 작성
// loadtest.json
{
  "config": {
    "target": "http://localhost:8001",
    "http": {
      "timeout": 30
    },
    "phases": [{ "duration": 30, "arrivalRate": 20 }]
  },
  "scenarios": [
    {
      "flow": [
        {
          "get": { "url": "/" }
        },
        {
          "post": {
            "url": "/auth/login",
            "json": { "email": "kimhs1470@naver.com", "password": "1234" },
            "followRedirect": false
          }
        },
        {
          "get": {
            "url": "/hashtag?hash=컴퓨터"
          }
        }
      ]
    }
  ]
}
  • `target`: 테스트할 타겟 서버
  • `phases`
    • `duration`: 몇 초동안 실행할 지
    • `arrivalRate`: 매초 몇명의 사용자를 생성할 지
  • `timeout`: 제한 시간내에 완료 못하면 실패로 간주
  • `scenarios`: 가상의 사용자들이 어떤 동작을 할지 작성
    • `flow`: 테스트 흐름 순서대로 실행
💡 부하 테스트를 할 때 리다이렉트되면 시나리오대로 테스트를 더 이상 진행하지 못한다 `"followRedirect": false`로 리다이렉트 되는것을 막는다.
  • 시나리오 설정한 부하테스트 파일 실행
npx artillery run loadtest.json

참고


 

 

Node.js 교과서 : 네이버 도서

네이버 도서 상세정보를 제공합니다.

search.shopping.naver.com

 

 

[개정3판] Node.js 교과서 - 기본부터 프로젝트 실습까지 강의 | 제로초(조현영) - 인프런

제로초(조현영) | 노드가 무엇인지부터, 자바스크립트 최신 문법, 노드의 API, npm, 모듈 시스템, 데이터베이스, 테스팅 등을 배우고 5가지 실전 예제로 프로젝트를 만들어 나갑니다. 클라우드에 서

www.inflearn.com

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
«   2025/01   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함