TDD

lsw·2021년 8월 27일
0

서버

목록 보기
3/6
post-thumbnail

4. TDD

TDD란 Test Driven Development의 두문자어로 테스트코드를 작성한 후 개발코드를 작성하는 개발방식을 일컷는다.

왜 사용할까?

저는
1. 코드로부터 예상 혹은 기대되는 내용을 테스트코드에 규약처럼 작성하여 개발코드를 작성하는 과정이 이 규약에 어긋나지 않도록 함
2. 각 api를 일일히 호출해 테스트하는 과정을 생략하기 위해 사용함
으로 받아들였다.

주의해야할 점
테스트코드가 갑이 되어야 한다는 것이다. 반대로 개발 코드는 테스트코드의 니즈를 충족시키는 을 역할이 되어야 하는데, 개발자의 변심, 상황의 변화에 따라 개발의 방향성과 내용이 변경된다면 테스트코드 또한 수시로 변경되어야 하며 이는 TDD의 취지와 맞지 않는다. 개발 내용이 주기적으로 바뀐다면, 개발 - 테스트 코드를 분리하여 작업하는 것은 이상속의 얘기가 될 것은 분명하다.
(독립변수, 종속변수의 개념을 다들 아실텐데 '종속변수는 독립변수에 영향을 받는다'만 이해하시고~ 테스트코드가 독립변수, 개발코드가 종속변수라고 이해해도 좋다.)


코드를 설명하기 앞서
몇가지 개념을 되새기고 넘어가고 싶다. 짚어보자면

4.1. mocha test - should

mocha테스트는 describe , it 매서드로 구성되어 있는데, 각

describe : 테스트 환경을 제공하는 테스트수트 이다. it : 실제 테스트를 진행하는 테스트케이스 이다.

npm i mocha

를 통해 패키지 제이슨 파일에 해당 모듈을 추가해 준 후,

const request = '검사하고 싶은 파일의 경로'
describe('~~일 때', () => {
    it('~~이다', done => {
        // 검사내용
	});		
});

검사를 진행한다. 이 때 검사를 위해 사용되는 모듈이 크게 두 가지, assert 와 should가 있다.
~는 ~여야 해, ~는 ~로 예상돼 등의 내용을 서술 하려면 assert(주장)이나 should(확신)이 있어야 한다.

const should = require('should');
const assert = require('asert');
const { a, b } = require('./test'); // 변수 a, b를 가져와서 테스트를 진행할겁니다.

describe('~~일 때', () => {
		it('~~이다', done => {
			// assert		
			assert.equal(a,b)	
			// should
			a.should.be.equal(b);
	});		
});

테스트 코드 작성에서는 웬만하면 should를 사용하는 것이 좋다고 하는데 이유는 모르겠다 ㅋ-ㅋ

4.2. mocha test - supertest

위의 코드를 보면 a와 b가 일치하는지의 여부, 즉 단일한 조건에대한 테스트를 진행하였는데 저희가 궁극적으로 하고자 하는 테스트의 목적은 클라이언트 요청에 따른 서버 api 응답 예상이기 때문에 이러한 단일 테스트 이상의 것이 필요로 한다. 이를 직접적으로 해결해 주는 것이 바로 supertest 모듈이다.

const request = require('supertest');
const app = require('express객체 app을 exports하는 파일의 경로') // api테스트가 필요한 파일 경로

describe('~~이면', () => {
	it('~~이다', done => {
			request(app)
			   .method1()
			   .method2()
			   .method3(done)
	});
});

사용법은 위와 같으며 테스트하고자 하는 api가 들어있는 (보통 서버를 직접 구동하는 파일에서 express 객체를 반환하면 이를)를 슈퍼테스트의 인자로 넣어준다.

4.2.1. mocha test 동기처리(테스트 케이스 콜백함수)

앞서 mocha test의 테스트 케이스 작성 시 it 매서드의 콜백함수로 done 함수를 호출한 적이 있다. supertest 객체 생성은 내부적으로 express 서버를 구동하는 무거운 일로, 비동기로 진행되는 mocha test에서 필히 동기처리가 필요합니다. 처리 방법으로 크게 세가지를 들어보면

  • done

    .end() 매서드 호출 시 별도 then - catch매서드의 사용이 필요 없다.

  • async / await

    promise의 상태가 변경될 때 까지(resolve될 때 까지) 기다리는 await 선언과 그의 사용을 위한 async 선언이다.

  • return

    mocha테스트는 리턴되는 모든 promise를 기다려준다. 따라서 return을 활용해 프라미스를 처리하는 것도 타당하다.

supertest는 내부적으로 Promise를 생성한 후 resolve 또는 reject하기 때문에 해당 결과를 처리해줄 수 있어야 한다. 이에 대한 방법으로

  1. then ~ catch => [resolved → then][rejected → catch ]
  2. .end() method
.end((res, err) => {
//에러처리 부분
     if(err) throw err;
// 상위 매서드(호출자)로 단계적으로 에러를 던짐. 최종적인 처리는 request(app) 에서 위뤄질 것으로 예상.
// 그 외부분***
    res.body. ....
}

앞서 설명한 동기처리 방식 중 return과 async방식은 .end()매서드가 이미 내부적으로 한번 호출되고 있으므로 .end() 추가호출 시 twice calling error 가 발생한다.

// return case
it('~~', () => {
return
     request(app)
        .get('/')
        .expect(~~)
// error 발생 지점
      ~~.end(done)~~
});

// async - await case
it('~~', async function{
await
    request(app)
	 .get()
         .expect()
// error 발생 지점
       ~~.end()~~
});

4.2.2. TDD 소스 코드(동기, 예외 처리)

/*
- mocha 테스트는 기본적으로 nodejs위에서 이뤄지기 때문에 비동기, 논블라킹 방식을 취한다.
- express서버를 구동하는 무거운 작업을 내부에서 진행하는 supertest를 처리하는 별도과정이 필요하다.
*/

/* 동기 처리에 대한 3가지 방법(상기방법과 동일)
1. done
2. async - await
3. return
*/

// done : 슈퍼테스트 마무리에 done매서드를 호출해준다.
const request = require('supertest');
const should = require('should');
const app = require('app');

// end ~ done
describe('/players는', () => {
    describe('성공 시', () => {
        it('~가 된다.', done => {
            request(app)
                .get('/players')
                .expect(200)
                .end( (res, err) => {
                    if (err) throw err;
                    res.body.should.have.property('key', 'value');
                    done(); // done함수 호출
                });
        });
    });
});

// expect ~ done : 에러처리는 가능하지만 response에 대한 통제권을 지정해주지 않은상태.
describe('/players는', () => {
    describe('성공 시', () => {
        it('~가 된다.', done => {
            request(app)
                .get('/players')
                .expect(200, done);
        });
    });
});

// then ~ catch ~ done : end()매서드 대신 then catch 사용
describe('/players는', () => {
    describe('성공 시', () => {
        it('~가 된다.', done => {
            request(app)
                .get('/players')
                .expect(200) // 프라미스 반환
                    .then( res => {
                        res.body.should.have.lengthOf(2); // res.body에 대한 테스트
                        console.log(res);
                    })
                    .catch( err => {
                        done(err); // done 호출
                    });
        });
    });
});

// 일반함수 + return & async - await : then ~ catch를 이용해 반환값, 에러를 처리한다.
describe('/players는', () => {
    describe('성공 시', () => {
        it('~가 된다.', () => {
            return request(app)
                .get('/players')
                .expect(200) // 여기서 프라미스의 상태가 변경된다.(만족 / 불만족)
                    .then( res => {
                        res.body.should.be.instanceOf(Array); // res에 대한 테스트
                    })
                    .catch( err => {
                        console.log(err);
                    });
        });
    });
});

describe('/players는', () => {
    describe('성공 시', () => {
        it('~가 된다.', async() => {
            await request(app)
                .get('/players')
                .expect(200) // 마찬가지 프라미스의 상태가 변하는 곳
                    .then( res => {
                        res.body.should.be.instanceOf(Array); // res에 대한 처리권한
                    })
                    .catch( err => {
                        console.log(err);
                    });
        });
    });
});

4.2.3. 서버 구동 시 자동 감시자 설정

설정) pm2 start file_dir --watch

해제) pm2 stop file_dir --watch

효과 : 소스코드가 변경되어도 파일 저장만 누르면 서버를 재 구동할 필요 없이 변경된 사항이 자동으로 반영됩니다. next js프레임워크를 이용한 react 웹페이지 만들기에서 최초 빌드 이후엔 소스변경 시 동기화에 별도 재구동이 필요하지 않은 경우와 같은 원리인 것 같다.


4.2.4. package.json script

package.json 파일 script 항목에 "단축단어": "동작 명령" 을 통해 동작명령어를 줄일 수 있다.

"test": "mocha file_dir" (주소 기준은 json파일이 존재하는 현위치이다.)

terminal : npm run test → mocha test 진행.


4.2.5. (GET, POST, PUT, DELETE 매서드) api 환경 슈퍼테스트

하나의 파일이 너무 방대해 질 때 코드 리팩터(refactor)를 통해 파일을 분리하는 작업을 수행하는 것이 좋다.

spec.js : 실제 테스트가 진행되는 파일(테스트 수트 / 케이스가 담겨져 있다.)

// 모듈, 미들웨어 가져오기
const request = require('supertest');
const app = require('../../server');
const should = require('should')
const models = require('../../models');
const mocha = require('mocha')

// GET
 describe('GET : /players는', () => {
     describe('성공 시', () => {
         // before( () => models.sequelize.sync({force:true}));
         it('player list를 반환한다.', () => {
             return request(app)
                .get('/players')
                .then( res => {
                    res.body.should.be.instanceOf(Array);
                })
                .catch( err => {
                    console.log(err);
                })
                /* .end((err, res) => {
                    if(err) throw err;
                    res.body.should.be.instanceOf(Array);
                    done();
                }); */
         });
         it('최대 limit갯수만큼 응답한다.', (done) => {
             request(app)
                .get('/players?limit=2')
                .end((err, res) => {
                    if (err) throw err;
                    res.body.should.have.lengthOf(2)
                    done();
                });
         });
     });
     describe('실패 시', () => {
         it('limit이 숫자형이아니면 400을 응답한다.', (done) => {
             request(app)
                .get('/players?limit=three')
                .expect(400, done);
                /* .end((err, res) => {
                    if (err) throw err; // expect에서 발생하는 error가 end매서드로 던져진 것. 따라서 throw처리를 하지 않으면 에러가 처리되지 않음.
                    done();
                });
                */
         });
     });
 });

 describe('GET : /players:mvp 는', () => {
     describe('성공 시', () => {
         it('mvp가 4인 객체를 반환한다.', (done) => {
             request(app)
                .get('/players/4')
                .end((err, res) => {
                    if(err) throw err;
                    res.body[0].should.have.property('final_mvp', 4);
                    done();
                });
         });
     });
 });

//DELETE
describe('DELETE : /players/:mvp는', () => {
    describe('성공 시', () => {
        it('상태코드 204를 반환한다.', (done) => {
            request(app)
               .delete('/players/4')
               .expect(204)
               .end((err, res) => {
                   done();
               });
        });
    });
    describe('실패 시', () => {
        it('mvp가 숫자가 아니면 상태코드 400을 반환한다.', async () => {
            await request(app)
               .delete('/players/four')
               .expect(400)
        });
    });
});

//POST
describe('POST : /players 는', () => {
    describe('성공 시', () => {
        let body;
        before(done => {
            request(app)
               .post('/players')
               .send({name: 'ball'})
               .expect(201)
               .end((err, res) => {
                   if (err) throw err;
                   body = res.body;
                   done();
               });
        });
        it('입력한 이름을 반환한다.', () => {
            body.should.have.property('name', 'ball');
        });
        it('생성된 유저객체를 반환한다.', () => {
            body.should.have.property('club', 'bulls');
        });
    });
    describe('실패 시', () => {
        it('파라매터 누락 시 400을 반환한다.', done => {
            request(app)
               .post('/players')
               .send({})
               .expect(400)
               .end(done);
        });
        it('name이 중복일 경우 409를 반환한다.', done => {
            request(app)
               .post('/players')
               .send({name: 'tatum'})
               .expect(409)
               .end(done);
        });
    });
});

//PUT
describe('PUT : /players 은', () => {
    describe('성공 시', () => {
        const name = 'zrue';
        it('변경된 name을 응답한다.', done => {
            request(app)
               .put('/players?club=bucks')
               .send({name})
               .end((err, res) => {
                   if (err) throw err;
                   res.body.should.have.property('name', 'zrue');
                   done();
               });
        });
    });
    describe('실패 시', () => {
        const name = 'howard';
        it('문자열이 아닌 club일 경우 400을 응답', done => {
            request(app)
               .put('/players?club=4')
               .send(name)
               .expect(400)
               .end(done);
        });
        it('name이 없을 경우 400 응답', done => {
            request(app)
               .put('/players?club=nets')
               .send()
               .expect(400)
               .end(done);
        });
        it('없는 클럽일 경우 404 응답', done => {
            request(app)
               .put('/players?club=w')
               .send({ name: 'howard' })
               .expect(404)
               .end(done);
        });
        it('이름이 중복일 경우 409 응답', done => {
            request(app)
               .put('/players?club=knicks')
               .send({ name: 'tatum'})
               .expect(409)
               .end(done);
        });
    });
});

router : 라우터 객체가 담긴 파일

const express = require('express');
const router = express.Router();
const playerService = require('./service');

router.get('/', playerService.getPlayers);
router.get('/:mvp', playerService.getMvpPlayer);
router.delete('/:mvp', playerService.deleteMvpPlayer);
router.post('/', playerService.createPlayer);
router.put('/', playerService.editPlayer);

module.exports = router;

service : 라우터의 미들웨어 함수가 담겨진 파일. 서비스 함수들의 묶음이라고도 한다.

// const sqliteDB = require('../../models');

// dummy data
players = [
    {club: 'lakers', name: 'lebron', age: 36, salary: 38, final_mvp: 4},
    {club: 'celtics', name: 'tatum', age: 23, salary: 28, final_mvp: 0},
    {club: 'nets', name: 'durant', age: 32, salary: 41, final_mvp: 2},
    {club: 'hawks', name: 'trae', age: 22, salary: 8, final_mvp: 0},
    {club: 'mavs', name: 'doncic', age: 22, salary: 10, final_mvp: 0},
    {club: 'clippers', name: 'kawai', age: 30, salary: 36, final_mvp: 2},
    {club: 'raptors', name: 'siakam', age: 27, salary: 31, final_mvp: 0},
    {club: 'heat', name: 'butler', age: 31, salary: 36, final_mvp: 0},
    {club: 'jazz', name: 'mitchel', age: 24, salary: 28, final_mvp: 0},
    {club: 'bucks', name: 'giannis', age: 26, salary: 38, final_mvp: 1},
    {club: 'knicks', name: 'walker', age: 33, salary: 32, final_mvp: 0},
];

// player list를 내려주는 매서드
exports.getPlayers =  (req, res) => {
    if (req.query.limit === undefined) limit = players.length;
    else limit = parseInt(req.query.limit, 10);
    if(Number.isNaN(limit)) return res.status(400).end();
    res.status(200);
    res.json(players.slice(0,limit)).end();
};

// mvp횟수를 쿼리하여 객체를 내려주는 매서드
exports.getMvpPlayer =  (req, res) => {
    const mvp = parseInt(req.params.mvp, 10);
    if (Number.isNaN(mvp)) return res.status(400).end();
    const player = players.filter( player => player.final_mvp === mvp);
    // if (player.length == 0) return res.status(404).end();
    res.json(player).end();
    };

// mvp횟수를 쿼리하여 해당객체를 데이터에서 삭제하는 매서드
exports.deleteMvpPlayer =   (req, res) => {
    const mvp = parseInt(req.params.mvp, 10);
    players =   players.filter( player => player.final_mvp !== mvp);
    if (Number.isNaN(mvp)) res.status(400).end();
    res.status(204).end();
};

// 선수 생성 매서드
exports.createPlayer =   (req, res) => {
    const name = req.body.name;
    const new_player = {club: 'bulls', age: 24, salary: 21, final_mvp: 0};
    const AlreadyExistPlayer =   players.filter( player => player.name == name).length;
    if (!name) return res.status(400).end();
    if (AlreadyExistPlayer) return res.status(409).end();
    new_player.name = name;
    players.push(new_player);
    res.status(201);
    res.json(new_player).end();
};

// 선수정보 수정 매서드
exports. editPlayer =   (req, res) => {
    const name = req.body.name;
    if (typeof name != 'string' || !name) return res.status(400).end();
    const AEplayer =   players.filter( player => player.name == name);
    if ( AEplayer.length != 0) return res.status(409).end();
    const club = req.query.club
    const playerList =  players.filter( player => player.club == club );
    if (playerList.length === 0) return res.status(404).end();
    const player = playerList[0];
    player.name = name;
    res.json(player).end();
};

// 데이터베이스 테이블을 ORM으로 추상화한 것을 모델이라 하며 이 모델을 통해 각종 매서드들에 접근하는 것이다.
// 모델을 정의하고(sequelize.define()) -> 정의된 모델과 데이터베이스를 연동한다. (sequelize.sync())

server : express 객체인 'app' 이 존재하는 파일

const express = require('express');
const bodyParser = require('body-parser');
const morgan = require('morgan');
const app = express();
const player = require('./api/player/router.js');

if (process.env.NODE_ENV != 'test') {
    app.use(morgan('dev')); // 로그를 찍어준당
}

app.use(bodyParser.urlencoded());
app.use(bodyParser.json());

app.use('/players', player);
module.exports = app; // for supertest

server.excuting : express객체의 listen 매서드 호출을 통해 실제 서버를 구동하는 파일

const app = require('./server.js');
app.listen(2500, () => {
    console.log('server is running on port 2500');
});

4.2.6. 결과

data : json형식의 더미데이터 리스트
https://user-images.githubusercontent.com/81002379/130533283-40c4918a-0d7d-41b6-a5ba-170f0fdc1054.png


terminal : mocha test 성공, 실패여부를 로그와함께 출력해준다.

https://user-images.githubusercontent.com/81002379/130532682-5fa61668-caea-43fa-9a70-b2d26b85381f.png


반환값 확인

호출) GET : /players

https://user-images.githubusercontent.com/81002379/130532631-e90cf44e-ee38-48dd-856c-87624e7608a0.png


호출) GET : /players/4

https://user-images.githubusercontent.com/81002379/130532726-199de952-cdc4-4516-a335-3997d7d7c116.png


호출) POST : /players

req.body type : form data (express 최신버전은 body-parser모듈이 내장되어 있어 form data의 해석이 가능하다)

https://user-images.githubusercontent.com/81002379/130532737-292085a8-aaf9-4adc-8853-6f6eeeeb2044.png


호출) PUT : /players?club=lakers

https://user-images.githubusercontent.com/81002379/130532748-17e61840-569b-433c-884d-44f292f04495.png


호출) DELETE : /players/0 → GET : /players

Mvp수상 경험이 없는 플레이어 리스트에서 제외, 변경된 리스트 겟

https://user-images.githubusercontent.com/81002379/130532752-1ae460e0-7da8-4050-b678-988aa8a9be90.png

근데 이게 과연 실효성이 있을까 의문은 듭니다. 개발과정은 어려우나 유지보수가 쉽다 정도로 이해하고 넘어가련다 ^0^

profile
미생 개발자

0개의 댓글