$shibayu36->blog;

クラスター株式会社のソフトウェアエンジニアです。エンジニアリングや読書などについて書いています。

Emacsで今編集しているJSのテストのみ実行する(Karma + Mocha環境の場合)

blog.shibayu36.org

前回の記事でKarma, Mocha, Chaiを使ったJSのユニットテスト環境を作ることができた。しかしテストを書き続けていると、「手元で全体のテストを再実行するのに時間がかかる」という問題が起こった。そこで今回は「今編集中のテストのみをEmacsから実行する」という作戦で問題を解決しようと考えた。

今回のサンプルコードは https://github.com/shibayu36/typescript-project-sample/tree/9e6baf1ebc9cd60083515918b23b6cb1dc24cea8 にあるので参考に。

課題

  • JSのテストをずっと書き続けていると全体のテストを実行するのに10〜数十秒程度かかるようになってくる
  • 手元でkarma startを使ってテストをしていると、ファイル変更のたびにテストを実行してくれるがkarma.conf.jsで指定されたファイル全部のテストを実行してしまう
  • 手元で開発中はまさに今編集しているテストのみ実行してくれるだけで良いのに、全体のテストを実行するのは無駄である
    • CIで全てのテストを実行しているというのは前提として

解決案

  • mochaコマンドのMochaのusageによると、grepというオプションでテストの絞り込みが出来る
  • Karmaを使っているので、直接mochaコマンドを使うことはできないが、 Karma - Configuration File を読むと、karma runの指定によってmochaに引数を渡すことが出来る
  • ならばEmacs上で今編集中のファイルから、カーソルの直近のdescribeの名前を抜き出して、それをgrepに渡すことで編集中のテストのみを実行できるのではないか

というので解決ができそうなのでやってみた。

テストファイル例

テストファイルは非常にシンプルなものを二つ作っておく。

module1という関数はmodule1という文字列を返し、 module2という関数はmodule2という文字列を返すとすると、これをテストするファイルは以下のようになる。

src/ts/test/module1.ts

/// <reference path="../../../typings/browser.d.ts" />

"use strict";

import { assert } from "chai";
import module1 from "../module1";

describe("module1 default function", () => {
    it("returns module1", () => {
        let expect: string;
        expect = 'module1';
        assert.equal(module1(), expect);
    });
});

src/ts/test/module2.ts

/// <reference path="../../../typings/browser.d.ts" />

"use strict";

import { assert } from "chai";
import module2 from "../module2";

describe("module2 default function", () => {
    it("returns module2", () => {
        let expect: string;
        expect = 'module2';
        assert.equal(module2(), expect);
    });
});

karma runを使って一部テストのみを実行する

上記のテスト例で、一部のテストのみをKarma + Mochaの環境で実行してみる。

mochaコマンドのMochaのusageによると、grepというオプションでpatternを指定すれば、patternにマッチするテストのみを実行することができる。この絞り込みはdescribeやitに適用されるみたい(ちゃんと調べてはいない)。

つまり、以下のようなコマンドを発行すれば、test/ディレクトリ以下でマッチするテストのみを実行することができる。

mocha --grep 'module1 default function' test/**/*


これができることは分かったけど、現状はkarmaをテストランナーとして利用しているため、このコマンドを直に打つことが出来ない。次にこれを解決する。

Karma - Configuration File を読むと、client.argsというものを利用すれば、利用しているテストフレームワークに引数を渡すことができるようだ。今回はテストフレームワークとしてmochaを利用しているので、これを利用してgrepオプションを渡せば良い。

このclient.argsはkarma runの引数として渡しながら実行もできる。見てみると -- 以降のオプションは全てclient.argsとしてテストフレームワークに渡されるようだ。

client.argsはkarma startには渡せないが、karma runだったら渡すことができる。karma runとは何かはkarma run --helpすれば分かるのだけど、既にstartで起動しているサーバーを利用して、テストを実行してくれるもののようだ。


いろいろ分かったところで、実際に使ってみる。まずはkarma startでサーバを起動する。

$(npm bin)/karma start
24 04 2016 13:50:53.442:INFO [framework.browserify]: registering rebuild (autoWatch=true)
24 04 2016 13:50:55.885:INFO [framework.browserify]: 208523 bytes written (0.41 seconds)
24 04 2016 13:50:55.886:INFO [framework.browserify]: bundle built
24 04 2016 13:50:55.888:WARN [karma]: No captured browser, open http://localhost:9876/
24 04 2016 13:50:55.892:INFO [karma]: Karma v0.13.22 server started at http://localhost:9876/
24 04 2016 13:50:55.902:INFO [launcher]: Starting browser Chrome
24 04 2016 13:50:57.066:INFO [Chrome 50.0.2661 (Mac OS X 10.10.5)]: Connected on socket /#9k4SvBn1G2VitcBsAAAA with id 61120412
Chrome 50.0.2661 (Mac OS X 10.10.5): Executed 2 of 2 SUCCESS (0.013 secs / 0.001 secs)

Chromeで起動して、2つのテストを実行したようだ。module1とmodule2のテストがあるので、2つのテストが実行されるのは意図通り。

続いてこのサーバを起動したままにしておいて、karma runを利用してmodule1のテストだけを実行してみる。grepを使って、"module1 default function"というのを絞り込めば良い。

$(npm bin)/karma run -- --grep 'module1 default function'
[2016-04-24 13:53:21.084] [DEBUG] config - Loading config /Users/shibayu36/development/src/github.com/shibayu36/typescript-project-sample/karma.conf.js
Chrome 50.0.2661 (Mac OS X 10.10.5): Executed 1 of 1 SUCCESS (0.005 secs / 0.001 secs)

これで先ほど起動していたサーバを使ってテストを行い、実際に1つのテストが実行されている。repoterがテスト名を出してくれないので分からないが、console.logとかを使うと実際にdescribeでmodule1 default functionと命名しているテストのみを実行してくれている。


これでkarma + mochaの環境で一部のテストのみを実行することが出来た。まとめると以下のコマンドを使うだけ。

$(npm bin)/karma start # こちらは起動しっぱなしにしておく
$(npm bin)/karma run -- --grep 'テスト名'

Emacs上でいま編集中のテストのみを実行する

ここまでで、describeのテスト名を指定すれば一部のテストを実行できるということが分かった。ならEmacs上で今編集中のファイルから、カーソルの直近のdescribeの名前を抜き出して、それをgrepに渡すことで編集中のテストのみを実行できる。

これを実行するためには以下の様な関数を定義しておけば良い。ただし、「プロジェクトがgitで管理されている」、「quickrunがemacsにインストールされている」ことが必要。

;; gitで管理されているrepositoryのトップディレクトリを探すUtility
(defun git-root-directory ()
  (cond ((git-project-p)
         (chomp
          (shell-command-to-string "git rev-parse --show-toplevel")))
        (t
         "")))

(defun run-js-mocha-describe-test ()
  (interactive)
  (let* ((topdir (git-root-directory))
         (test-grep-args nil))
    (save-excursion
      (when (or
             ;; 直近もしくは直後でdescribe('テスト名')となっている場所を探し、テスト名を抜き出す
             (re-search-backward "\\bdescribe(\s*[\"']\\(.*?\\)[\"']" nil t)
             (re-search-forward "\\bdescribe(\s*[\"']\\(.*?\\)[\"']" nil t))
        (setq test-grep-args (match-string 1))))
    (if test-grep-args
        ;; テスト名があったらquickrunを用いて
        ;; $(npm bin)/karma run -- --grep 'テスト名'
        ;; のようなコマンドを実行する
        (quickrun
         :source
         `((:command . "$(npm bin)/karma")
           (:default-directory . ,topdir)
           (:exec . (,(concat "%c run -- --grep " test-grep-args))))))))

あとはkarma startで常時テストサーバを起動させておいた上で、テストファイルを編集中にこのrun-js-mocha-describe-testを実行すれば良いだけ。これで以下のようにEmacs上で一部のテストだけ実行することができるようになった。


まとめ

今回は「手元でJSの全体のテストを実行するのに時間がかかる」という課題を、「今編集中のテストのみをEmacsから実行する」という方法で解決してみた。編集しているテストをサクッと実行できると開発速度が上がるのでおすすめ。

JSのテスト環境をいろいろ試してみてるけど、便利にするためにKarmaとMochaを組み合わせるということをしているために、逆にカスタマイズはやりづらいという面がある。この辺は今後なんかいい感じになってデファクトが便利になってくれるといいなあという気持ちになった。