Outsider's Dev Story

Stay Hungry. Stay Foolish. Don't Be Satisfied.
RetroTech 팟캐스트 44BITS 팟캐스트

Karma 테스트 러너 사용하기

어느새 잊고 있었지만 처음 AngularJS를 보기 시작했을 때 양방향 바인딩이나 디렉티브 같은 AngularJS의 기능들도 인상적이었지만 유닛테스트부터 e2e테스트까지 테스트 인프라가 완벽하게 갖추어져 있어서 "이놈들 정말 제대로 준비해서 만들었구나!"하는 생각이 들었다. 기존에서 QUnit등의 많은 테스트 프레임워크가 있지만 정작 테스트를 작성하면 항상 아쉬운 부분이 생기고 PhantomJS등의 등장으로 자동화가 많이 나아졌음에도 이것저것 하다보면 항상 Selenium을 도입하게 되는 게 현실이다.

AngularJS는 강력한 프레임워크뿐만 아니라 유닛테스트를 위한 Karma와 E2E테스트를 위한 Protractor를 제공해서 테스트환경까지 준비해놓고 있다. 물론 Karma와 Protractor를 AngularJS에서만 사용할 수 있는 건 아니다. 여기서 Karma가 담당하는 유닛테스트는 각 메서드나 기능단위로 테스트를 작성하는 것을 의미하고 Protractor가 담당하는 E2E 테스트는 Selenium 등으로 테스트하듯이 애플리케이션의 사용자 시나리오 전체를 테스트하는 것을 의미한다. 이 글에서 다룰 내용은 Karma에 대한 얘기인데 초반에는 Testacular라는 이름으로 릴리즈 되었지만, 후에 Karma라는 이름으로 변경되었다.

Karma란?

Karma 저장소보면 자바스크립트 테스트 러너라고 나와 있다. 이 정의가 주는 의미는 Karma는 유닛테스트용 프레임워크가 아니라 작성한 테스트를 실행해 주는 역할을 한다고 보면 된다. 그래서 Karma를 사용하더라도 테스트 자체는 기존에 익숙한 QUnit, Mocha, Jasmine을 그대로 사용할 수 있고 테스트 실행만 karma를 이용해서 하게 된다. 서버사이드에 비해서 프론트앤드 자바스크립트는 그 특성상 테스트 환경이 상당히 열악했는데 Karma 덕에 서버사이드의 비교해도 부족함이 없을 정도로 진보했다고 본다.

Karma 환경 설정

Karma를 사용하려면 일단 설치해야 하는데 Node.js로 만들어졌으므로 npm install -g karma로 설치할 수 있다. 설치하고 나면 이제 커맨드라인에서 karma라는 명령어를 사용할 수 있다.

$ karma --version
Karma version: 0.10.9

일단 테스트를 하려면 테스트 대상이 필요하므로 테스트를 할 간단한 애플리케이션을 다음과 같이 만들어 보자. 여기서는 jQuery와 Angular를 사용했는데 이는 Bower를 이용해서 설치했고 그래서 각 라이브러리 파일은 components 폴더 아래 존재한다. (bower install jquery angular) 애플리케이션은 모두 src 디렉터리 아래 있다.


<!DOCTYPE html>
<html lang="en" ng-app="MyApp">
<head>
  <title>Karma Demo</title>
</head>
<body ng-controller="MainController">

  
  
  
</body>
</html>
// src/app.js
angular.module('MyApp', [])
  .factory('MathService', function () {
    return {
      sum: function(a, b) {
        return a + b;
      }
    };
  });

Karma를 사용하려면 karma.conf.js파일이 필요한데 직접 만들기는 복잡하므로 karma init명령어를 사용해서 만들자.

karma init으로 karma.conf.js를 만드는 화면

karma init을 실행하면 인터렉티브하게 질문에 답을 하면서 필요한 설정을 할 수 있다. Karma의 커맨드라인은 아주 잘 만들어져 있는데 위에서 보듯이 가장 먼저 사용할 테스트 프레임워크를 묻는다. 앞에서 얘기했듯이 Karma는 테스트 러너일 뿐이므로 테스트 프레임워크는 따로 선택해서 사용해야 한다. 여기서는 Mocha를 사용했는데 사용할 수 있는 옵션은 탭을 누르면 순서대로 바뀌므로 원하는 옵션을 바로 선택할 수 있다. 기본적으로 Jasmine, QUnit, Mocha를 지원하지만, 그 외 테스트 프레임워크를 사용하려면 Karma 어댑터가 필요하다.(위 화면에서도 mocha를 선택하자 karma-mocha가 설치 안 되어 있다고 경고가 나온 걸 볼 수 있다.) 이어서 RequireJS의 사용 여부와 테스트에 사용할 브라우저, 소스파일과 테스트 파일을 포함해서 제외할 파일 패턴을 선택한다. 마지막으로 대상 파일을 감시하고 있다가 수정하면 테스트를 자동으로 실행할 것인지 아닌지를 묻고 karma.conf.js가 만들어진다.

// Karma configuration
// Generated on Mon Jan 20 2014 22:11:41 GMT+0900 (KST)

module.exports = function(config) {
  config.set({

    // base path, that will be used to resolve files and exclude
    basePath: '',


    // frameworks to use
    frameworks: ['mocha'],


    // list of files / patterns to load in the browser
    files: [
      'src/**/*.js',
      'test/**/*.test.js'
    ],


    // list of files to exclude
    exclude: [

    ],


    // test results reporter to use
    // possible values: 'dots', 'progress', 'junit', 'growl', 'coverage'
    reporters: ['progress'],


    // web server port
    port: 9876,


    // enable / disable colors in the output (reporters and logs)
    colors: true,


    // level of logging
    // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
    logLevel: config.LOG_INFO,


    // enable / disable watching file and executing tests whenever any file changes
    autoWatch: true,


    // Start these browsers, currently available:
    // - Chrome
    // - ChromeCanary
    // - Firefox
    // - Opera (has to be installed with `npm install karma-opera-launcher`)
    // - Safari (only Mac; has to be installed with `npm install karma-safari-launcher`)
    // - PhantomJS
    // - IE (only Windows; has to be installed with `npm install karma-ie-launcher`)
    browsers: ['Chrome', 'Firefox'],


    // If browser does not capture in given timeout [ms], kill it
    captureTimeout: 60000,


    // Continuous Integration mode
    // if true, it capture browsers, run tests and exit
    singleRun: false
  });
};

위 파일이 만들어진 karma.conf.js파일이다. 설정은 직관적이므로 각 필드를 일일이 설명하지는 않겠다.

mocha 환경 설정

Mocha를 사용하기 위해서 간단한 설정파일을 만들어 보자.

// mocha.conf.js
window.mocha.setup({
  timeout: 5000
});

mocha 객체에 설정 값을 넣어서 만들었다. 이 파일이 반드시 필요한 것은 아니지만, mocha를 제어하기 위한 설정들이 필요하므로 별도의 파일을 만들어서 관리하는 편이 낫다.

chai 환경 설정

mocha는 테스트 프레임워크이지만 assertion 라이브러리는 또 별도이므로 필요한 assertion 라이브러리를 가져다 써야 하는데 여기서는 Chai를 쓰자. 나는 보통 should.js를 좋아하지만, 브라우저에서는 Chai가 더 쓰기 편한 걸로 알고 있다.

// chai.conf.js
var expect = chai.expect;

chai.conf.js파일도 mocha처럼 chai의 환경설정의 역할을 한다.(정확히는 환경설정은 아니지만...) chai는 assertion 형식이 여러 가지가 있어서 should, expect, assert 중에서 선택할 수 있는데 이 정의를 모든 테스트파일마다 정의하는 대신에 이 파일 하나에서 정의해서 전체를 제어하려는 의도이다. 그러니까 위 코드는 chai의 expect를 사용한다는 의미다.

테스트 작성

테스트를 작성하기 전에 준비할 것이 좀 있긴 하지만 설명하기 복잡하므로 일단 테스트부터 먼저 작성해 보자. 테스트 파일은 test 폴더 아래 unit폴더를 두고 그 아래 작성했다.

// test/unit/app.test.js
describe('MyApp', function() {
  'use strict';

  beforeEach(module('MyApp'));

  describe('MathService', function() {
    it('sum(2,4) should be 6', inject(function(MathService) {
      expect(MathService.sum(2, 4)).be.equal(6);
    }));

    it('sum(3,6) shoule be 9', inject(function(MathService) {
      expect(MathService.sum(3, 6)).be.equal(9);
    }));
  });
});

앞에서 작성했던 MathService를 테스트한 간단한 코드다. 5번 라인의 module('MyApp')는 정확히는 angular.mock.module('MyApp')이고 이 함수는 ngMock에 포함된 함수이다. AngularJS에서는 자동으로 의존성 주입을 해주지만 테스트에서는 명시적으로 필요한 모듈을 주입할 수 있어야 하므로 ngMock이 필요하다. 그래서 테스트를 실행하기 전 단계인 beforeEach에서 MyApp 모듈의 설정을 불러온 것이다.

이제 각 테스트를 보면 inject함수를 사용한 것을 볼 수 있는데 이 함수도 angular.mock.inject에 있는 함수다. 여기서는 MathService라는 서비스를 테스트할 것이므로 앞에서 로드한 MyApp에서 MathServiceinject함수로 주입하고 이를 이용해서 테스트를 수행한다.

Karma 테스트 연동

이제 준비가 모두 완료되었으니 Karma와 연동해서 테스트를 수행하자. Karma를 연동하기 전에 테스트를 위한 자바스크립트 라이브러리를 먼저 가져와야 한다. 이는 Bower를 이용해서 bower install angular-mocks chai로 설치를 하자.(mocha는 karma 쪽에서 설치하므로 브라우저용 라이브러에서는 필요 없다.)

이어서 앞에서 나왔던 karma-mocha를 설치해야 하는데 앞에서 나온 명령어대로 npm install karma-mocha 명령어로 설치한다.

마지막으로 테스트를 위해서 앞에서 만들었던 karma.conf.js파일을 약간 수정해 보자.

// list of files / patterns to load in the browser
files: [
  'src/**/*.js',
  'test/**/*.test.js'
],

앞에서 위처럼 대상 파일의 패턴을 지정했었는데 이 부분을 다음과 같이 변경하자.

// list of files / patterns to load in the browser
files: [
  // dependencies
  'components/jquery/jquery.js',
  'components/angular/angular.min.js',

  // application code
  'src/**/*.js',

  // test dependencies
  'components/angular-mocks/angular-mocks.js',
  'components/chai/chai.js',
  'test/mocha.conf.js',
  'test/chai.conf.js',

  // tests
  'test/**/*.test.js'
],

files에 지정한 파일들은 테스트를 실행할 때 브라우저에 모두 로딩이 되므로 애플리케이션이 의존성을 가진 모든 파일은 여기에 지정해 주어야 한다. 보통은 자바스크립트만으로 충분하겠지만, CSS도 테스트에 필요하다면 여기에 같이 지정하면 된다. 주석으로 구분해 놨는데 애플리케이션이 의존하고 있는 jQuery와 AngularJS 파일을 불러오고 애플리케이션 코드를 모두 불러왔다. 그리고 테스트에 필요한 angular-mocks와 chai 라이브러리를 불러오고 앞에서 작성한 mocha와 chai의 설정파일로 로드한다. 그리고 마지막으로 테스트파일에 대한 패턴을 넣은 것이다. 파일 경로는 karma.conf.js파일의 위치에서 상대경로로 입력하면 된다.

설정이 모두 완료되었으므로 프로젝트 루트 경로에서 karma start를 실행하면 다음과 같이 브라우저가 실행된다. 설정에서 Chrome과 Firefox를 지정했으므로 둘다 실행이 되었고 Karma와 연결되어 있다고 나온 것을 볼 수 있다.

크롬과 파이어폭스가 실행된 화면

커맨드 라인에서는 다음과 같이 나온다.

karma를 실행한 커맨드라인

크롬과 파이어폭스가 실행되었다는 메시지가 나오고 각각 총 2개의 테스트에서 2개가 모두 성공했다고 표시된다.

이 부분이 Karma의 정말 좋은 점인데 Karma는 브라우저 인스턴스를 띄우고 Karma 서버와 브라우저가 Socket.io로 통신을 한다. 그래서 테스트는 실제 브라우저(혹은 Headless 브라우저)에서 수행하고 그 결과만 커맨드라인으로 전달된다. 테스트나 애플리케이션 파일이 변경 시 테스트가 수행되도록 설정해 놓으면 소스작성만 계속하면서 커맨트라인만 보고 있으면 바로바로 테스트를 결과를 알 수 있다. 일반적으로 브라우저 자바스크립트를 작성할 때 전체 애플리케이션에 대한 테스트파일 전부를 한꺼번에 확인하기 어려운 문제가 Karma를 사용하면 아주 쉽게 해결된다.

karma 콘솔에 console.log가 출력된 화면

디버깅 등을 목적으로 console.log로 출력한 내용도 모두 커맨드라인에 전송돼서 위와 같이 출력된다.

맺음말

그동안 AngularJS를 쓰면서 테스트를 잘 작성하지 않았던 것은 AngularJS에 대한 이해도가 부족해서 그런 것도 있지만 뭘 테스트할지 몰라서 그랬던 부분이 더 크다. 예를 들어 서버에서 데이터를 받아와서 화면에 뿌려준다고 하면 테스트 데이터를 이용해서 받아온 데이터로 DOM 조작이 제대로 이뤄져서 화면에 표현되었는가(보통 함수 내에서 여기까지 수행하므로)를 테스트하게 되지만 AngularJS의 양방향 바인딩 덕에 스코프 내에 변수만 할당하면 자동으로 화면에 바인딩이 된다. 화면에서 표현식을 잘못 사용하는 것은 테스트할 이유가 없고 변수할당은 너무 간단하므로 역시 테스트할 필요가 없고 할당한 변수를 화면에 표시하는 것은 AngularJS의 몫이므로 내가 테스트할 이유가 없다. 이렇다 보니 비즈니스 로직이 아주 간결해져서 정확히 어떤 부분을 테스트할지 몰라서 그동안은 테스트하지 않았다. 그래도 몇 달 AngularJS를 만지다 보니 이제는 좀 감이 왔다.

한참 써본 결과 Karma는 정말 강력하다. 개발자마다 테스트에 대한 견해차가 있긴 하지만 AngularJS를 사용한다면 Karma도 꼭 써보길 바란다. 좀 만져보니 처음 설정파일이 복잡해서 그렇지(Angular 예제 프로젝트 등을 보면...) 막상 약간 만져보면 진입 장벽이 별로 높지 않고 초반에 설정만 한번 이해하고 나면 그 뒤로는 굳이 만질 일도 거의 없다.

2014/01/23 23:53 2014/01/23 23:53