TDD(Test Driven Development)는 QA를 떠나 서비스를 개발할 때 필수적으로 거쳐야 한다.

NodeJS에서 TDD를 수행하기 위해 mochachai를 이용할 수 있다. 이 외에도 좋은 라이브러리가 많으니, 각자 취향에 맞는 라이브러리를 사용하면 된다.

TDD를 공부하기 위해 간단한 To Do List를 함께 제작할 것이다.


Install Dependencies

npm install mocha chai chai-as-promise **--save-dev**|--global

Mocha

Mocha는 Javascript를 Test할 수 있는 하나의 프레임워크다. Mocha를 사용하여 우리가 수행할 단위 테스트 환경을 쉽고 간결하게 작성할 수 있으며 작성한 함수 등에 대하여 스펙에 맞게 테스트를 수행할 수 있다.

또한 Mocha가 제공하는 describeit을 이용하여 테스트에 대한 설명을 작성하고 구분지음으로써 어떤것에 대한 내용인지 확인할 수 있다.

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)함수가 존재한다.

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를 해야하는지 배워보겠다.

+ Recent posts