작업하는 몇몇 소스가 브라우저에서도 사용이 되고, 노드(혹은 io.js도 가능할 듯.)에서도 사용이 됩니다. 통합된 환경에서 어떻게 하면 유닛테스트를 할 수 있을까 고민을 해보았습니다.

그러던 중 QUnit이라는 도구가 눈에 띄었습니다.

QUnit

qunitjs.com

간단히 얘기하자면 jQuery팀에서 사용하는 테스트 도구입니다. 기존에 하는 유닛테스트와 가장 유사한 것 같아서 선택 한 것 같습니다.

그외에 몇가지 테스트 도구를 간단히 소개하자면..

  • Jasmine : Test-Driven이 아닌 Behavior-Driven 테스트 도구입니다. 루비에서 RSpec이 익숙하시면 이쪽도 봐두시면 좋습니다.
  • Mocha.js : TDD, BDD 두 스타일 모두를 지원하는 간단한 테스트 도구입니다. 국내 쪽 소개글은 여기서 볼 수 있습니다.

이 글 작성하면서 다른 도구들을 찾아봤는데 더 재밌어보이는게 많아보이네요. 그래도 QUnit으로 작성하기로 했기 때문에 QUnit을 계속 이야기 하도록 하겠습니다.

QUnit의 장점이 무엇이냐. 하면.. 제가 사용할 수 있다는 것? 그만큼 봐야할 문서도 적고 단순하다는걸 장점으로 꼽을 수 있을 것 같습니다. 테스트 하는데 이것저것 다 필요없고 assertEqual, assertTrue만 필요하다면 사용하기 좋은 도구인 것 같습니다.

시작하기에 앞서..

환경을 구축하는데 가장 많이 고민 한 것이 소스의 모듈화 였습니다. 지금 현재 개발하는 프로그램이 많은 소스로 구성되어있고 해당 파일 하나당 하나의 테스트 모듈을 갖게 하는것이 목표였습니다. 그렇다면 Node와 Browser환경에서 공통 모듈을 작성할 수 있는 방법이 뭐가 있을까 고민을 했습니다.

노드에서는module.exports = myModule;의 형태를 사용해야 할 것이고, 브라우저에서는 AMD 즉, define(function() { return myModule; }의 형태를 사용해야 합니다.

물론, 통합된 모듈을 만들때 쓰는 패턴이 있긴 합니다.

;(function(global, factory){
	if (typeof exports === "object") {
		module.exports = factory();
	}
	else if ( typeof define === 'function' && define.amd ) {
		define(factory);
	}
	else {
		global.MyModule = factory();
	}
})(this, function() {
	/* Source ... */
	return myModule;
});

그러나 위의 소스를 매 테스트파일마다 넣는 것도 굉장히 복잡한 느낌이었습니다. 그래서 requirejs를 노드에도 도입해서 사용하기로 했습니다.

덧붙여, node-qunit이라는 녀석이 이미 있습니다. 그러나 저녀석으로 하면 파일별로 모듈화 하고 브라우저랑 동시에 사용할 수 없습니다. (AMD를 사용하지 않기 때문.)

간단한 구조

일단 두가지 프로그램을 설치합시다.

$ npm install requirejs --save-dev
$ npm install qunitjs --save-dev

그리고 저는 다음과 같이 파일을 구성하였습니다.

  • /node_modules/* : 여기에는 방금 npm을 통해 설치한 경로.
  • /tests/bootstrap.js : 테스트 시작하기전에 실행할 내용들.
  • /tests/sample-test.js : 테스트 모듈(모든 파일은 *-test.js 의 형태를 따릅니다.).
  • /src/sample.js : 우리가 만든 모듈.
  • /qunit.html : 웹 테스트 시작 파일입니다.
  • /qunit.js : 노드 테스트 시작 파일입니다. ```

qunit.html 파일은 웹에서 실행하면 유닛테스트가 실행됩니다. qunit.js파일은 노드에서 유닛테스트가 실행됩니다. 그리고 각각 파일은 공통적으로 /tests/bootstrap.js파일을 불러오면 테스트가 실행됩니다. tests및에 있는 테스트 모듈의 경우 *-test.js의 파일명을 붙였고 여기서 *에 해당하는 파일은 /src/*에 1:1로 매칭됩니다.

Browser에서 QUnit 설정

qunit.html파일 내용은 다음과 같이 구성합니다.

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>QUnit Example</title>
<link rel="stylesheet" href="./node_modules/qunitjs/qunit/qunit.css" />
</head>
<body>

<div id="qunit"></div>

<script src="./node_modules/requirejs/require.js"></script>
<script type="text/javascript">
requirejs.config({
    baseUrl: "./tests",
    paths: {
        qunit: "../node_modules/qunitjs/qunit/qunit",
        src: "../src"
    },
    shim: {
        qunit: {
            exports: 'QUnit',
            init: function() {
                QUnit.config.autoload = false;
                QUnit.config.autostart = false;
            }
        }
    }
});
requirejs(['bootstrap']);
</script>
</body>
</html>

requirejs 설정에서 baseUrl을 ./tests로 정했기 때문에 qunit은 상대경로인 ../node_modules/qunitjs/qunit/qunit으로 정해주어야 합니다. 그리고 AMD를 통해 실행되어야 하기 때문에 shim에서 exports를 설정해야합니다.(QUnit은 AMD를 아직 지원하지 않습니다.) 그리고 QUnit은 기본적으로 소스를 불러올 때마다 실행하기 때문에 autoloadautostart설정을 꺼줘야 합니다.

Node에서 QUnit 설정

qunit.js파일은 다음과 같이 구성합니다.

var QUnit = require('qunitjs');
var requirejs = require('requirejs');
var StyledConsole = require('styled-console');

requirejs.config({
    baseUrl: "./tests",
    paths: {
        src: "../src"
    }
});

requirejs.define('qunit', QUnit); // requirejs에서 qunit 을 사용할 수 있도록 직접설정.

QUnit.config.autoload = false;
QUnit.config.autostart = false;

QUnit.log(function( details ) {
    if ( details.result ) {
        return;
    }

    var output = "<c:red>FAILED:</c:red> " + 
        (details.module ? details.module + ': ' : '') +
        (details.name ? details.name + ': ' : '') +
        (details.message ? details.message : '' );

    if (details.actual) {
        output += "expected: " + details.expected + ", actual: " + details.actual;
    }
    if (details.source) {
        output += "\n<c:sblack>" + details.source + '</c:sblack>';
    }
    console.log(StyledConsole(output).parse());
});

requirejs(['bootstrap']);

위의 브라우저 설정과 비슷합니다. 다만 여기서는 브라우저에서 실행하는 것이 아니라 requirejs에서 shim설정이 불가능합니다. 그래서 requirejs.define을 통해 직접 선언해주어야 합니다. 마찬가지로 autoload, autostart를 꺼주셔야 합니다. 그리고 QUnit은 브라우저 용으로 제작된 유닛테스트 도구라서 어떤 형태로 로그를 찍어줘야할지 함수를 설정해주어야 합니다. 그것이 QUnit.log라는 부분입니다. 저는 콘솔을 예쁘게 (나름) 색깔을 갖추기 위해 styled-console을 사용했습니다. 필요하신 분들은 다음 명령어를 통해 설치 가능합니다.

$ npm install styled-console --save-dev

/tests/bootstrap.js 구성

requirejs(['qunit', 'sample-test', 'other-sample-test', ... ], function(QUnit) {
	QUnit.load();
	QUnit.start();
});

단순히 QUnit(requirejs.config에서 설정한 이름)을 불러오고 테스트할 파일들을 위 형태에 맞춰 나열만 하면 됩니다.

/tests/sample-test.js구성

requirejs(
    ['qunit', 'src/sample'],
    function(QUnit, Sample) {
        QUnit.test('Sample Test Suite', function(assert) {
            assert.ok(Sample.getTrue());
            assert.equal(Sample.getTwo(), 1); // 일부러 에러날 경우.
       });
    }
);

위와 같이 src/sample.js 파일을 불러와서 자유롭게 테스트 내용을 작성하면 됩니다. 기본적으로 QUnit은 전역함수를 사용할 수 있도록 편의를 제공합니다만 (test라던가 ok라던가..) 저는 저런 단순한 이름은 전역변수를 사용했을 때 함수의 내용을 보장(작은 규모에선 괜찮으나 큰 규모로 커졌을 때..)할 수 없다고 생각이 들어 변수로 받아온 녀석만을 사용했습니다.

더 많은 Assert 관련 함수는 여기에서 찾아 볼 수 있습니다.

결과

브라우저 테스트

QUnit Browser test

Node 테스트

QUnit Node test

노드의 경우는 QUnit.log를 작성할 때, 성공한 것은 아예 출력을 막았기 때문에 실패한 결과만을 출력해줍니다.

마무리

굉장히 기본소스만을 활용해서 유닛테스트 환경을 만들었기 때문에 조금만 응용하면 다양하게 활용할 수 있을 것이라 생각됩니다. 일단 지금은 브라우저 테스트는 일일이 해야하지만 다음에 시간이 되면 다양한 브라우저 환경까지 자동화 해보려고 합니다. :)