TDD(Test Driven Development)는 QA를 떠나 서비스를 개발할 때 필수적으로 거쳐야 한다.
NodeJS에서 TDD를 수행하기 위해 mocha
와 chai
를 이용할 수 있다. 이 외에도 좋은 라이브러리가 많으니, 각자 취향에 맞는 라이브러리를 사용하면 된다.
TDD를 공부하기 위해 간단한 To Do List를 함께 제작할 것이다.
Install Dependencies
npm install mocha chai chai-as-promise **--save-dev**|--global
Mocha
Mocha는 Javascript를 Test할 수 있는 하나의 프레임워크다. Mocha를 사용하여 우리가 수행할 단위 테스트 환경을 쉽고 간결하게 작성할 수 있으며 작성한 함수 등에 대하여 스펙에 맞게 테스트를 수행할 수 있다.
또한 Mocha가 제공하는 describe
와 it
을 이용하여 테스트에 대한 설명을 작성하고 구분지음으로써 어떤것에 대한 내용인지 확인할 수 있다.
Chai
Chai는 assertion 라이브러리로 Mocha와 함께 쓰이는 라이브러리다. Chai는 Mocha를 통해 수행한 테스트의 결과가 내가 기대한 값인지 테스트할 수 있도록 assertion을 제공한다.
특히 Chai는 사람이 이해할 수 있는 구조로 syntax가 작성되어 있기 때문에 사용하기 편하다.
우리는 chai와 chai-as-promise를 함께 사용하여 NodeJS에서 발생하는 Asynchronous Function에 대한 테스트도 수행할 것이다.
Setting Environment
이제 Mocha와 Chai를 이용한 테스트 환경을 구축하려 한다.
NodeJS상에서 ES6를 이용하여 프로젝트를 만들것이다.
NodeJS: 16.16.0
ECMAScript6
Language: Javascript ( not typescript )
mkdir todolist
cd todolist
npm install mocha chai chai-as-promised --save-dev
npm install express http fs
mkdir test route public middleware model
Mocha Chai 설치가 끝났다면, package.json 파일에 다음과 같이 굵은 내용을 추가해 준다.
{
"name": "tdd-todolist",
"version": "0.0.1",
"description": "Study Test Driven Develop in NodeJS - To Do List",
"main": "starter.js",
**"scripts": {
"test": "mocha test/**/**.spec.js",
"start": "starter.js"
},**
"author": "eugene",
"devDependencies": {
"chai": "^4.3.7",
"chai-as-promised": "^7.1.1",
"mocha": "^10.2.0"
},
"dependencies": {
"express": "^4.18.2",
"fs": "^0.0.1-security",
"http": "^0.0.1-security"
},
**"type": "module"**
}
이 때, “scripts”아래의 “test”는 test 디렉토리 아래의 모든 *.spec.js를 mocha를 통해 테스트한다. 는 뜻이다.
이를 통해 생성된 프로젝트 구조는 다음과 같다.
Simple Test
이제 간단한 테스트를 통해 환경이 잘 구축되었는지 보려한다. model/user.js
, test/user.spec.js
를 통해 user가 잘 동작하는지 확인해 볼 것이다.
model/user.js
user.js는 간단하게 전달받은 사용자의 이름과 비밀번호를 바탕으로 객체를 생성하고 다음과 같은 함수를 가진다.
toObject(): User 정보를 Object 형태로 출력한다.
promise(shouldResolve, shouldError): promise 함수를 테스트한다.
- shouldResolve: Boolean
- caller에게 resolve(true|false)를 반환할 것인지 설정한다.
- shouldError: Boolean
- caller에게 error를 반환할 것인지 설정한다.
toObject()
Method를 통해 user의 정보를 출력하는 클래스다. 여기에 추가로, Javascript의 특징인 Asynchronous Function의 테스트를 수행하는promise(resolve, shouldError)
함수가 존재한다.- shouldResolve: Boolean
export default class User {
constructor(name, password){
this.name = name;
this.password = password;
}
toObject(){
return {
name: this.name,
password: this.password
}
}
promise(shouldResolve, shouldError){
return new Promise((resolve, reject) => {
if(shouldError){
return reject();
}
if(shouldResolve){
return resolve(true);
}
return resolve(false);
})
}
}
test/user.spec.js
user.spec.js는 위에서 생성한 model/user.js
가 정상적으로 동작하는지 확인하는 Unit Test를 제공한다.
따라서 우리는 User클래스에 존재하는 toObject()
함수와 promise(shouldResolve, shouldError)
함수를 모두 테스트 할 것이다.
chai와 chaiAsPromised를 사용할 것이기 때문에, 다음과 같은 구문을 최상단에 작성하고, 우리가 테스트할 클래스를 가져온다.
import chai from 'chai';
import chaiAsPromised from 'chai-as-promised';
chai.use(chaiAsPromised);
const expect = chai.expect;
const assert = chai.assert;
import User from '../model/user.js';
그 다음 줄 부터 test를 수행할 내용을 작성하면 되는데, 그 모양은 다음과 같다.
describe를 통한 Unit Test 단위 정의 예시
describe('테스트를 수행에 대한 최상위 이름', () => { describe('그 다음 이름', () => { describe... }); });
it을 통한 테스트 수행 예시
describe('최상위 이름', () => { it('함수 확인', () => { expect(확인할 대상).to.be.a('function') }); });
이를 바탕으로 우리가 수행할 테스트 내용을 작성하면 다음과 같다.
먼저 User class를 정상적으로 사용할 수 있는지 expect().to.be.a('function')
과 expect().to.be.a.instanceOf(Parent)
를 통해 확인한다.
describe('"Up"', () => {
it('should be exist', () => {
expect(User).to.be.a('function');
});
it('should be a class', () => {
const user = new User();
expect(user).to.be.a.instanceOf(User);
});
});
다음으로, User class 내에 모든 Method가 정상적으로 동작하는지 확인한다. 이 때, 우리는 async 함수를 따로 갖고 있으므로 synchronous와 asynchronous를 구분해서 수행하겠다.
const user = new User('eugene', 'password');
/* Synchronous 함수 */
describe('"Synchronous"', () => {
/* toObject() 함수를 통해 user의 이름과 비밀번호가 잘 설정되는지 확인한다. */
it('toObject()', () => {
const obj = user.toObject();
expect(obj.name).to.be.equal('eugene');
expect(obj.password).to.be.equal('password');
})
});
/* Asynchronous 함수 */
describe('"Asynchronous"', () => {
const promise = user.promise;
/* promise함수가 정말 promise 함수인가? */
it('"promise" is promise function', () => {
const _promise = promise();
expect(_promise.then).to.be.a('Function');
expect(_promise.catch).to.be.a('Function');
})
/* promise함수에서 내가 설정한 인자를 전달하면, 그 결과가 예상대로 반환되는가 */
it('"promise()" should be resolved', async () => {
promise( true ).then(
(data) => expect(data).to.be.a.true,
(error) => expect(error).to.be.a.false
);
})
/* promise함수에서 내가 설정한 인자를 전달하면, 그 결과가 예상대로 반환되는가 */
it('"promise()" should be a false', async () => {
promise( false ).then(
(data) => expect(data).to.be.a.true,
(error) => expect(error).to.be.a.false
);
})
/* promise함수에서 내가 의도한 에러가 잘 발생하는가 */
it('"promise()" should be a error', () => {
expect(promise( false, true )).to.be.rejectedWith(Error);
})
});
이 내용들을 모두 포함하면, test/user.spec.js가 완성된다.
import chai from 'chai';
import chaiAsPromised from 'chai-as-promised';
chai.use(chaiAsPromised);
import User from '../model/user.js';
const expect = chai.expect;
const assert = chai.assert;
describe('User module', () => {
describe('"Up"', () => {
it('should be exist', () => {
expect(User).to.be.a('function');
});
it('should be a class', () => {
const user = new User();
expect(user).to.be.a.instanceOf(User);
});
});
describe('"Method Check"', () => {
const user = new User('eugene', 'password');
describe('"Synchronous"', () => {
it('toObject()', () => {
const obj = user.toObject();
expect(obj.name).to.be.equal('eugene');
expect(obj.password).to.be.equal('password');
})
});
describe('"Asynchronous"', () => {
const promise = user.promise;
it('"promise" is promise function', () => {
const _promise = promise();
expect(_promise.then).to.be.a('Function');
expect(_promise.catch).to.be.a('Function');
})
it('"promise()" should be resolved', async () => {
promise( true ).then(
(data) => expect(data).to.be.a.true,
(error) => expect(error).to.be.a.false
);
})
it('"promise()" should be a false', async () => {
promise( false ).then(
(data) => expect(data).to.be.a.true,
(error) => expect(error).to.be.a.false
);
})
it('"promise()" should be a error', () => {
expect(promise( false, true )).to.be.rejectedWith(Error);
})
});
});
});
Mocha를 통한 test 수행
이제 작성한 test/user.spec.js가 잘 되는지 확인하면 된다. 명령어는 우리가 package-.json
에 작성해 놓은대로, npm test
명령어를 통해 수행할 수 있다.
npm test
만약 결과가 내 예상(user.spec.js에 기술한 것)과 다르다면, 다음과 같이 테스트에 실패한 부분에서 에러가 발생한다.
이렇게 오늘 TDD를 위한 기초에 대해 공부해봤다. 다음 장 에서는 todolist 개발을 함께하면서 어떻게 TDD를 해야하는지 배워보겠다.