Unit test를 진행하기 위해서 Jest라는 테스트 프레임워크가 도입되기 전까지는, 서드 파티 라이브러리들을 합쳐서 하나의 테스팅 환경을 구성했습니다. Jasmine(Angular에 최적화되어 있습니다) or Mocha + Chai + Sinon + Istanbul로 테스트를 진행할 수 있습니다.
Mocha
Jest가 모든 도구들을 가지고 있다면, Mocha는 이에 비해서 Test Runner의 기능만을 가지고 있기 때문에 가볍습니다. 마치 모니터 / 키보드 / 스피커 /... 이 없는 서버 본체처럼 필요한 기능들은 서드 파티 라이브러리를 추가로 가져다가 사용하는 것으로 테스트 환경을 구성하는 것을 의도합니다. 특히 Jest는 jest-environment-jsdom / jest-snapshot 등도 있어서 backend(server)에서 테스트를 위한 환경을 구성한다면, Mocha의 사용을 검토할 필요가 있다고 생각합니다. 다만 Jest는 parallelization과 round-robin scheduling을 채택하고 있어서 프로젝트의 크기가 커질수록 성능이 향상될 가능성이 있다고 합니다. (medium.com/airbnb-engineering/unlocking-test-performance-migrating-from-mocha-to-jest-2796c508ec50)
// global env
npm install --global mocha
// dev env
npm install --save-dev mocha
Mocha를 설치했더라도, mocha는 assertion에 대한 지원이 없기 때문에 should.js(shouldjs.github.io), expect.js(github.com/Automattic/expect.js), chai.js(www.chaijs.com) 등 추가 라이브러리를 사용하는 것으로 matcher를 가져와야 합니다. mocking도 마찬가지로 jsmockito.js(jsmockito.org), sinon.js(sinonjs.org) 등 추가 라이브러리를 필요로 합니다.
"scripts": {
"test": "mocha"
}
package.json에 스크립트를 작성하는 것도 옵션들을 같이 사용할 수 있습니다. 타입 스크립트로 작성된 테스트를 수행하기 위해서 ts-node를 옵션에 넣는 것으로 해결할 수 있습니다. (www.npmjs.com/package/ts-node에서 'mocha'로 검색)
mocha --require ts-node/register --watch-extensions ts,tsx "test/**/*.{ts,tsx}"
혹은 .mocharc.yaml을 통해서 config 옵션을 지정할 수도 있습니다. (mochajs.org/#configuring-mocha-nodejs)
mocha --require ts-node/register --config=config/.mocharc.yaml test/**/*.spec.{ts,tsx}
.mocharc.yaml (github.com/mochajs/mocha/blob/master/example/config/.mocharc.yml)
# This is an example Mocha config containing every Mocha option plus others.
allow-uncaught: false
async-only: false
bail: false
check-leaks: false
color: true
delay: false
diff: true
exit: false # could be expressed as "no-exit: true"
extension:
- 'js'
# fgrep and grep are mutually exclusive
# fgrep: something
file:
- '/path/to/some/file'
- '/path/to/some/other/file'
forbid-only: false
forbid-pending: false
full-trace: false
global:
- 'jQuery'
- '$'
# fgrep and grep are mutually exclusive
# grep: something
growl: false
ignore:
- '/path/to/some/ignored/file'
inline-diffs: false
# needs to be used with grep or fgrep
# invert: false
jobs: 1
package: './package.json'
parallel: false
recursive: false
reporter: 'spec'
reporter-option:
- 'foo=bar'
- 'baz=quux'
require: '@babel/register'
retries: 1
slow: '75'
sort: false
spec:
- 'test/**/*.spec.js' # the positional arguments!
timeout: '2000' # same as "timeout: '2s'"
# timeout: false # same as "no-timeout: true" or "timeout: 0"
trace-warnings: true # node flags ok
ui: 'bdd'
v8-stack-trace-limit: 100 # V8 flags are prepended with "v8-"
watch: false
watch-files:
- 'lib/**/*.js'
- 'test/**/*.js'
watch-ignore:
- 'lib/vendor'
특히 이중에 parallel option을 true로 주게 되면 개별 파일의 수가 많은 경우 테스트에 걸리는 시간을 단축할 수 있습니다. 파일 단위로 병렬 처리를 하기 때문에 한 파일에 많은 테스트 코드가 작성되어 있다면 큰 이점을 보지 못할 수도 있습니다.
Mocha는 before / after... 등 jest와 같은 기능을 하지만 명칭이 조금 다르게 쓰입니다. test()는 없고 it()만 존재합니다. describe(), it()과 더불어서 context()라는 것도 존재하는데 describe - it의 사이 단계로 사용하거나 혹은 케이스를 분리하는 것을 도와 가독성을 높입니다.
describe('given', function() {
before(function() {
console.log('BEFORE');
// mocking section
});
context('when', function() {
console.log('context is the same as describe(), just for readability');
it('then', function() {
console.log('it');
// assertion
});
});
});
Mocha의 test runner는 Jest보다 더 많은 기능들을 사용할 수 있습니다. (mochajs.org/#dynamically-generating-tests)
Chai
Chai는 Mocha만으로 구성을 한 상태에서 node.js의 Assert()를 사용하는 것보다 편한 처리를 할 수 있습니다. 또한 chai-enzyme을 사용해서 frontend(client)에서도 DOM관련 unit test에 대해서 Mocha + Chai로 대체할 수 있습니다.
npm install --global chai
Chai는 should(www.chaijs.com/guide/styles/#should), expect(www.chaijs.com/guide/styles/#expect), assert(www.chaijs.com/guide/styles/#assert)를 제공해서 TDD / BDD의 matcher를 구현할 수 있습니다. 풍부한 method를 제공합니다. 또한 done()과 함께 중복 호출을 막으면서 에러에 대한 테스팅이 가능합니다.
externalService.functionA = sinon.stub();
describe('given', () => {
describe('when success', () => {
it('then', done => {
externalService.functionA.returns('SUCCESS');
myService.functionA().then(response => {
expect(response).to.equal('SUCCESS');
done();
}).catch(error => {
done(error);
});
});
});
describe('when fail', () => {
it('then', done => {
externalService.functionA.throws(function() { return new ParameterError() });
myService.functionA().then(() => {
throw new Error();
}).catch(error => {
done(expect(error).to.be.instanceOf(ParameterError));
});
});
});
});
Sinon
Sinon은 사전에서 'Greek who by a false tale induced the Trojans to drag the wooden horse into Troy'로 그 이름처럼 모킹을 지원합니다. Jest처럼 Sinon가 지원하는 stub(), spy(), 그리고 fake()를 통해서 mocking을 할 수 있습니다.
npm install sinon
stub()
import { describe, before, after, it } from 'mocha';
import { expect } from 'chai';
import sinon from 'sinon';
import MyService from './MyService.js';
const externalService:any = {};
const myService = new Myservice(externalService); // injection
externalService.extFunc = sinon.stub(); // mocking
describe('given', () => {
describe('when success case of calling myFunc', () => {
it('then', done => {
externalService.extFunc.returns('SUCCESS'); // mocks a return value
myService.myFunc().then(response => {
expect(response).to.equal('SUCCESS');
done();
}).catch(error => {
done(error);
});
});
});
});
const service: any = {};
const error = new Error();
const value = "VALUE";
service.stubFuncA = sinon.stub().throws(); // default exception : Error()
service.stubFuncB = sinon.stub();
service.stubFuncB.throws(); // same expression to upper line
service.stubFuncC = sinon.stub().returns(value);
service.stubFuncD = sinon.stub().resolves(value);
service.stubFuncE = sinon.stub().rejects(error);
stub() 선언과 함께 함수에 대한 반환 값을 지정할 수 있으며 선언과 지정을 분리해서 수행할 수 있습니다. (sinonjs.org/releases/latest/stubs)
Sinon에서도 Jest처럼 restore기능을 제공합니다. stub(), spy(), 그리고 fake()로 선언된 함수들 모두 사용이 가능합니다.
const service: any = {};
const stubFunction = sinon.stub(service, "functionA").returns(false);
stubFunction.restore();
restore()를 통해서 함수의 모킹을 하지 않은 원 함수로 변경할 수 있습니다. 프로미스를 생성하고 반환하는 함수라면 모킹 된 함수를 통해서 다른 함수에 대한 테스트 이후 restore를 하는 부분을 then() chaning에 맞게 선언을 하는 것을 통해서 다른 unit test에 영향을 미치지 않도록 처리할 수 있습니다. 다만 --parallel option을 준 Mocha는 Jest와 다르게 완전한 병렬 처리가 아니기 때문에 (Jest는 vm을 각각 생성하고 테스트를 수행합니다.) 다른 테스트에 영향을 주지 않도록 적절한 파일 분할 처리가 필요합니다.
spy()
const service: any = {};
const value = "value";
const error = new Error();
const spyFunctionA = sinon.spy(service, "functionA");
spy()를 통해서 함수에 대한 arguments, return values, value types 등 행위에 대한 기록을 할 수 있습니다. assertion을 하기 위한 용도로 사용됩니다. (sinonjs.org/releases/latest/spies)
const service: any = {};
const value = "value";
const error = new Error();
const spyFunction = sinon.spy(service, "function");
const inputArray = ["first", "second", "third"];
describe('When', () => {
before(() => {
spyFunction.withArgs(inputArray);
});
it('Then', done => {
service.function();
expect(spyFunction.called()).to.be.true;
done();
});
});
// spy.called : true if the spy was called at least once
// spy.notCalled : true if the spy was not called
호출을 할 때 사용할 아규먼트를 결정하는 것과 동시에 반환에 대한 조정도 가능합니다.
// return arrays : when first call, "hi" soon be returned
service.spyFunctionB = sinon.spy().returnValues(["hi", "this is", "my", value]);
// throw arrays : when first call, error with false soon be throwed
// WARNING : if you use returnValues and exceptions both for mocking one function,
// that's an Overwrite format
service.spyFunctionC = sinon.spy().exceptions([new Error(false), error, new Error("TUT")]);
fake()
fake()는 Sinon v5부터 사용이 가능한 기능으로, stub, spy의 모든 methods를 지원하고 해당 기능을 통해서 default behavior (return / throw / resolve / reject)에 대한 설정이 가능합니다. (sinonjs.org/releases/latest/fakes)
fake()로 만들어진 function은 wrap를 하고자 하는 stub() 그리고 spy()와는 다르게 함수 자체를 만드는 것에 대한 기능입니다. fake()는 immutable로, 한번 만들어지면 해당 함수의 수정이 불가능합니다. 테스트를 위한 함수를 mocking 하고 싶다면, sinon.replace를 통해서 교체를 해야 합니다.
const service: any = {};
const value = "value";
const error = new Error();
const fakeFunction = sinon.fake.returns(false);
describe('When', () => {
before(() => {
sinon.replace(service, "myFunction", fakeFunction);
});
it('Then', done => {
const response = service.myFunction();
expect(response).equal(false);
done();
});
after(() => {
sinon.restore();
});
});
test platform을 구성하기 위해서 Mocha-Chai-Sinon을 같이 사용하는 방법에 대해서 소개드렸습니다. 각각 라이브러리가 조화롭게 작동하니 해당 조합도 테스트 환경을 구성할 때 기술 스택으로 하나의 방법이 될 수 있겠다는 생각입니다. 분량 조절 실패로 (ㅜ.ㅜ) Yup와 Istanbul에 대해서는 #3, 그리고 Enzyme 등 DOM test를 위한 라이브러리에 대한 설명은 그 후 진행해보겠습니다.
※ 잘못된 부분이나 궁금한 점 댓글로 작성해 주시면 최대한 답변하겠습니다. 읽어주셔서 감사합니다.
※ Javascript Testing #3 - Yup, Istanbul에서 각각을 소개해 볼 예정입니다.
'React > Testing' 카테고리의 다른 글
Javascript Testing #1 - Jest (0) | 2021.03.16 |
---|