Assert: Node.js 테스트 모듈

요즘은 TDD니, BDD니 해서 테스트의 중요성이 더 없이 강조되고 있다. 워낙 많이 언급되다 보니 여지껏 테스트 코드를 작성할 일이 없었던 나도 조금은 공부해야겠다는 생각이 들었다. 방대한 라이브러리를 품고 있는 JavaScript 생태계답게, 테스트 관련 라이브러리나 프레임워크도 다양하지만 일단은 Node.js에서 포함하고 있는 기본적인 모듈인 Assert를 정리해봤다.

Assert는 유닛 테스트를 위해서 Node.js에서 사용할 수 있는 테스트 모듈이다. 별도의 설치없이도 다음처럼 import하면 바로 사용할 수 있다.

1
const assert = require('assert');

유닛 테스트는 특정 모듈이 예상한 대로 돌아가는 지 검증하는 것이 목표이므로 다음과 같이 요상한 모듈이 있다고 가정한다(..)

1
2
3
4
5
6
7
8
// just-module.js
'use strict';

exports = module.exports;

exports.returnFalse = () => {
return false;
};

just-module 이라는 녀석이 가지고 있는 returnFalse()라는 메소드는 호출 할 때마다 매 번 false를 반환하는 놀라운 메소드이다(..)

메소드

assert() / ok() / ifError()

assert는 그 자체가 테스트용 함수이기도 하다.

1
2
3
assert(justModule.returnFalse()); // throw AssertionError!
assert.ok(justModule.returnFalse()); // assert()의 alias임.
assert.ifError(justModule.returnFalse()) // Pass

ok()assert()의 alias 이므로 똑같이 동작한다. ifError()assert()와 반대로 인자로 넘어온 값이 false랑 같은 지 비교한 후 같지 않으면 에러를 발생시킨다.

위 코드를 실행하면 AssertionError를 발생시키며 프로그램이 종료된다. assert()는 인자로 넘어온 값이 true랑 같은 지 비교한 후, 같지 않으면 에러를 발생시키기 때문이다.

다음처럼 변경하면 에러가 일어나지 않는다.

1
2
3
assert(justModule.returnFalse() == false); // Pass
assert.ok(!justModule.returnFalse()); // Pass
assert.ifError(justModule.returnFalse()); // Pass

equal() / notEqual()

equal()notEqual()은 인자를 두 개 넘겨서 비교할 수 있는 메소드이다.

1
2
assert.equal(justModule.returnFalse(), ''); // Pass
assert.notEqual(justModule.returnFalse(), [0]); // throw AssertionError!

응? 눈치가 빠른 분이라면 코드가 왜 이따구인지 감이 오실 것이다. 그렇다. equal()notEqual()은 비교할 때 ==!=를 사용한다! 강제 형변환을 한 뒤에 비교하는 것이다. =====의 차이에 대해 잘 모른다면 이 글을 참고하면 좋다. (무려 블로그의 첫 글..)

아무튼, JavaScript의 강제 형변환을 별로 좋아하지 않는 사람이라면 이걸로 테스팅하기에는 뭔가 찜찜할 것이다. 하지만 역시 Assert는 ===!==로 비교하는 메소드도 가지고 있다.

strictEqual() / notStrictEqual()

strictEqual()notStrictEqual()은 Identity 연산자로 비교한다.

1
2
assert.strictEqual(justModule.returnFalse(), ''); // throw AssertionError!
assert.notStrictEqual(justModule.returnFalse(), [0]); // Pass

이제 ===!==로 비교하므로 조금 더 명확한 비교가 가능해졌다!

deepEqual() / notDeepEqual()

여기까지만 읽으면 equal()이든 strictEqual()이든 그냥 ===== 쓰면 되는 거 아닌가? 싶은 생각이 들 수 있다. 사실 맞다. 가독성 외에는 큰 차이가 없다. 하지만 deepEqual()notDeepEqual()은 조금 더 편리한 기능을 제공한다.

먼저, 테스트를 위해 모듈에 조금 더 고수준의 함수를 구현해보자.

1
2
3
4
5
// just-module.js
// ...
exports.getObject = () => {
return { a: 1 };
}

justModule이라는 모듈에 getObject()라는 몹시 복잡한 작업을 하는 함수가 추가되었다(…) 그리고 테스트 코드를 작성한다.

1
2
assert.equal(justModule.getObject(), justModule.getObject()); // throw AssertionError!
assert.notEqual(justModule.getObject(), justModule.getObject()); // Pass

우리가 짠 함수는 항상 똑같이 생긴 객체를 반환하는데, 이상하게도 equal()을 사용한 비교에서 AssertionError를 발생시키고 notEqual()에서 통과된다. 그렇다. 아시다시피 객체의 변수는 참조만을 저장하고, 반환된 두 개의 객체는 서로 참조하고 있는 객체가 다르므로 에러가 발생할 수 밖에 없는 것이다. 이 글을 참조.

이렇게 객체는 equal()으로 비교하면 안된다. 따라서 이런 때에 deepEqual()이나 notDeepEqual()을 사용할 수 있다. deepEqual()은 객체 내부까지 비교해서 일치여부를 체크하는 메소드이다. 위 코드는 다음과 같이 고칠 수 있다.

1
2
assert.deepEqual(justModule.getObject(), justModule.getObject()); // Pass
assert.notDeepEqual(justModule.getObject(), justModule.getObject()); // throw AssertionError!

이제 예상대로 잘 동작한다!

deepStrictEqual() / notDeepStrictEqual()

deepStrictEqual()notDeepStrictEqual()이 무슨 메소드인지는 대충 짐작이 갈 것이다. deepEqual()notDeepEqual()이 Equality 연산자(==, !=)로 비교하므로 Identity 연산자(===, !==)를 통해 깊은 비교를 할 수 있도록 해주는 메소드이다. ‘deep’과 ‘strict’ 모두 앞에 다룬 내용이므로 예제는 패스.

다만, 한 가지 주의할 점이 있는데, 이 메소드들은 Old Node.js에서는 지원하지 않는다는 것이다. v0.10, v0.12에서는 사용할 수 없으며 v4.x부터 지원한다. 따라서 테스트 코드가 오래된 버전의 Node.js에서도 돌아야 한다면 core-assert라는 라이브러리를 사용하면 된다.

throws() / doesNotThrow()

throws()doesNotThrow()는 인자로 넘긴 함수 블록이 에러를 throw 하는 지 체크하는 메소드다. 테스트를 위해 모듈에 무조건 에러를 던지는 메소드를 구현한다.

1
2
3
4
5
// just-module.js
// ...
exports.throwError = () => {
abc(def); // ReferenceError expected
};

당연하지만, throwError()라는 메소드에서 abcdef는 존재하지 않는 변수이므로 ReferenceError를 발생시킬 것이다.

throws() 메소드는 아래처럼 사용할 수 있다.

1
2
3
assert.throws(() => {
justModule.throwError(); // throw ReferenceError
}); // but pass

이 코드를 실행시키면 ReferenceError가 발생함에도 불구하고 프로그램이 정상적으로 종료될 것이다. 에러를 발생시킬 것이라는 예상이 맞았기 때문이다. 여기서는 오히려 에러를 발생시키지 않으면 AssertionError가 발생한다.

throws() 메소드에는 함수 블럭말고도 발생하는 에러의 조건이 될 수 있는 추가적인 인자를 넘길 수 있다. throw() 호출 코드를 다음과 같이 수정한다.

1
2
3
assert.throws(() => {
justModule.throwError(); // throw ReferenceError
}, TypeError); // can't pass at this time.

throwError()라는 메소드는 ReferenceError를 발생시키는데, 인자로 TypeError의 생성자를 전달하였으므로, 예상이 틀리게 되고, 결국 예정된 ReferenceError를 발생시키므로 테스트는 실패한다.

인자로는 생성자말고도, 조건이 되는 함수 블럭, 정규식 등을 넘길 수 있다.

doesNotThrow()는 조금 이상한 녀석이다. throws()와 반대성격의 메소드이므로 에러가 발생하지 않을 것임을 예상한다고 볼 수 있는데, 인자로 넘긴 타입과 같은 타입의 에러가 발생하면 AssertionError가 발생하지만, 다른 에러가 발생하면 그냥 그 에러가 발생해서 테스트가 멈춘다. 다음 예제를 보자.

1
2
3
assert.doesNotThrow(() => {
justModule.throwError(); // throw ReferenceError
}, ReferenceError); // throw AssertionError

여기서는 발생하지 말아야할 에러가 발생했으므로 AssertionError가 발생한다. 여기서 ReferenceError가 아니라 TypeError를 인자로 넘기면 어떻게 될까? 마치 테스트가 통과될 것 같지 않은가?

1
2
3
assert.doesNotThrow(() => {
justModule.throwError(); // throw ReferenceError
}, TypeError); // can't pass..

하지만 내 예상과는 달리 원래 발생하는 ReferenceError가 그대로 발생한다. 따라서 테스트는 멈추게 된다. 이 쯤에서 이 메소드의 존재의의가 궁금해진다(…) 어차피 에러를 발생시키는 구문이라면 doesNotThrow() 안에서 실행시키지 않아도 테스트를 멈추게 할테니 말이다. 이 부분은 그럴듯한 사례나 이유를 찾지 못했다. 혹시 알고 계신 분은 댓글을 남겨주시기 바란다.

fail()

fail()모 보험처럼 묻지도 따지지도 않고 무조건 AssertionError 에러를 발생시키는 메소드다. doesNotThrow()와 마찬가지로, 굳이 이런 메소드가 필요한지에 대해 의문이 들었는데 실제로도 거의 사용되지 않는 메소드라고 한다. 사용법은 매우 간단하고, 중요하지 않은 메소드이므로 넘어가도록 하겠다.

마치며

사실 끝에 와서 맥 빠지는 말이지만, Assert는 실제로는 그렇게 자주 사용되지는 않는 것 같다. 머리말에 밝혔듯이 검증된데다가 가독성도 뛰어난 테스트용 프레임워크/라이브러리가 많기 때문이다. should.jsexpect.js는 당장 Assert를 대체할 수 있는데다가 브라우저에서도 사용가능하고 이미 검증되어있는 좋은 테스트 프레임워크다.

하지만 그렇다고 해서 Assert를 공부하는 것 자체가 의미없는 행위는 아니다. 일단 Assert는 서드파티 라이브러리가 아니고 안정적이며, 별도의 설치가 필요 없으므로 작은 프로젝트에도 가볍게 사용할 수 있다. 게다가 should.js의 should 객체는 Assert 모듈을 확장한 것이기 때문에 should.js를 사용할 때도 적용할 수 있다.

참고링크