KarmaとJasmineとWebpackでJavaScript(TypeScript)でユニットテストを実行してレポートやカバレッジを出力する

TypeScript Advent Calendar 2016 の 最終日 の記事を書かせて頂きました。

# はじめに

Javascript のユニットテストを初めてやります。
その上で調査したことを書きます。

# ゴール

TypeScript + Webpack + Karma + Jasmine
出力: テストレポート＆カバレッジレポート

# 作業ステップ

1. Jasmine 導入
2. Karma 導入(Karma + Jasmine)
3. Webpack導入(Karma + Webpack)
4. TypeScriptの導入(TypeScript + Webpack + Karma)

# 前提

- npm が使えること
- `node_modules/.bin` にパスが通っていること
    - ※ `npm -g` は使っていません
- TypeScript を HelloWorld レベルで使えること
- Webpack を HelloWorld レベルで使えること


# テスト対象コードの準備

```js:src/calc-utils.js
function add(x, y) {
  return x + y;
}
```

こちらをテスト対象にします。

# Jasmine導入

Jasmine とは、Javascript のテスティングフレームワークです。

```bash:ターミナル
$ npm install -D jasmine
```

## テストコード実装

```js:test/calc-utils-test.js
describe('関数のテスト', function() {
  // 成功するテスト
  it('1 + 1 = 2である', function(){
    expect(add(1, 1)).toBe(2);
  });

  // 失敗するテスト
  it('1 + 2 = 3である', function(){
    expect(add(1, 2)).toBe(2);
  });
});
```

## Hello World

以下のファイルを HTML で読み込むだけでテスト＆テスト結果が表示されるようです。

- node_modules/jasmine-core/lib/jasmine-core/jasmine.css
- node_modules/jasmine-core/lib/jasmine-core/jasmine.js
- node_modules/jasmine-core/lib/jasmine-core/jasmine-html.js
- node_modules/jasmine-core/lib/jasmine-core/boot.js

### HTML ファイルの作成

```html:index.html
<html>
  <head>
    <title>Jasmine Test</title>
    
    <link rel="stylesheet" type="text/css" href="./node_modules/jasmine-core/lib/jasmine-core/jasmine.css">
    <script src="./node_modules/jasmine-core/lib/jasmine-core/jasmine.js"></script>
    <script src="./node_modules/jasmine-core/lib/jasmine-core/jasmine-html.js"></script>
    <script src="./node_modules/jasmine-core/lib/jasmine-core/boot.js"></script>
    
    <script src="./src/calc-utils.js"></script>
    <script src="./test/calc-utils-test.js"></script>
  </head>
  <body>
  </body>
</html>
```

### ブラウザで開いてみる

![Screenshot from 2016-12-16 12-26-48.png](https://qiita-image-store.s3.amazonaws.com/0/53393/637b1df9-b0da-10ca-6d61-a6fb1e7d346e.png)

※以降 `index.html` は使用しません。

# Karma導入(Karma + Jasmine)

Node.js 上で動作するテストランナー
AngularJS の開発で使うために作られたらしいです。
テストを実行し、テストレポートを出力したり、カバレッジを取得したりという機能が備わっているようです。

```bash:ターミナル
$ npm install -D karma
```

## Hello World

### Karma 用の設定ファイル作成

```bash:ターミナル
$ karma init

Which testing framework do you want to use ?
Press tab to list possible options. Enter to move to the next question.
> jasmine

Do you want to use Require.js ?
This will add Require.js plugin.
Press tab to list possible options. Enter to move to the next question.
> no

Do you want to capture any browsers automatically ?
Press tab to list possible options. Enter empty string to move to the next question.
> Chrome
> 

What is the location of your source and test files ?
You can use glob patterns, eg. "js/*.js" or "test/**/*Spec.js".
Enter empty string to move to the next question.
> src/*.js
> test/*.js
> 

Should any of the files included by the previous patterns be excluded ?
You can use glob patterns, eg. "**/*.swp".
Enter empty string to move to the next question.
> 

Do you want Karma to watch all the files and run the tests on change ?
Press tab to list possible options.
> yes

Config file generated at "/home/chibi/unit-test/karma.conf.js".
```

### Karma 実行

```bash:ターミナル
$ karma start
```

![Screenshot from 2016-12-16 13-32-03.png](https://qiita-image-store.s3.amazonaws.com/0/53393/e911434d-3bfe-cb93-94b2-7217ada179ea.png)

なんかブラウザが立ち上がった

```bash:ターミナル
$ karma start
16 12 2016 13:36:27.571:WARN [karma]: No captured browser, open http://localhost:9876/
16 12 2016 13:36:27.609:WARN [karma]: Port 9876 in use
16 12 2016 13:36:27.614:INFO [karma]: Karma v1.3.0 server started at http://localhost:9877/
16 12 2016 13:36:27.616:INFO [launcher]: Launching browser Chrome with unlimited concurrency
16 12 2016 13:36:27.676:INFO [launcher]: Starting browser Chrome
16 12 2016 13:36:29.778:INFO [Chrome 55.0.2883 (Linux 0.0.0)]: Connected on socket /#_5lnmqR9SDXJe0M6AAAA with id 46430444
Chrome 55.0.2883 (Linux 0.0.0): Executed 0 of 2 SUCCESS (0 secs / 0 secs)
[1AChrome 55.0.2883 (Linux 0.0.0): Executed 1 of 2 SUCCESS (0 secs / 0.002 secs)
[1AChrome 55.0.2883 (Linux 0.0.0) 関数のテスト 1 + 2 = 3である FAILED
	Expected 3 to be 2.
	    at Object.<anonymous> (test/calc-utils-test.js:7:23)
Chrome 55.0.2883 (Linux 0.0.0): Executed 2 of 2 (1 FAILED) (0 secs / 0.004 secs)
[1AChrome 55.0.2883 (Linux 0.0.0): Executed 2 of 2 (1 FAILED) (0.086 secs / 0.004 secs)

```

なんか上手くいったっぽい

### ブラウザが閉じれない(閉じるとまた開く)

karma がウォッチしてて、
コードが変わるたびにテストしてくれるようにしているためですかね。
karma の設定ファイルの以下の部分を `true` に変えてあげることで実行を続けることがなくなりました。

```diff:karma.conf.js
    // Continuous Integration mode
    // if true, Karma captures browsers, runs the tests and exits
-   singleRun: true,
+   singleRun: true,
```

## PhantomJS

PhantomJS という Headless ブラウザです。
`karma start` を実行するたびに Chrome が起動していたので、PhantomJS を使うようにしました。

```bash:ターミナル
$ npm install -D karma-phantomjs-launcher
```

### karma.conf.js を編集


```diff:karma.conf.js
    // start these browsers
    // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
-   browsers: ['Chrome'],
+   browsers: ['PhantomJS'],
```

実行すると、コンソールログのみに！

## JUnit形式レポート出力

```bash:ターミナル
$ npm install -D karma-junit-reporter
```

### karma.conf.js を編集

```diff:karma.conf.js
    // test results reporter to use
    // possible values: 'dots', 'progress'
    // available reporters: https://npmjs.org/browse/keyword/karma-reporter
-   reporters: ['progress'],
+   reporters: ['progress', 'junit'],
+
+   junitReporter: {
+     outputDir: 'report'
+   },
```

### 出力結果

```bash:ターミナル
$ ls report/
TESTS-PhantomJS_2.1.1_(Linux_0.0.0).xml
```

```bash:ターミナル
$ cat report/TESTS-PhantomJS_2.1.1_\(Linux_0.0.0\).xml 
<?xml version="1.0"?>
<testsuite name="PhantomJS 2.1.1 (Linux 0.0.0)" package="" timestamp="2016-12-16T05:34:43" id="0" hostname="Chibintu" tests="2" errors="0" failures="1" time="0.004">
  <properties>
    <property name="browser.fullName" value="Mozilla/5.0 (Unknown; Linux x86_64) AppleWebKit/538.1 (KHTML, like Gecko) PhantomJS/2.1.1 Safari/538.1"/>
  </properties>
  <testcase name="関数のテスト 1 + 1 = 2である" time="0" classname="PhantomJS_2_1_1_(Linux_0_0_0).関数のテスト"/>
  <testcase name="関数のテスト 1 + 2 = 3である" time="0.004" classname="PhantomJS_2_1_1_(Linux_0_0_0).関数のテスト">
    <failure type="">Expected 3 to be 2.
test/calc-utils-test.js:7:27
loaded@http://localhost:9876/context.js:151:17
</failure>
  </testcase>
  <system-out>
    <![CDATA[
]]>
  </system-out>
  <system-err/>
```

## カバレッジ出力

```bash:ターミナル
$ npm install -D karma-coverage
```

### karma.conf.js を編集

```diff:karma.conf.js
    // preprocess matching files before serving them to the browser
    // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
    preprocessors: {
+     'src/*.js': ['coverage']
    },

    // test results reporter to use
    // possible values: 'dots', 'progress'
    // available reporters: https://npmjs.org/browse/keyword/karma-reporter
-   reporters: ['progress', 'junit'],
+   reporters: ['progress', 'junit', 'coverage'],

    junitReporter: {
      outputDir: 'report'
    },
+
+   coverageReporter: {
+     dir: 'report',
+     reporters: [
+       { type: 'html' },
+       { type: 'cobertura' }
+     ]
+   },
```

### 出力結果

```bash:ターミナル
$ ls report/
PhantomJS 2.1.1 (Linux 0.0.0)  TESTS-PhantomJS_2.1.1_(Linux_0.0.0).xml

$ ls report/PhantomJS\ 2.1.1\ \(Linux\ 0.0.0\)/
cobertura-coverage.xml

$ cat report/PhantomJS\ 2.1.1\ \(Linux\ 0.0.0\)/cobertura-coverage.xml 
<?xml version="1.0" ?>
<!DOCTYPE coverage SYSTEM "http://cobertura.sourceforge.net/xml/coverage-04.dtd">
<coverage lines-valid="2"  lines-covered="2"  line-rate="1"  branches-valid="0"  branches-covered="0"  branch-rate="1"  timestamp="1481869473013" complexity="0" version="0.1">
<sources>
	<source>/home/chibi/unit-test</source>
</sources>
<packages>
	<package name="src"  line-rate="1"  branch-rate="1" >
	<classes>
		<class name="calc-utils.js"  filename="src/calc-utils.js"  line-rate="1"  branch-rate="1" >
		<methods>
			<method name="add"  hits="2"  signature="()V" >
				<lines><line number="1"  hits="2" /></lines>
			</method>
		</methods>
		<lines>
			<line number="1"  hits="1"  branch="false" />
			<line number="2"  hits="2"  branch="false" />
		</lines>
		</class>
	</classes>
	</package>
</packages>
</coverage>
```

![Screenshot from 2016-12-16 19-47-16.png](https://qiita-image-store.s3.amazonaws.com/0/53393/4daa32fb-d63c-6476-deb8-3c3655a7d61f.png)

# Webpack導入(Karma + Webpack)

ここでは `src/` にファイルが1個しかないので Webpack 効果は薄いけど・・・

```bash:ターミナル
$ npm install -D webpack
$ npm install -D karma-webpack
```

## テスト対象コードを変更

Webpack を導入するにあたり、テスト対象コードを module 化します。

```js:src/calc-utils.js(変更後)
module.exports = {
  add: function() {
    return x + y;
  }
}
```

## テストコードを変更

```js:test/calc-utils-test.js(変更後)
var calcutils = require('../src/calc-utils');

describe('関数のテスト', function() {
  // 成功するテスト
  it('1 + 1 = 2である', function(){
    expect(calcutils.add(1, 1)).toBe(2);
  });

  // 失敗するテスト
  it('1 + 2 = 3である', function(){
    expect(calcutils.add(1, 2)).toBe(2);
  });
});
```

## webpack.config.js を作成

```js:webpack.config.js
module.exports = {
  entry: "./src/calc-utils.js",
  output: {
    filename: "bundle.js"
  }
};
```

## karma.conf.js を変更

module 化したことによって `src/` をロードする必要がなくなりました。

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

また、Webpack を導入したことによって `require` を解決するために `karma-webpack` を使用します。


```diff:karma.conf.js
    // preprocess matching files before serving them to the browser
    // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
    preprocessors: {
-     'src/*.js': ['coverage']
+     'src/*.js': ['coverage', 'webpack']
+     'test/*.js': ['webpack']
    },
```

## カバレッジが出力できなくなってしまった・・・

```bash:ターミナル
$ cat report/PhantomJS\ 2.1.1\ \(Linux\ 0.0.0\)/cobertura-coverage.xml 
<?xml version="1.0" ?>
<!DOCTYPE coverage SYSTEM "http://cobertura.sourceforge.net/xml/coverage-04.dtd">
<coverage lines-valid="0"  lines-covered="0"  line-rate="1"  branches-valid="0"  branches-covered="0"  branch-rate="1"  timestamp="1481876370794" complexity="0" version="0.1">
<sources>
	<source>/home/chibi/unit-test</source>
</sources>
<packages>
</packages>
</coverage>
```

こんな感じ・・・
色々調べた結果、2つのパッケージを導入

## Karma + Webpack で Coverage を出力する

```bash:ターミナル
$ npm install -D karma-sourcemap-loader
$ npm install -D istanbul-instrumenter-loader
```

### karma.conf.js を編集

```diff:karma.conf.js
    // preprocess matching files before serving them to the browser
    // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
    preprocessors: {
-     'src/*.js': ['coverage', 'webpack'],
-     'test/*.js': ['webpack']
+     'src/*.js': ['coverage', 'webpack', 'sourcemap'],
+     'test/*.js': ['webpack', 'sourcemap']
    },
+
+
+   // Webpack settings.
+   webpack: {
+     devtool: 'inline-source-map',
+     module: {
+       preLoaders: [
+         {
+           test: /\.js$/,
+           exclude: /(test|node_modules)/,
+           loader: 'istanbul-instrumenter'
+         }
+       ]
+     }
+   },
```

~~`webpack: {}` の部分は `webpack.config.js` の中でも良さそうな気がする~~
試したらダメだった。 `karma.conf.js` に書かないとダメみたい。

`karma.conf.js` から `webpack.config.js` を参照するわけではなくて、
`karma.conf.js` に記述されている `webpack` の設定を読むだけのようです。

### karma.conf.js を微修正

`istanbul-instrumenter-loader` 自体がカバレッジに関するパッケージなので、
以下の行を削除してもカバレッジが出力されるようになるっぽい

```diff:karma.conf.js
    // preprocess matching files before serving them to the browser
    // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
    preprocessors: {
-     'src/*.js': ['coverage', 'webpack', 'sourcemap'],
      'test/*.js': ['webpack', 'sourcemap']
    },
```

ここまでで JavaScript + Webpack + Karma + Jasmine

# TypeScriptの導入(TypeScript + Webpack + Karma)

```bash:コンソール
$ npm install -D typescript
```

Webpackを使っているので以下もインストール

```bash:コンソール
$ npm install -D ts-loader
```

更にKarmaも使っているので以下もインストールしておく

```bash:コンソール
$ npm install -D karma-typescript-preprocessor
```

更に更に TypeScript で Jasmine のコードを扱うので型定義ファイルをインストールしておく

```bash:コンソール
$ npm install -D @types/jasmine
```

## JavaScriptのコードをTypeScriptに置き換える

### src

```js:src/calc-utils.js(置き換え前)
module.exports = {
  add: function() {
    return x + y;
  }
}
```

```ts:calc-utils.ts(置き換え後)
export class CalcUtils {
  public static add(x: number, y: number): number {
    return x + y;
  }
}
```

### test

```js:test/calc-utils-test.js(置き換え前)
var calcutils = require('../src/calc-utils');

describe('関数のテスト', function() {
  // 成功するテスト
  it('1 + 1 = 2である', function(){
    expect(calcutils.add(1, 1)).toBe(2);
  });

  // 失敗するテスト
  it('1 + 2 = 3である', function(){
    expect(calcutils.add(1, 2)).toBe(2);
  });
});
```

```ts:test/calc-utils-test.ts(置き換え後)
/// <reference path="../node_modules/@types/jasmine/index.d.ts" />
import {CalcUtils} from '../src/calc-utils';

describe('関数のテスト', function() {
  it('1 + 1 = 2である', function(){
    expect(CalcUtils.add(1, 1)).toBe(2);
  });

  it('1 + 2 = 3である', function(){
    expect(CalcUtils.add(1, 2)).toBe(2);
  });
});
```

## tsconfig.json を作成

```json:tsconfig.json
{
  "compileOnSave": true,
  "compilerOptions": {
    "target": "ES5",
    "noImplicitAny": false,
    "removeComments": false,
    "sourceMap": false
  },
  "exclude": [
    "node_modules",
    "test"
  ],
  "files": [
    "./src/calc-utils.ts"
  ]
}
```

## karma.conf.js を変更

```diff:karma.conf.js
    // Webpack settings.
    webpack: {
      devtool: 'inline-source-map',
+     resolve: {
+       extensions: ['', '.ts', '.js', ".tsx"]
+     },
      module: {
+       loaders: [
+         {
+           test: /\.ts$/,
+           loader: 'ts-loader'
+         }
+       ],
        postLoaders: [
          {
-           test: /\.js$/,
+           test: /\.ts$/,
            exclude: /(test|node_modules)/,
            loader: 'istanbul-instrumenter'
          }
        ]
      }
    },
```

Karma + Webpack で TypeScript をビルドできるようにするために、
`webpack.config.json` に記述するべき TypeScript用の設定を `karma.conf.js` に記述する。
Webpack のみでビルドするときは `webpack.config.json` にも記述が必要

## 出力結果

![4f4fc66d-5c4a-50fd-964b-f8b691332aec.png](https://qiita-image-store.s3.amazonaws.com/0/53393/8ea128fe-5752-a9ee-c4f4-3bf3104ecbf4.png)

ちゃんと TypeScript のコードでカバレッジが取れている。

# 最後に

```json:package.json
"devDependencies": {
  "@types/jasmine": "^2.5.38",
  "istanbul-instrumenter-loader": "^1.1.0",
  "jasmine": "^2.5.2",
  "karma": "^1.3.0",
  "karma-chrome-launcher": "^2.0.0",
  "karma-coverage": "^1.1.1",
  "karma-jasmine": "^1.1.0",
  "karma-junit-reporter": "^1.2.0",
  "karma-phantomjs-launcher": "^1.0.2",
  "karma-sourcemap-loader": "^0.3.7",
  "karma-typescript-preprocessor": "^0.3.0",
  "karma-webpack": "^1.8.0",
  "ts-loader": "^1.3.3",
  "typescript": "^2.1.4",
  "webpack": "^1.14.0"
}
```
`package.json` のパッケージたちです。

# 参考にしたURL

https://tech.recruit-mp.co.jp/front-end/post-5299/
http://blog.odoruinu.net/2014/11/27/test-webpack-based-application-with-karma/
http://qiita.com/howdy39/items/cdd5b252096f5a2fa438

ありがとうございました！