[Node JS] Jest / supertest 개념 및 CRUD API testing 간단 예제

Onam Kwon·2022년 12월 29일
0

Node JS

목록 보기
16/25

Jest

기본 설정 및 테스트 실행

  • Jest란 페이스북에서 개발한 오픈 소스 자바스크립트 테스트 프레임워크이다.
  • Jest를 사용하는 이유는 원래 mocha로 연습해보려 했으나, Jest에 필요한 함수들이 몇개 보여서 Jest로 바꿔서 연습했다.
  • Jest를 사용하기 위해선 Jest를 설치해야 하며, 아래는 각각 글로벌로 설치하는 방법과 devDependencies에 로컬로써 설치하는 커맨드이다.
npm install -g jest

npm install --save-dev jest
  • Jest를 글로벌로 설치하면 Jest CLI를 command line terminal 에서 사용할 수 있게 해준다.
    • 아래 명령어를 통해 터미널에서 Jest를 사용한 테스팅을 시작할 수 있다.
jest
  • 모카를 devDependencies에만 추가했다면, 아래 명령어를 통해 모카를 사용할 수 있다.
    • 해당 프로젝트의 최상단이 cwd여야함.
./node_modules/mocha/bin/jest
  • Jest는 실행시 자동으로 ./test/ 디렉토리에 존재하는 테스트 파일들을 찾는다.
    • 따라서 아래 명령어를 통해 test디렉토리를 만들어준다.
mkdir test
  • package.jsonscript 내부 test를 아래와 같이 수정하면
/* package.json */
{
  "scripts": {
    "test": "jest"
  }
}
  • 아래와 같은 명령어로 테스트를 실행할 수 있다.
npm test
  • 위 명령어를 입력하면 자동으로 package.json에서 test항목으로 설정했던 jest명령어가 실행된다.
  • Jest를 선택한 이유는 beforeAll()메소드와 afterAll()메소드를 사용하기 위해서였다.
  • 이 함수들은 jest 명령어에 의해 테스트가 시작되면 처음에 한번, 마지막에 한번 메소드를 호출한다.
  • 통합 테스트 하나에는 단위 테스트가 여러개 있으며 단위 테스트를 한번 실행할때마다 DB와 연결하고 끊는 행위는 비효율적이다.
    • 따라서 보통 통합 테스트 시작 전에 연결한 후, 통합 테스트가 끝나면 마지막에 연결을 끊는다.
    • 이때 사용되는 함수가 Jest모듈의 beforeAll(), afterAll() 함수이다.
beforeAll(async () => {
  /**
   * Connecting MongoDB once the test has been started.
   */
  await mongoose.connect(
    process.env.MONGO_URI
  )
  .then(() => console.log('MongoDB conected...'))
  .catch((err) => {
    console.log(err);
  });
});

describe('GET /test', () => {
    it('response with html', (done) => {
      request(app)
        .get('/test/')
        .expect('Content-Type','text/html; charset=UTF-8')
        .expect(200)
        .end((err, res) => {
          if(err) {
            done(err);
          } else {
            done();
          }
        });
    });
  });

afterAll(async () => {
  /**
   * Closing MongoDB.
   */
  await mongoose.connection.close();
});
  • describe()메소드를 기준으로 위의 beforeAll()메소드에서 DB와 연결해주며, 아래의 afterAll()메소드에서 DB와 연결을 해제해 준다.
    • describe()는 테스트 그룹을 묶어주는 역할을 한다.
  • 이 외에도 Jest에서 제공하는 다양한 함수들이 존재한다.
    • beforeEach(), afterEach(): 각 텍스트의 전 후마다 반복 실행.
    • .only(): test.only()처럼 해당 테스트만 임시로 실행하고 싶은 경우 사용.
    • .skip(): test.skip()처럼 해당 테스트는 임시로 스킵하고 싶은 경우 사용.

Jest외 다른 테스트 모듈

  • mocha라고 불리는 브라우저나 노드에서 작동하는 오픈소스 자바스크립트 테스팅 프레임워크도 존재한다.

Chai

  • mocha를 사용하기 위해서 모카 모듈 말고도 assertion 모듈도 필요하다.
    • assertion은 특정 프로그램의 결과값과 기댓값이 일치하는지 확인해주는 기능을 보유하고 있다.
    • 모카에서는 assertion 기능을 지원하는 어떠한 모듈을 사용해도 상관없다.
  • 노드에서 지원하는 빌트인 모듈 assert외에도 Chai, Expect.js, Should.js등 다양한 모듈이 있지만 이 게시글에서는 Chai를 사용한다.
    • Chai는 테스트 시나리오 작성에 필요한 메소드를 제공하는 다양한 assertion 라이브러리중 하나이다.

supertest

  • 여러개의 단위 테스트가 모여 API의 기능을 테스트 하는 통합테스트를 하기 위해선 supertest라는 라이브러리가 필요하다.
  • supertest란 ExpressJS 통합 텍스트용 라이브러리로 내부에서 익스프레스 서버를 구동시켜 가상의 요청을 보내 결과를 검증할 수 있도록 도와주는 라이브러리이다.
    • 실제로 서버를 돌리지 않고 앱 자체를 객체로 가져와 테스트 할 수 있다.
    • request()메소드를 사용할 예정.

server.js / app.js 분리

  • 그렇다면 app.js를 객체로 가져오기 위해 해야할 일은 서버와 앱을 분리해야 한다.
  • 원래 앱이 아래와 같은 형식으로 이루어져 있었다면,
const express = require('express');
const app = express();

const testRouter = require('./controllers/test/testRouter');
const port = 80;

app.listen(port, function() {
    console.log('Listening on '+port);
});

app.use('/test', testRouter);
  • CLI에서 node app 과 같은 형식의 명령어로 앱을 실행할 수 있었을 것이다.
  • 하지만 앞으로는 아래와 같이 app.js 와 server.js로 분리해 사용할 계획이다.
// app.js
const express = require('express');
const app = express();

const testRouter = require('./controllers/test/testRouter');

app.use('/test', testRouter);

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

const port = 80;
const server = app.listen(port, function() {
    console.log('Listening on '+port);
});
  • 기존의 app.js에서 서버 리스닝을 하지 않고 server.js를 추가해 server.js에서 리스닝 하는 이유는 API테스트를 할 때 실제 서버가 구동되어 버리는 경우를 방지하기 위함이다.
    • 서버를 분리하지 않고 app객체를 가져오면 서버가 실행되버리기 때문에 분리후 실행하는 파일, 객체파일 두개로 나눴다.
"scripts": {
    "test": "jest",
    "start": "node server.js"
  },
  • 추가로 package.json에서 서버 실행 명령어를 node app.js에서 node server.js로 변경해 준다.

supertest.request()

  • supertestrequest()는 가상의 서버를 실행하며 api를 요청할 수 있다.
const request = require('supertest');
const app = require('../app');

describe('GET /test', () => {
	it('response with html', (done) => {
		request(app)
          .get('/test/')
          .expect('Content-Type','text/html; charset=UTF-8')
          .expect(200)
          .end((err, res) => {
          if(err) {
            done(err);
          } else {
            done();
          }
        });
    });
});
  • 슈퍼 테스트 모듈을 request변수에 담은 후 서버 객체를 가져와 request()에 넣어준다.
  • 그 후 이어서 get()함수를 사용해 get요청을 할 수 있고 당연히 post, delete, put등 다양하게 사용할 수 있다.
    • 헤더를 보내려면 set()메소드를 이어서 사용해 주면 되며
    • 바디를 보내려면 send()메소드를 사용하면 된다.
  • 마지막으로 이어서 expect()함수로 응답을 검증한다.
    • 위의 코드는 응답의 상태코드를 200으로 기대하며, Content-Type은 'text/html; charset=UTF-8'로 기대한다. 아닐시 에러가 나오면 실패.

CRUD API testing

  • 아래는 테스트 시작 전 MongoDB와 연결 후 테스트를 실행한 후, DB와의 연결을 종료하는 테스트 코드이다.
  • 하나의 통합 테스트는 3개의 단위 테스트로 이루어져 있으며, 토큰 발급, 토큰 확인, 토큰 삭제 순으로 진행된다.
    • 로그인, 로그아웃.

🔽server.spec.js🔽

const request = require('supertest');
const path = require('path');
const mongoose = require('mongoose');

// calling enviroment variable from .env file
require('dotenv').config({ path: path.resolve(__dirname, '../.env') });

const app = require('../app'); 

beforeAll(async () => {
  /**
   * Connecting MongoDB once the test has been started.
   */
  await mongoose.connect(
    process.env.MONGO_URI
  )
  .then(() => console.log('MongoDB conected...'))
  .catch((err) => {
    console.log(err);
  });
});

describe('CRUD API testing', () => {
  describe('JWT token test', () => {
    // In order to use cookies in more than one `it` methods. 
    var cookies;
    it('Issuing a token', async function() {
      var payload = {id:"one",pw:"one"};
      payload = JSON.stringify(payload);
      try {
        var res = await request(app)
        .post('/user/:id/:pw/')
        .send(payload)
        .expect(200);
      } catch(e) {
        console.log(e);
      }
      cookies = res.headers['set-cookie'][0];
    });

    it('Verifying a token', async () => {
      const res = await request(app)
        .get('/user/')
        .set('Cookie', cookies)
        .expect(200);
      expect(res.text.includes(`<div class="login">`)).toEqual(true);
    });
    
    it('Removing token', async () => {
      const res = await request(app)
        .delete('/logout/');
      expect(res.headers['set-cookie']).toEqual(undefined);
    });

  });
});

afterAll(async () => {
  /**
   * Closing MongoDB.
   */
  await mongoose.connection.close();
});
// /user router
router.get('/', userMiddleWare.showMain);
router.post('/:id/:pw', userMiddleWare.signIn);
router.delete('/logout', userMiddleWare.signOut);

토큰 발급

// Sing in.
exports.signIn = async (req, res) => {
    var param = req.body;
    try {
        param = JSON.parse(Object.keys(param)[0]);
    } catch(err) {
        // console.log(err);
    }
    const {id, pw} = param;
    const userConfirmed = await this.loginCheck(id, pw);
    if(userConfirmed) {
        const token = await this.issueToken(id);
        return res
            .cookie('user', token,{maxAge: 30 * 60 * 1000}) // 1000 is a sec
            .end();
    } else {
        return res.status(200).send('Your password is not correct.');
    }
}
  • 아이디와 비밀번호를 one으로 미리 회원가입 해둔 후 테스트를 진행하였다.
    • 물론 당연히 반복적으로 여러가지 아이디와 비밀번호를 자동으로 바꿔가며 테스트 할 수도 있다.
  • id와 pw를 미리 만들어둔 one으로 설정한 후, POST메소드를 통해 요청하므로 send()메소드에 정보를 담아 보낸다.
  • request의 리턴값을 res에 반환하며 해당 객체는 응답 객체가 되며, 서버에서 생성된 토큰은 헤더의 쿠키에 저장되어 있다.

토큰 확인

// Main login page.
 exports.showMain = (req, res) => {
    const user = req.decoded;
    if(user) {
        return res.render(path.join(__dirname, '../../views/user/user'), {user:user});
    } else {
        return res.sendFile(path.join(__dirname, '../../views/user/user.html'));
    }
}
  • 토큰을 확인하는 부분은 토큰이 만약 유효하다면 정상적인 유저가 나왔으므로 ejs파일을 리턴할 때 유저에 관한 코드가 포함되 있으므로 그 부분이 있는지 확인하는 방법을 통해 테스트를 진행했다.
    • 더 좋게 하고싶었는데 더 좋은 방법을 찾지는 못했다.
  • 이 테스트는 사실 유저가 맞다고 가정하고 진행하는 테스트라 맞는 경우의 코드만 작성했음.
  • 인증된 유저가 존재하지 않는다면 .html파일로 응답하게 되며, 해당 파일은 <div class="login"> 태크를 포함하고 있지 않다.

토큰 삭제

// Sign out.
exports.signOut = (req, res) => {
    return res.clearCookie('user').end();
}
  • 쿠키를 지웠으므로, 해당 요청을 진행한 후 헤더에서 쿠키를 확인했을때 undefined가 나온다면 테스트에 통과하도록 작성했다.

실행

  • 위에서 설정을 다 해줬으므로, 아래의 명령어를 통해 실행할 수 있다.
npm test
~/Desktop/Desktop/CS/Practical/AWS/Project/Downloads mvc !2npm test                                                                                                                                                              18:53:29

> aws-node@1.0.0 test
> jest

  console.log
    MongoDB conected...

      at log (test/server.spec.js:17:23)

 PASS  test/server.spec.js
  CRUD API testing
    GET /test
      ✓ response with html (32 ms)
    JWT token test
      ✓ Issuing a token (115 ms)
      ✓ Verifying a token (14 ms)
      ✓ Removing token (4 ms)

Test Suites: 1 passed, 1 total
Tests:       4 passed, 4 total
Snapshots:   0 total
Time:        2.315 s
Ran all test suites.
  • 쓰고나서 보니 코드는 깃헙에서 보는게 더 편할거같다..

Github

profile
권오남 / Onam Kwon

0개의 댓글