ミカヅキClojure : dooを使って、楽々ClojureScriptのUnitTest
Clojureではユニットテスト用のフレームワークとコマンドが標準で用意されているので楽チンですが、ClojureScriptはちょっと面倒です。そこで、今回は楽にするdooを紹介。今回は、ちょっと記事が長め。
Clojureには、clojure.test
というユニットテストフレームワークが標準で用意されています。
deftest
マクロなどを使って書いたテストケースはこんな感じです。
(deftest a-test
(testing "FIXME, I fail."
(is (= 0 1))))
あ、これは標準のプロジェクトテンプレートで書かれているものですが、これを下記のlein
コマンドで実行すると、テスト結果が出力されます。
$ lein test
Testing cljs-test.core-test
FAIL in (a-test) (core_test.clj:7)
FIXME, I fail.
expected: 0
actual: 1
diff: - 0
+ 1
Ran 1 tests containing 1 assertions.
1 failures, 0 errors.
Tests failed.
この例だとテスト失敗していますが、 (is (= 0 1))
のところを(is (= 1 1))
にして直すところからテスト駆動な開発を進めていけるわけです。
ClojureScriptのテストは面倒?
ClojureScriptにも標準でcljs.test
というテストフレームワークが用意されているのですが、ちょっと扱い方がやっかいです。
テストについては下記に書かれているのですが、
REPLでやる方法が書かれています。
ただし、lein test
のようにコマンドベースで一括でテストする方法をやりたいと思い始めると、途端に藪が始まります。
ClojureScriptの場合は、lein-cljsbuild
というプラグインを使って、ビルドしたりしているかと思いますが、理想的には
$ lein cljsbuild test
でテストが実行できればいいのでしょう。
これを実現可能にするには、下記のようにいくつかの準備をしないといけません。
- PhantomJSなどの起動JavaScriptスクリプトの用意
- テスト専用の実行HTMLの用意
project.clj
でのビルド設定の用意:test-commands
の設定
テストの方法には、ほかにもfigwheel
を用いた方法などもあります。
上記の方法のサンプルは、lein-cljsbuild
プラグインのサンプルとして用意されています。
僕もこの方法を使っていたのですが、もう少し楽チンなやりかたはないだろうかと探した挙句たどり着いたのが、doo
というプラグインです。
dooは何がいいのか?
doo
は、一言でいうと、テスト用の設定を簡略化してくれるLeiningen用のプラグインです。
doo
を使うことで、下記のような面倒な設定が不要になります。
:test-commands
の用意が不要- 各種JavaScript実行用のオプションがあらかじめ用意されている
- テスト用の専用HTMLや起動JavaScriptの用意が不要
特にJavaScript実行用のオプションの豊富さは目を見張ります。
公式にサポートされているもので下記があります。
- Chrome
- Firefox
- IE
- Safari
- Opera
- Slimer
- PhantomJS
- Node.js
- Rhino / Nashorn
加えて、
- V8
- Jscore
に関して、将来的なサポートが想定されています。
さて、実際に素朴なClojureScriptプロジェクトを作成し、それにテストを組み込んで実行してみます。
今回のゴール
今回は、サンプルプロジェクトを組んだあと、doo
を使ってユニットテストをコマンドラインから走らせる方法を把握します。
具体的には、ClojureScriptを書いて、テストケースを書いて、
$ lein doo phantom test
とコンソールに入力するだけで、PhantomJSを使ったユニットテストが走らせることを可能にします。しかも、ほとんど追加で設定などは不要なので、すごい楽というのを体験します。
サンプルプロジェクトを作成
やることは、
- プロジェクトの用意
- Node.js関係の準備
- ClojureScript用の
project.clj
の定義 - ClojureScriptのコードを記述
- コンパイルしたJavaScriptを開いて実行確認するためのHTML作成
まずは、素朴ClojureScriptプロジェクトを作成します。
プロジェクト名はcljs_tdd
としました。
$ lein new app cljs_tdd
続いて、プロジェクトのディレクトリに移動します。
$ cd cljs_tdd
さて、まずはこのプロジェクト用のpackage.json
を用意しましょう。
ここにテストで利用するPhantomJsの依存関係を記述します。
まず、下記を実行して、package.json
を生成します。
$ npm init
もしnpm init
をはじめて使うという方は、このコマンドを起動すると聞かれるいくつかの質問に答えるとpackage.json
が生成できるものと思っておけばいいでしょう。
$ npm install phantomjs --save-dev
実行時に--save-dev
をつけることで、package.json
に依存関係を自動で記述してもらいます。
これは別のユーザなどが使う際にnpm install
と入力するだけで、必要なNode.jsパッケージがインストール可能にするために大事なことです。
ここまでで出来上がったpackage.json
は下記のようになります。
{
"name": "cljs_tdd",
"version": "1.0.0",
"description": "FIXME: description",
"main": "index.js",
"directories": {
"doc": "doc",
"test": "test"
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "MIT",
"devDependencies": {
"phantomjs": "^2.1.3"
}
}
一回、PhantomJSがインストールされたかどうか、コンソールで実行してみましょう。
下記コマンドをコンソールで打ってみます。
$ node_modules/.bin/phantomjs -v
2.1.1
このようにバージョンが出力されれば成功です。
ちなみに、グローバルでPhantomJSがインストール済みな人は気にする必要はありません。
続いて、project.clj
を下記のようにします。
project.clj
(defproject cljs_tdd "0.1.0-SNAPSHOT"
:description "FIXME: write description"
:url "http://example.com/FIXME"
:license {:name "Eclipse Public License"
:url "http://www.eclipse.org/legal/epl-v10.html"}
:dependencies [[org.clojure/clojure "1.7.0"]
[org.clojure/clojurescript "1.7.48" :exclusions [org.apache.ant/ant]]]
:plugins [[lein-cljsbuild "1.1.3"]]
:cljsbuild {:builds {:dev {:source-paths ["src_cljs"]
:compiler {:output-to "app/js/main.js"
:optimizations :simple}}}}
:clean-targets ["app/js/main.js"]
:main ^:skip-aot cljs-test.core
:target-path "target/%s"
:profiles {:uberjar {:aot :all}})
追加したのは、
- ClojureScriptを依存定義に追加
- Leiningenプラグインとして
lein-cljsbuild
を追加 cljsbuild
用のビルド定義を追加- クリーン用のファイルを追加
といった感じです。ビルド設定は本当に必要最低限しか記述していません。
ここで設定しているのは、src_cljs
以下にClojureScript用のコードを置くこと、そしてJavaScriptのコンパイル結果をapp/js/main.js
に吐き出すことです。
さて、続いてsrc_cljs
以下にClojureScriptコードを書いていきます。src_cljs/cljs_tdd/core.cljs
に下記のようなコードを書きます。
(ns cljs-tdd.core)
;;; 今回テストする素朴な関数
(defn my-fn
"roleとstatusで絞り込む関数"
[m role status]
(if (vector? m)
(->> m
(filterv (fn [x]
(and (= role (:role x))
(= status (:status x))))))
[]))
;;; コンソールに出力
(.log js/console (str (my-fn [{:role 1 :status 0 :name "kara_d"}
{:role 2 :status 0 :name "taro"}
{:role 2 :status 1 :name "hanako"}]
2
1)))
忘れてはいけないポイントは、拡張子を.cljs
にするということです。これをうっかり忘れて全然読み込んでもらえないとかあります。
コードの内容は、下記のような構造のデータを渡した時に、
[{:role 1 :status 0 :name "kara_d"}
{:role 2 :status 0 :name "taro"}
{:role 2 :status 1 :name "hanako"}]
roleとstatusで絞り込んだマップを返すという関数を書いてあります。まあ、深い意味はありません。
これで、サンプルコードとしては終わりで、あとはそれをブラウザで開いてみるHTMLを用意します。app/index.html
を下記のコードで用意してください。
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<title>Document</title>
<script src="js/main.js"></script>
</head>
<body></body>
</html>
中身はコンパイルされたJavaScriptを読み込む以外は何もしていませんし、このHTMLは、単に今回のサンプルの確認用でユニットテストに必要というわけでもありません。
まあ、ここまでで一旦休憩ということで、ClojureScriptをビルドし、ブラウザで確認してみましょう。
$ lein cljsbuild once
無事JavaScriptがapp/js/main.js
として出力されたら、ブラウザでapp/index.html
を開きます。
開発コンソールを表示させて、上のキャプチャのように表示されたら無事サンプルの実装は完了です。
dooでテストランナーを走らせてみる
doo
を使うのは楽なのですが、それでもちょびっとだけ準備が必要です。
必要なことは下記のとおりです。
project.clj
にテスト用のビルド設定を書く- テストケースを書く
doo
用のランナー設定を書く
project.cljの修正
project.clj
に下記の項目を追加します。
- 依存関係として、
[lein-doo "0.1.6"]
を追加 - テスト用のビルド設定、
:test
を追加 - クリーン用のターゲットとして、
:clean-targets
に"app_test/js/main.js"
を追加 doo
用のPhantomJSの実行パスの指定
追加したコードが以下のようになります。
(defproject cljs_tdd "0.1.0-SNAPSHOT"
:description "FIXME: write description"
:url "http://example.com/FIXME"
:license {:name "Eclipse Public License"
:url "http://www.eclipse.org/legal/epl-v10.html"}
:dependencies [[org.clojure/clojure "1.7.0"]
[org.clojure/clojurescript "1.7.48" :exclusions [org.apache.ant/ant]]]
:plugins [[lein-cljsbuild "1.1.3"]
[lein-doo "0.1.6"]]
:cljsbuild {:builds {:dev {:source-paths ["src_cljs"]
:compiler {:output-to "app/js/main.js"
:optimizations :simple}}
:test {:source-paths ["src_cljs" "test_cljs"]
:compiler {:output-to "app_test/js/main.js"
:optimizations :simple
}}}}
:clean-targets ["app/js/main.js"
"app_test/js/main.js"]
:doo {:paths {:phantom "node_modules/.bin/phantomjs"}}
:main ^:skip-aot cljs-test.core
:target-path "target/%s"
:profiles {:uberjar {:aot :all}})
テスト用のビルド設定では、コンパイル後の出力先をapp_test/js/main.js
にしています。これはテスト時に一時的に出力するスクリプトで、本番には使用しません。
また、クリーン時のファイルも同じファイルを削除するようにしています。
テストケースを書く
続いて、いよいよテストケースを書きます。test_cljs/cljs_test/test/core.cljs
を用意し、下記のコードを書きます。
(ns cljs-tdd.test.core
(:require [cljs.test :refer-macros [deftest is testing]]
[cljs-tdd.core :as c]))
(deftest test-my-fn
(testing "roleとstatusで絞り込む"
(testing "hanakoの抽出"
(is (= (c/my-fn [{:role 1 :status 0 :name "kara_d"}
{:role 2 :status 0 :name "taro"}
{:role 2 :status 1 :name "hanako"}]
2
1)
[{:role 2 :status 1 :name "hanako"}])))
(testing "nilの場合"
(is (= (c/my-fn nil nil nil) [])))))
このテストコード自体は、Clojureで書いているテストコードと同様なものです。doo
プラグインのメリットの一つは、テスト形式は新しい形式で書く必要がない点があります。
テストランナー向けのコードを用意する
最後に、ちょっとdoo
ならではのコードを書きます。test_cljs/cljs_test/test/core_runner.cljs
を用意し、下記のコードを書きましょう。やっていることは、doo
にテストするコードのネームスペースを渡しているのみです。
(ns cljs-test.test.core-runner
(:require ;; [cljs.test :as test]
[doo.runner :refer-macros [doo-all-tests doo-tests]]
[cljs-test.test.core]))
(doo-tests 'cljs-test.test.core)
以上で、テストコードの作成は完了です。
テストの実行
さて、テストを実行してみましょう。 下記のコマンドで実行が出来ます。
$ lein doo phantom test
すると、下記のような表示が出たはずです。
今回は、実装済みのサンプルを確認するところから書いたので、成功するテストケースのみ書いています。
各人のスタイルでテストケースを書き進めてみてください。
;; ======================================================================
;; Testing with Phantom:
[{:role 2, :status 1, :name "hanako"}]
Testing cljs-tdd.test.core
Ran 1 tests containing 2 assertions.
0 failures, 0 errors.
無事テストが通りました!
ちなみに、テストコードは監視されており、編集すると再度テストを走らせてくれます。
また、コマンドの第4引数に監視するかどうかを含めることができます。
$ lein doo phantom test auto
だと、監視し、
$ lein doo phantom test once
だと、一度きりのテストとなります。
Colophon
- 編集長
- Greative GK. 原一浩 ( kara_d )
- 製版システム
- Clojure / Compojure / Ring / Enlive / markdown-clj / Jetty / MySQL
- Share this magazine!
- Follow designudge
- Follow @designudge