본문 바로가기
nodejs

[node.js] 유저 목록 조회, 유저 조회, 삭제 테스트 + API 서버 구현

by jinbro 2017. 6. 16.

[API 만들고 테스트하자]
- 그 전에 꼭 알아야할 것
1) 테스트 수트(describe) : 큰 카테고리 경우를 정의할 때 사용
2) 테스트 케이스(it) : 카테고리 내에서 테스트할 거리를 테스트할 때 사용


- 유저목록조회 API 테스트 코드
(1) 테스트 조건   
- 유저목록조회 성공했을 때 유저객체를 담은 배열로 응답, 최대 limit 갯수 만큼 응답한다
- 유저목록조회 실패했을 때 limit(쿼리스트링)의 값이 숫자가 아니라면 400 코드를 응답한다


(2) 테스트 수트와 케이스로 나누기 : 테스트 바탕 조건과 테스트 해보아야 할 것을 구분
- 수트 : 전체 테스트, 성공, 실패
- 케이스 : 성공 - 유저객체를 담은 배열로 응답 - 최대 limit 갯수만큼 응답, 실패 - 400 코드 응답


(3) 케이스 만들기
/* app.js */
app.get('/users', function(req, res){
  if(!req.query.limit){
    res.json(users);
  } else {
    const limit = parseInt(req.query.limit, 10);

    if(isNaN(limit)){
      return res.status(400).end();
    }
    res.json(users.slice(0, limit));
  }
});

/* app.spec.js */

const request = require('supertest');
const should = require('should');
/* have to generate to module */
const app = require('./app');

describe('GET /users는', () => {

    /* 성공 케이스 테스트 */
    describe('성공 시',() =>{
      it('유저 객체를 담은 배열로 응답한다', (done) => {
        request(app)
            .get('/users')
            .end((err, res)=>{
              res.body.should.be.instanceOf(Array);
              done();
            });
      });

      it('최대 limit 갯수만큼 응답한다', (done) => {
        request(app)
            .get('/users?limit=2')
            .end((err, res) =>{
              res.body.should.have.lengthOf(2);
              done();
            });
      });
    });

    /* 실패 케이스 테스트 */
    describe('실패 시', () => {
      it('limit이 숫자형이 아니면 400을 응답한다.', (done) =>{
          request(app)
              .get('/users?limit=ㅎㅇ')
              .expect(400)
              .end(done);
      });
    });
});
- 작성한 코드(app.js)가 mocha + should + supertest에 의해 테스트를 할 때 제대로 요청받고 응답하는지 테스트한 것
- 코드 이해가 되지않으면 전 포스팅이나 mocha, should, supertest api 문서를 참고 
1) describe, it은 mocha
2) should be는 should, assertion types 검색
3) request.get.end는 supertest, end 내 콜백함수 (err,res)는 error와 result의 줄임말

- done은 app.get이 비동기 처리이기때문에 요청 - 응답 후(app.js) 테스트 끝내는 것

- 정리
1) '/users'에 요청을 했을 때 두가지 케이스에 대해 테스트하는 것
2) 두번째 케이스는 실패할 것 : 쿼리스트링 limit 값(임의 값)만큼 오지않았기 때문에
=> app.js 코드 변경 : req 객체를 통해 쿼리스트링(limit) 값 가져오고, 유저 배열에서 limit 값만큼 잘라서 응답하면 됨
app.get('/users', function(req, res){
     const limit = req.query.limit;
     res.json(users.slice(0, limit)); 

});
- 쿼리스트링이 있는지 따지고, 없다면 users 전체, 있다면 값이 숫자가 맞는지, 숫자가 맞다면 limit 갯수 만큼 리턴
1) parseInt와 isNaN 사용
2) res.end() : 응답 프로세스를 종료, node core가 동작한다고 express api 문서에 나와있음
3) status() : 기본값으로 200, 400을 설정하려면 인수로 설정 

- expect로 응답 코드 결과 예상 설정, 별다른 것 없으면 .end(done)으로 테스트 끝내도 됨


- 유저 조회 API 테스트 코드
(1) 테스트 조건 
- 조회 성공했을 때 id가 1인 유저 객체를 반환한다
- 조회 실패했을 때 id가 숫자가 아니면 400으로 응답한다, id로 유저를 찾을 수 없을 경우 404로 응답한다


(2) 테스트 수트와 케이스로 나누기
- 수트 : 전체 테스트, 성공, 실패
- 케이스 : 성공 - id 1인 유저 객체 반환, 실패 - id 숫자 아니면 400 응답 / id 유저를 찾을 수 없을 경우 404 응답


(3) 케이스 만들기
/* app.js */
app.get('/users/:id', function(req,res){
     const id = parseInt(req.params.id, 10);

     if(isNaN(id)){
          return res.status(400).end();
     }

     const user = users.filter((user) => {
       return user.id === id;
     })[0];

     if(!user){
       return res.status(404).end();
     }

     res.json(user);
});

/* app.spec.js */

describe('GET /users/1은', () => {
     /* 성공 케이스 */
     describe('성공 시', () => {
          it('id가 1인 유저 객체를 반환한다', (done) => {
               request(app)
                    .get('/users/1')
                    .end((err, res) => {
                         res.body.should.have.property('id', 1);
                         done();
                    });
          });
     });

     /* 실패 케이스 */
     describe('실패 시', () => {
          it('id가 아닐 경우 400으로 응답한다', (done) => {
               request(app)
                    .get('/users/ㅁㄴㅇ')
                    .expect(400)
                    .end(done);
          });

          it('id로 유저를 찾을 수 없을 경우 404로 응답한다', (done) => {
               request(app)
                    .get('/users/5000')
                    .expect(404)
                    .end(done);
          });
     });
});
- /users/:id 전달받은 id값은 req.params에 저장되어있음 
- id값이 숫자가 아닐 때, 요청할 때 받은 id값으로 자원을 찾았는데 없을 때


- 유저 삭제 API 테스트 코드
(1) 테스트 조건
- 성공 시 : 204를 응답한다
- 실패 시 : id가 숫자가 아닐 경우 400으로 응답한다


(2) 테스트 수트와 케이스
- 수트 : 전체 테스트, 성공, 실패
- 케이스 : 204 응답 / 400 응답


(3) 케이스 만들기
/* app.js */
app.delete('/users/:id', function(req, res){
    const id = parseInt(req.params.id, 10);

    if(isNaN(id)){
      return res.status(400).end();
    }

    users = users.filter((user) => {
      return user.id !== id;
    });
    res.status(204).end();
});


/* app.spec.js */

describe('DELETE /users/1은', () =>{
  /* 성공 케이스 */
  describe('성공 시', () => {
    it('204를 응답한다', (done)=>{
      request(app)
        .delete('/users/1')
        .expect(204)
        .end(done);
    });
  });

  /* 실패 케이스 */
  describe('실패 시', () => {
    it('400을 응답한다', (done) => {
      request(app)
        .delete('/users/one')
        .expect(400)
        .end(done);
    });
  });
});
- 배열 요소 삭제 후 빈자리 채우는 로직을 만들기보다 filter로 특정 id값이랑 일치하지않는 요소만 걸러 기존 배열 덮어쓰기
- 204 코드 : 성공적인 응답을 했지만 콘텐츠 없을 때 사용


- 유저 추가 API 테스트 코드
(0) 필요한 라이브러리 : body-parser
- post 요청으로 날아오는 데이터를 파싱하기위해 사용할 라이브러리
- 기본적으로 express는 post 요청 데이터 파싱(body 파싱 - 태그 value)을 지원해주지않음 : 써드파티 모듈가지고 해결
- npm i body-parser --save
 /* app.js */
const bodyParser = require('body-parser');


app.use(bodyParser.json()); // for parsing application/json


(1) 테스트 조건
- 성공 : 201코드 반환, 생성된 객체를 반환한다, 입력한 name을 반환한다
- 실패 : name 파라미터 누락 시 400 반환, name 중복의 경우 409 반환


(2) 테스트 수트 케이스 나누기
- 수트 : 전체, 성공, 실패
- 케이스 : 성공 3가지, 실패 2가지(400, 409 코드)


(3) 케이스 만들기
/* app.js */
app.post('/users', function(req, res){
    const name = req.body.name;

    if(!name){
      return res.status(400).end();
    }

    const isExist = users.some((user) => {
        return user.name === name;
    });

    if(isExist){
      return res.status(409).end();
    }

    const id = Date.now();
    const user = {
      id: id,
      name: name
    };
    users.push(user);
    res.status(201).json(user);
});


/* app.spec.js */

describe('POST /users는', () => {
    /* 성공 케이스 */
    describe('성공 시', () => {
      let body;
          name = 'park-jinbro';

      before((done) => {
        request(app)
          .post('/users')
          .send({name: name})
          .expect(201)
          .end((err, res) => {
            body = res.body;
            done();
          });
      });
      it('생성된 객체를 반환한다', () => {
        body.should.have.be.property('id');
      });
      it('등록된 name을 반환한다', () => {
        body.should.have.be.property('name', name);
      });
    });

    /* 실패 케이스 */
    describe('실패 시', () => {
      it('name 값을 누락할 경우 400 반환', (done) => {
        request(app)
          .post('/users')
          .send({})
          .expect(400)
          .end(done);
      });


      it('name 중복의 경우 409 반환', (done) =>{
        request(app)
          .post('/users')
          .send({name: 'jinhyung'})
          .expect(409)
          .end(done);
      });
    });
});
- 3가지 결과를 테스트 할 때 사용하는 코드가 똑같음 : mocha.js의 before를 사용하기
- before은 비동기처리를 해야하기때문에 done, it은 동기처리로 테스트하기때문에 done이 필요없음
- before이나 it이 실행되기전(실제 테스트 실행되기전) 필요한 변수 선언 후 사용(공통적으로 사용된다던지....)
- 데이터 중복 기능 체크 시 중복되는 데이터를 넣어봐야 테스트가 가능함 : 그렇지않으면 201 반환해서 테스트케이스 오류


- 유저 수정 API 테스트 코드
(1) 테스트 조건
- 성공 : 수정할 객체 반환
- 실패 : id가 숫자가 아닐 경우 400 코드 반환, id에 해당하는 유저가 없을 경우 404 코드 반환


(2) 테스트 수트, 케이스 나누기
- 수트 : 전체, 성공, 실패
- 케이스 : 성공 시 - 수정 객체 반환 / 실패 시 - 400코드, 404 코드 반환


(3) 케이스 만들기
/* app.js */
app.put('/users/:id', function(req, res){
  const id = parseInt(req.params.id, 10);
  const name = req.body.name;

  if(isNaN(id) || !name){
    return res.status(400).end();
  }

  const isConflict = users.some((user) => {
    return user.name === name;
  });

  if(isConflict){
    return res.status(409).end();
  }

  const user = users.filter((user) => {
    return user.id === id;
  })[0];

  if(!user){
    return res.status(404).end();
  }

  user.name = name;
  res.json(user);
});


/* app.spec.js */

describe('PUT /users/2는', () => {
  /* 성공 케이스 */
  describe('성공 시', () => {
    it('변경된 name을 반환함',  (done) => {
      let name = 'jinbro-park';
      request(app)
        .put('/users/2')
        .send({name: name})
        .end((err, res) => {
          res.body.should.have.property('name', name);
          done();
        });
    });
  });

  /* 실패 케이스 */
  describe('실패 시', () => {
    it('id가 숫자가 아닐 경우 400 코드 응답', (done) => {
      request(app)
        .put('/users/two')
        .expect(400)
        .end(done);
    });

    it('name 값이 누락되었을 경우 400 코드 응답', (done) => {
      request(app)
        .put('/users/2')
        .send({})
        .expect(400)
        .end(done);
    });

    it('id 값으로 유저를 찾지 못했을 경우 404 코드 응답', (done) => {
      request(app)
        .put('/users/999')
        .send({name: 'fake'})
        .expect(404)
        .end(done);
    });

    it('name 값이 이미 존재하는 경우 409 코드 응답', (done) => {
      request(app)
        .put('/users/2')
        .send({name: 'park'})
        .expect(409)
        .end(done);
    });
  });
});



[정리]
- 테스트 수트와 케이스를 나누다보니 에러 경우의 수를 자연스럽게 생각하게됨 : 성공부터 짜고 실패 부분 짜기
- 테스트할 경우의 수를 생각하다보니 자연스럽게 어떤 컨트롤을 넣어야할지 알겠음 : 하나씩 테스트 하면서 만들기, 여러개 한꺼번에 하면......
- 테스트 코드 짜는게 어려운 것이 아니니깐 하나씩 테스트하면서 코딩해도 되겠음
- 자료구조는 언제나 중요함 : 설계할 때도 설계하고 사용할 때도 자료구조 공부를 한 것이...
- 아래는 테스트 결과 이미지




[참고자료]
- inflearn, 테스트주도개발(TDD)로 만드는 NodeJS API 서버 : https://goo.gl/3fEGPp
- 자바스크립트 isNaN, parseInt, control
- express, API 참조 : http://expressjs.com/ko/4x/api.html

- github page, should.js : https://shouldjs.github.io/



댓글